diff options
| author | Michael Albinus | 2018-07-18 16:51:56 +0200 |
|---|---|---|
| committer | Michael Albinus | 2018-07-18 16:51:56 +0200 |
| commit | 7a258fa0bb9f9e7600229b95ed54ee5ee0bc9a0c (patch) | |
| tree | 9654a226674f2153c703512f1298adc1866d1ceb | |
| parent | cb50077b1eb7c1467f2f200e01599b391d025bfa (diff) | |
| download | emacs-7a258fa0bb9f9e7600229b95ed54ee5ee0bc9a0c.tar.gz emacs-7a258fa0bb9f9e7600229b95ed54ee5ee0bc9a0c.zip | |
Adapt shadowfile.el for Tramp (Bug#4526, Bug#4846)
* etc/NEWS: Mention changes in shadowfile.el.
* lisp/shadowfile.el (top): Require 'tramp instead of 'ange-ftp.
(shadow-cluster): New defstruct.
(shadow-make-cluster, shadow-cluster-name, shadow-cluster-primary)
(shadow-cluster-regexp, shadow-get-user)
(shadow-parse-fullname): Remove.
(shadow-info-file, shadow-todo-file, shadow-system-name)
(shadow-homedir, shadow-regexp-superquote, shadow-suffix)
(shadow-set-cluster, shadow-get-cluster, shadow-site-name)
(shadow-name-site, shadow-site-primary, shadow-site-cluster)
(shadow-read-site, shadow-parse-name, shadow-make-fullname)
(shadow-replace-name-component, shadow-local-file)
(shadow-expand-cluster-in-file-name, shadow-contract-file-name)
(shadow-same-site, shadow-file-match, shadow-define-cluster)
(shadow-define-literal-group, shadow-define-regexp-group)
(shadow-make-group, shadow-shadows-of-1, shadow-read-files)
(shadow-write-info-file, shadow-write-todo-file)
(shadow-initialize): Adapt variables and functions.
* test/lisp/shadowfile-tests.el: New file.
| -rw-r--r-- | etc/NEWS | 8 | ||||
| -rw-r--r-- | lisp/shadowfile.el | 462 | ||||
| -rw-r--r-- | test/lisp/shadowfile-tests.el | 876 |
3 files changed, 1114 insertions, 232 deletions
| @@ -94,12 +94,20 @@ it now shows the global revision number, in the form of its changeset | |||
| 94 | hash value. To get back the previous behavior, customize the new | 94 | hash value. To get back the previous behavior, customize the new |
| 95 | option 'vc-hg-symbolic-revision-styles' to the value '("{rev}")'. | 95 | option 'vc-hg-symbolic-revision-styles' to the value '("{rev}")'. |
| 96 | 96 | ||
| 97 | --- | ||
| 98 | ** shadowfile.el has been rewritten to support Tramp file names. | ||
| 99 | |||
| 97 | 100 | ||
| 98 | * New Modes and Packages in Emacs 26.2 | 101 | * New Modes and Packages in Emacs 26.2 |
| 99 | 102 | ||
| 100 | 103 | ||
| 101 | * Incompatible Lisp Changes in Emacs 26.2 | 104 | * Incompatible Lisp Changes in Emacs 26.2 |
| 102 | 105 | ||
| 106 | --- | ||
| 107 | ** shadowfile config files have changed their syntax. | ||
| 108 | Existing files "~/.emacs.d/shadows" and "~/.emacs.d/shadow_todo" must | ||
| 109 | be removed prior using the changed 'shadow-*' commands. | ||
| 110 | |||
| 103 | 111 | ||
| 104 | * Lisp Changes in Emacs 26.2 | 112 | * Lisp Changes in Emacs 26.2 |
| 105 | 113 | ||
diff --git a/lisp/shadowfile.el b/lisp/shadowfile.el index 0095d6959ef..e1a9b8e1d98 100644 --- a/lisp/shadowfile.el +++ b/lisp/shadowfile.el | |||
| @@ -25,37 +25,38 @@ | |||
| 25 | ;; This package helps you to keep identical copies of files in more than one | 25 | ;; This package helps you to keep identical copies of files in more than one |
| 26 | ;; place - possibly on different machines. When you save a file, it checks | 26 | ;; place - possibly on different machines. When you save a file, it checks |
| 27 | ;; whether it is on the list of files with "shadows", and if so, it tries to | 27 | ;; whether it is on the list of files with "shadows", and if so, it tries to |
| 28 | ;; copy it when you exit Emacs (or use the shadow-copy-files command). | 28 | ;; copy it when you exit Emacs (or use the `shadow-copy-files' command). |
| 29 | 29 | ||
| 30 | ;; Installation & Use: | 30 | ;; Installation & Use: |
| 31 | 31 | ||
| 32 | ;; Add clusters (if necessary) and file groups with shadow-define-cluster, | 32 | ;; Add clusters (if necessary) and file groups with `shadow-define-cluster', |
| 33 | ;; shadow-define-literal-group, and shadow-define-regexp-group (see the | 33 | ;; `shadow-define-literal-group', and `shadow-define-regexp-group' (see the |
| 34 | ;; documentation for these functions for information on how and when to use | 34 | ;; documentation for these functions for information on how and when to use |
| 35 | ;; them). After doing this once, everything should be automatic. | 35 | ;; them). After doing this once, everything should be automatic. |
| 36 | 36 | ||
| 37 | ;; The lists of clusters and shadows are saved in a ~/.emacs.d/shadows | 37 | ;; The lists of clusters and shadows are saved in `shadow-info-file', |
| 38 | ;; (`shadow-info-file') file, so that they can be remembered from one | 38 | ;; so that they can be remembered from one Emacs session to another, |
| 39 | ;; Emacs session to another, even (as much as possible) if the Emacs | 39 | ;; even (as much as possible) if the Emacs session terminates |
| 40 | ;; session terminates abnormally. The files needing to be copied are | 40 | ;; abnormally. The files needing to be copied are stored in |
| 41 | ;; stored in `shadow-todo-file'; if a file cannot be copied for any | 41 | ;; `shadow-todo-file'; if a file cannot be copied for any reason, it |
| 42 | ;; reason, it will stay on the list to be tried again next time. The | 42 | ;; will stay on the list to be tried again next time. The |
| 43 | ;; `shadow-info-file' file should itself have shadows on all your accounts | 43 | ;; `shadow-info-file' file should itself have shadows on all your |
| 44 | ;; so that the information in it is consistent everywhere, but | 44 | ;; accounts so that the information in it is consistent everywhere, |
| 45 | ;; `shadow-todo-file' is local information and should have no shadows. | 45 | ;; but `shadow-todo-file' is local information and should have no |
| 46 | ;; shadows. | ||
| 46 | 47 | ||
| 47 | ;; If you do not want to copy a particular file, you can answer "no" and | 48 | ;; If you do not want to copy a particular file, you can answer "no" and |
| 48 | ;; be asked again next time you hit C-x 4 s or exit Emacs. If you do not | 49 | ;; be asked again next time you hit "C-x 4 s" or exit Emacs. If you do not |
| 49 | ;; want to be asked again, use shadow-cancel, and you will not be asked | 50 | ;; want to be asked again, use "M-x shadow-cancel", and you will not be asked |
| 50 | ;; until you change the file and save it again. If you do not want to | 51 | ;; until you change the file and save it again. If you do not want to |
| 51 | ;; shadow that file ever again, you can edit it out of the shadows | 52 | ;; shadow that file ever again, you can edit it out of the shadows |
| 52 | ;; buffer. Anytime you edit the shadows buffer, you must type M-x | 53 | ;; buffer. Anytime you edit the shadows buffer, you must type "M-x |
| 53 | ;; shadow-read-files to load in the new information, or your changes will | 54 | ;; shadow-read-files" to load in the new information, or your changes will |
| 54 | ;; be overwritten! | 55 | ;; be overwritten! |
| 55 | 56 | ||
| 56 | ;; Bugs & Warnings: | 57 | ;; Bugs & Warnings: |
| 57 | ;; | 58 | ;; |
| 58 | ;; - It is bad to have two emacses both running shadowfile at the same | 59 | ;; - It is bad to have two Emacsen both running shadowfile at the same |
| 59 | ;; time. It tries to detect this condition, but is not always successful. | 60 | ;; time. It tries to detect this condition, but is not always successful. |
| 60 | ;; | 61 | ;; |
| 61 | ;; - You have to be careful not to edit a file in two locations | 62 | ;; - You have to be careful not to edit a file in two locations |
| @@ -64,19 +65,16 @@ | |||
| 64 | ;; | 65 | ;; |
| 65 | ;; - It ought to check modification times of both files to make sure | 66 | ;; - It ought to check modification times of both files to make sure |
| 66 | ;; it is doing the right thing. This will have to wait until | 67 | ;; it is doing the right thing. This will have to wait until |
| 67 | ;; file-newer-than-file-p works between machines. | 68 | ;; `file-newer-than-file-p' works between machines. |
| 68 | ;; | 69 | ;; |
| 69 | ;; - It will not make directories for you, it just fails to copy files | 70 | ;; - It will not make directories for you, it just fails to copy files |
| 70 | ;; that belong in non-existent directories. | 71 | ;; that belong in non-existent directories. |
| 71 | ;; | ||
| 72 | ;; Please report any bugs to me (boris@gnu.org). Also let me know | ||
| 73 | ;; if you have suggestions or would like to be informed of updates. | ||
| 74 | 72 | ||
| 75 | 73 | ||
| 76 | ;;; Code: | 74 | ;;; Code: |
| 77 | 75 | ||
| 78 | (require 'cl-lib) | 76 | (require 'cl-lib) |
| 79 | (require 'ange-ftp) | 77 | (require 'tramp) |
| 80 | 78 | ||
| 81 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | 79 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; |
| 82 | ;;; Variables | 80 | ;;; Variables |
| @@ -107,35 +105,35 @@ files that have been changed and need to be copied to other systems." | |||
| 107 | :type 'boolean | 105 | :type 'boolean |
| 108 | :group 'shadow) | 106 | :group 'shadow) |
| 109 | 107 | ||
| 110 | ;; FIXME in a sense, this changed in 24.4 (addition of locate-user-emacs-file), | 108 | (defcustom shadow-info-file (locate-user-emacs-file "shadows" ".shadows") |
| 111 | ;; but due to the weird way this variable is initialized to nil, it didn't | ||
| 112 | ;; literally change. Same for shadow-todo-file. | ||
| 113 | (defcustom shadow-info-file nil | ||
| 114 | "File to keep shadow information in. | 109 | "File to keep shadow information in. |
| 115 | The `shadow-info-file' should be shadowed to all your accounts to | 110 | The `shadow-info-file' should be shadowed to all your accounts to |
| 116 | ensure consistency. Default: ~/.emacs.d/shadows" | 111 | ensure consistency. Default: ~/.emacs.d/shadows" |
| 117 | :type '(choice (const nil) file) | 112 | :type 'file |
| 118 | :group 'shadow) | 113 | :group 'shadow |
| 114 | :version "26.2") | ||
| 119 | 115 | ||
| 120 | (defcustom shadow-todo-file nil | 116 | (defcustom shadow-todo-file |
| 117 | (locate-user-emacs-file "shadow_todo" ".shadow_todo") | ||
| 121 | "File to store the list of uncopied shadows in. | 118 | "File to store the list of uncopied shadows in. |
| 122 | This means that if a remote system is down, or for any reason you cannot or | 119 | This means that if a remote system is down, or for any reason you cannot or |
| 123 | decide not to copy your shadow files at the end of one Emacs session, it will | 120 | decide not to copy your shadow files at the end of one Emacs session, it will |
| 124 | remember and ask you again in your next Emacs session. | 121 | remember and ask you again in your next Emacs session. |
| 125 | This file must NOT be shadowed to any other system, it is host-specific. | 122 | This file must NOT be shadowed to any other system, it is host-specific. |
| 126 | Default: ~/.emacs.d/shadow_todo" | 123 | Default: ~/.emacs.d/shadow_todo" |
| 127 | :type '(choice (const nil) file) | 124 | :type 'file |
| 128 | :group 'shadow) | 125 | :group 'shadow |
| 126 | :version "26.2") | ||
| 129 | 127 | ||
| 130 | 128 | ||
| 131 | ;;; The following two variables should in most cases initialize themselves | 129 | ;;; The following two variables should in most cases initialize themselves |
| 132 | ;;; correctly. They are provided as variables in case the defaults are wrong | 130 | ;;; correctly. They are provided as variables in case the defaults are wrong |
| 133 | ;;; on your machine (and for efficiency). | 131 | ;;; on your machine (and for efficiency). |
| 134 | 132 | ||
| 135 | (defvar shadow-system-name (system-name) | 133 | (defvar shadow-system-name (concat "/" (system-name) ":") |
| 136 | "The complete hostname of this machine.") | 134 | "The identification for local files on this machine.") |
| 137 | 135 | ||
| 138 | (defvar shadow-homedir nil | 136 | (defvar shadow-homedir "~" |
| 139 | "Your home directory on this machine.") | 137 | "Your home directory on this machine.") |
| 140 | 138 | ||
| 141 | ;;; | 139 | ;;; |
| @@ -186,12 +184,12 @@ created by `shadow-define-regexp-group'.") | |||
| 186 | (car list)) | 184 | (car list)) |
| 187 | 185 | ||
| 188 | (defun shadow-regexp-superquote (string) | 186 | (defun shadow-regexp-superquote (string) |
| 189 | "Like `regexp-quote', but includes the ^ and $. | 187 | "Like `regexp-quote', but includes the \\` and \\'. |
| 190 | This makes sure regexp matches nothing but STRING." | 188 | This makes sure regexp matches nothing but STRING." |
| 191 | (concat "^" (regexp-quote string) "$")) | 189 | (concat "\\`" (regexp-quote string) "\\'")) |
| 192 | 190 | ||
| 193 | (defun shadow-suffix (prefix string) | 191 | (defun shadow-suffix (prefix string) |
| 194 | "If PREFIX begins STRING, return the rest. | 192 | "If PREFIX begins with STRING, return the rest. |
| 195 | Return value is non-nil if PREFIX and STRING are `string=' up to the length of | 193 | Return value is non-nil if PREFIX and STRING are `string=' up to the length of |
| 196 | PREFIX." | 194 | PREFIX." |
| 197 | (let ((lp (length prefix)) | 195 | (let ((lp (length prefix)) |
| @@ -204,70 +202,66 @@ PREFIX." | |||
| 204 | ;;; Clusters and sites | 202 | ;;; Clusters and sites |
| 205 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | 203 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; |
| 206 | 204 | ||
| 207 | ;;; I use the term `site' to refer to a string which may be the name of a | 205 | ;;; I use the term `site' to refer to a string which may be the |
| 208 | ;;; cluster or a literal hostname. All user-level commands should accept | 206 | ;;; cluster identification "/name:", a remote identification |
| 209 | ;;; either. | 207 | ;;; "/method:user@host:", or "/system-name:' (the value of |
| 210 | 208 | ;;; `shadow-system-name') for the location of local files. All | |
| 211 | (defun shadow-make-cluster (name primary regexp) | 209 | ;;; user-level commands should accept either. |
| 212 | "Create a shadow cluster. | ||
| 213 | It is called NAME, uses the PRIMARY hostname and REGEXP matching all | ||
| 214 | hosts in the cluster. The variable `shadow-clusters' associates the | ||
| 215 | names of clusters to these structures. This function is for program | ||
| 216 | use: to create clusters interactively, use `shadow-define-cluster' | ||
| 217 | instead." | ||
| 218 | (list name primary regexp)) | ||
| 219 | |||
| 220 | (defmacro shadow-cluster-name (cluster) | ||
| 221 | "Return the name of the CLUSTER." | ||
| 222 | (list 'elt cluster 0)) | ||
| 223 | 210 | ||
| 224 | (defmacro shadow-cluster-primary (cluster) | 211 | (cl-defstruct (shadow-cluster (:type list) :named) name primary regexp) |
| 225 | "Return the primary hostname of a CLUSTER." | ||
| 226 | (list 'elt cluster 1)) | ||
| 227 | |||
| 228 | (defmacro shadow-cluster-regexp (cluster) | ||
| 229 | "Return the regexp matching hosts in a CLUSTER." | ||
| 230 | (list 'elt cluster 2)) | ||
| 231 | 212 | ||
| 232 | (defun shadow-set-cluster (name primary regexp) | 213 | (defun shadow-set-cluster (name primary regexp) |
| 233 | "Put cluster NAME on the list of clusters. | 214 | "Put cluster NAME on the list of clusters. |
| 234 | Replace old definition, if any. PRIMARY and REGEXP are the | 215 | Replace old definition, if any. PRIMARY and REGEXP are the |
| 235 | information defining the cluster. For interactive use, call | 216 | information defining the cluster. For interactive use, call |
| 236 | `shadow-define-cluster' instead." | 217 | `shadow-define-cluster' instead." |
| 237 | (let ((rest (cl-remove-if (lambda (x) (equal name (car x))) | 218 | (let ((rest (cl-remove-if (lambda (x) (equal name (shadow-cluster-name x))) |
| 238 | shadow-clusters))) | 219 | shadow-clusters))) |
| 239 | (setq shadow-clusters | 220 | (setq shadow-clusters |
| 240 | (cons (shadow-make-cluster name primary regexp) | 221 | (cons (make-shadow-cluster :name name :primary primary :regexp regexp) |
| 241 | rest)))) | 222 | rest)))) |
| 242 | 223 | ||
| 243 | (defmacro shadow-get-cluster (name) | 224 | (defun shadow-get-cluster (name) |
| 244 | "Return cluster named NAME, or nil." | 225 | "Return cluster named NAME, or nil." |
| 245 | (list 'assoc name 'shadow-clusters)) | 226 | (shadow-find |
| 227 | (lambda (x) (string-equal (shadow-cluster-name x) name)) | ||
| 228 | shadow-clusters)) | ||
| 229 | |||
| 230 | ;;; SITES | ||
| 231 | |||
| 232 | (defun shadow-site-name (site) | ||
| 233 | "Return name if SITE has the form \"/name:\", otherwise SITE." | ||
| 234 | (if (string-match "\\`/\\(\\w+\\):\\'" site) | ||
| 235 | (match-string 1 site) site)) | ||
| 236 | |||
| 237 | (defun shadow-name-site (name) | ||
| 238 | "Return \"/name:\" if NAME has word syntax, otherwise NAME." | ||
| 239 | (if (string-match "\\`\\w+\\'" name) | ||
| 240 | (format "/%s:"name) name)) | ||
| 246 | 241 | ||
| 247 | (defun shadow-site-primary (site) | 242 | (defun shadow-site-primary (site) |
| 248 | "If SITE is a cluster, return primary host, otherwise return SITE." | 243 | "If SITE is a cluster, return primary identification, otherwise return SITE." |
| 249 | (let ((c (shadow-get-cluster site))) | 244 | (let ((cluster (shadow-get-cluster (shadow-site-name site)))) |
| 250 | (if c | 245 | (if cluster |
| 251 | (shadow-cluster-primary c) | 246 | (shadow-cluster-primary cluster) |
| 252 | site))) | 247 | site))) |
| 253 | 248 | ||
| 254 | ;;; SITES | ||
| 255 | |||
| 256 | (defun shadow-site-cluster (site) | 249 | (defun shadow-site-cluster (site) |
| 257 | "Given a SITE (hostname or cluster name), return cluster it is in, or nil." | 250 | "Given a SITE, return cluster it is in, or nil." |
| 258 | (or (assoc site shadow-clusters) | 251 | (or (shadow-get-cluster (shadow-site-name site)) |
| 259 | (shadow-find | 252 | (shadow-find |
| 260 | (function (lambda (x) | 253 | (lambda (x) |
| 261 | (string-match (shadow-cluster-regexp x) | 254 | (string-match (shadow-cluster-regexp x) (shadow-name-site site))) |
| 262 | site))) | ||
| 263 | shadow-clusters))) | 255 | shadow-clusters))) |
| 264 | 256 | ||
| 265 | (defun shadow-read-site () | 257 | (defun shadow-read-site () |
| 266 | "Read a cluster name or hostname from the minibuffer." | 258 | "Read a cluster name or host identification from the minibuffer." |
| 267 | (let ((ans (completing-read "Host or cluster name [RET when done]: " | 259 | (let ((ans (completing-read "Host identification or cluster name: " |
| 268 | shadow-clusters))) | 260 | shadow-clusters))) |
| 269 | (if (equal "" ans) | 261 | (when (or (shadow-get-cluster (shadow-site-name ans)) |
| 270 | nil | 262 | (string-equal ans shadow-system-name) |
| 263 | (string-equal ans (shadow-site-name shadow-system-name)) | ||
| 264 | (setq ans (file-remote-p ans))) | ||
| 271 | ans))) | 265 | ans))) |
| 272 | 266 | ||
| 273 | (defun shadow-site-match (site1 site2) | 267 | (defun shadow-site-match (site1 site2) |
| @@ -281,63 +275,95 @@ be matched against the primary of SITE2." | |||
| 281 | (string-match (shadow-cluster-regexp cluster1) primary2) | 275 | (string-match (shadow-cluster-regexp cluster1) primary2) |
| 282 | (string-equal site1 primary2))))) | 276 | (string-equal site1 primary2))))) |
| 283 | 277 | ||
| 284 | (defun shadow-get-user (site) | ||
| 285 | "Return the default username for a SITE." | ||
| 286 | (ange-ftp-get-user (shadow-site-primary site))) | ||
| 287 | |||
| 288 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | 278 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; |
| 289 | ;;; Filename manipulation | 279 | ;;; Filename manipulation |
| 290 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | 280 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; |
| 291 | 281 | ||
| 292 | (defun shadow-parse-fullname (fullname) | ||
| 293 | "Parse FULLNAME into (site user path) list. | ||
| 294 | Leave it alone if it already is one. Return nil if the argument is | ||
| 295 | not a full ange-ftp pathname." | ||
| 296 | (if (listp fullname) | ||
| 297 | fullname | ||
| 298 | (ange-ftp-ftp-name fullname))) | ||
| 299 | |||
| 300 | (defun shadow-parse-name (name) | 282 | (defun shadow-parse-name (name) |
| 301 | "Parse any NAME into (site user name) list. | 283 | "Parse any NAME into a `tramp-file-name' structure. |
| 302 | Argument can be a simple name, full ange-ftp name, or already a hup list." | 284 | Argument can be a simple name, remote file name, or already a |
| 303 | (or (shadow-parse-fullname name) | 285 | `tramp-file-name' structure." |
| 304 | (list shadow-system-name | 286 | (cond |
| 305 | (user-login-name) | 287 | ((null name) nil) |
| 306 | name))) | 288 | ((tramp-file-name-p name) name) |
| 307 | 289 | ((file-remote-p name) (tramp-dissect-file-name name)) | |
| 308 | (defsubst shadow-make-fullname (host user name) | 290 | ((shadow-local-file name) |
| 309 | "Make an ange-ftp style fullname out of HOST, USER (optional), and NAME. | 291 | (make-tramp-file-name |
| 310 | This is probably not as general as it ought to be." | 292 | :host (shadow-site-name shadow-system-name) |
| 311 | (concat "/" | 293 | :localname (shadow-local-file name))) |
| 312 | (if user (concat user "@")) | 294 | ;; Cluster name. |
| 313 | host ":" | 295 | ((string-match "^/\\([^:/]+\\):\\([^:]*\\)$" name) |
| 314 | name)) | 296 | (let ((name (match-string 1 name)) |
| 297 | (file (match-string 2 name))) | ||
| 298 | (when (shadow-get-cluster name) | ||
| 299 | (make-tramp-file-name :host name :localname file)))))) | ||
| 300 | |||
| 301 | (defsubst shadow-make-fullname (hup &optional host name) | ||
| 302 | "Make a Tramp style fullname out of HUP, a `tramp-file-name' structure. | ||
| 303 | Replace HOST, and NAME when non-nil." | ||
| 304 | (let ((hup (copy-tramp-file-name hup))) | ||
| 305 | (when host (setf (tramp-file-name-host hup) host)) | ||
| 306 | (when name (setf (tramp-file-name-localname hup) name)) | ||
| 307 | (if (null (tramp-file-name-method hup)) | ||
| 308 | (format | ||
| 309 | "/%s:%s" (tramp-file-name-host hup) (tramp-file-name-localname hup)) | ||
| 310 | (tramp-make-tramp-file-name | ||
| 311 | (tramp-file-name-method hup) | ||
| 312 | (tramp-file-name-user hup) | ||
| 313 | (tramp-file-name-domain hup) | ||
| 314 | (tramp-file-name-host hup) | ||
| 315 | (tramp-file-name-port hup) | ||
| 316 | (tramp-file-name-localname hup) | ||
| 317 | (tramp-file-name-hop hup))))) | ||
| 315 | 318 | ||
| 316 | (defun shadow-replace-name-component (fullname newname) | 319 | (defun shadow-replace-name-component (fullname newname) |
| 317 | "Return FULLNAME with the name component changed to NEWNAME." | 320 | "Return FULLNAME with the name component changed to NEWNAME." |
| 318 | (let ((hup (shadow-parse-fullname fullname))) | 321 | (concat (file-remote-p fullname) newname)) |
| 319 | (shadow-make-fullname (nth 0 hup) (nth 1 hup) newname))) | ||
| 320 | 322 | ||
| 321 | (defun shadow-local-file (file) | 323 | (defun shadow-local-file (file) |
| 322 | "If FILE is at this site, remove /user@host part. | 324 | "If FILE is not remote, return it. |
| 323 | If refers to a different system or a different user on this system, | 325 | If it refers to a different system, return nil." |
| 324 | return nil." | 326 | (cond |
| 325 | (let ((hup (shadow-parse-fullname file))) | 327 | ((null file) nil) |
| 326 | (cond ((null hup) file) | 328 | ;; `tramp-file-name' structure. |
| 327 | ((and (shadow-site-match (nth 0 hup) shadow-system-name) | 329 | ((and (tramp-file-name-p file) (null (tramp-file-name-method file))) |
| 328 | (string-equal (nth 1 hup) (user-login-name))) | 330 | (tramp-file-name-localname file)) |
| 329 | (nth 2 hup)) | 331 | ((tramp-file-name-p file) nil) |
| 330 | (t nil)))) | 332 | ;; Local host name. |
| 333 | ((string-match | ||
| 334 | (format "^%s\\([^:]*\\)$" (regexp-quote shadow-system-name)) file) | ||
| 335 | (match-string 1 file)) | ||
| 336 | ;; Cluster name. | ||
| 337 | ((and (string-match "^/\\([^:/]+\\):\\([^:]*\\)$" file) | ||
| 338 | (shadow-get-cluster (match-string 1 file))) | ||
| 339 | (let ((file (match-string 2 file)) | ||
| 340 | (primary | ||
| 341 | (shadow-cluster-primary | ||
| 342 | (shadow-get-cluster (match-string 1 file))))) | ||
| 343 | (when (string-equal primary shadow-system-name) (setq primary nil)) | ||
| 344 | (shadow-local-file (concat primary file)))) | ||
| 345 | ;; Local name. | ||
| 346 | ((null (file-remote-p file)) file))) | ||
| 331 | 347 | ||
| 332 | (defun shadow-expand-cluster-in-file-name (file) | 348 | (defun shadow-expand-cluster-in-file-name (file) |
| 333 | "If hostname part of FILE is a cluster, expand it to cluster's primary hostname. | 349 | "If hostname part of FILE is a cluster, expand it to cluster's primary hostname. |
| 334 | Will return the name bare if it is a local file." | 350 | Will return the name bare if it is a local file." |
| 335 | (let ((hup (shadow-parse-name file))) | 351 | (when (stringp file) |
| 336 | (cond ((null hup) file) | 352 | (cond |
| 337 | ((shadow-local-file hup)) | 353 | ;; Local file. |
| 338 | ((shadow-make-fullname (shadow-site-primary (nth 0 hup)) | 354 | ((shadow-local-file file)) |
| 339 | (nth 1 hup) | 355 | ;; Cluster name. |
| 340 | (nth 2 hup)))))) | 356 | ((string-match "^\\(/[^:/]+:\\)[^:]*$" file) |
| 357 | (let ((primary | ||
| 358 | (save-match-data | ||
| 359 | (shadow-cluster-primary | ||
| 360 | (shadow-get-cluster | ||
| 361 | (shadow-site-name (match-string 1 file))))))) | ||
| 362 | (if (not primary) | ||
| 363 | file | ||
| 364 | (setq file (replace-match primary nil nil file 1)) | ||
| 365 | (or (shadow-local-file file) file)))) | ||
| 366 | (t file)))) | ||
| 341 | 367 | ||
| 342 | (defun shadow-expand-file-name (file &optional default) | 368 | (defun shadow-expand-file-name (file &optional default) |
| 343 | "Expand file name and get FILE's true name." | 369 | "Expand file name and get FILE's true name." |
| @@ -352,46 +378,50 @@ true." | |||
| 352 | (homedir (if (shadow-local-file hup) | 378 | (homedir (if (shadow-local-file hup) |
| 353 | shadow-homedir | 379 | shadow-homedir |
| 354 | (file-name-as-directory | 380 | (file-name-as-directory |
| 355 | (nth 2 (shadow-parse-fullname | 381 | (file-local-name |
| 356 | (expand-file-name | 382 | (expand-file-name (shadow-make-fullname hup nil "~")))))) |
| 357 | (shadow-make-fullname | 383 | (suffix (shadow-suffix homedir (tramp-file-name-localname hup))) |
| 358 | (nth 0 hup) (nth 1 hup) "~"))))))) | 384 | (cluster (shadow-site-cluster (shadow-make-fullname hup nil "")))) |
| 359 | (suffix (shadow-suffix homedir (nth 2 hup))) | 385 | (when cluster |
| 360 | (cluster (shadow-site-cluster (nth 0 hup)))) | 386 | (setf (tramp-file-name-method hup) nil |
| 387 | (tramp-file-name-host hup) (shadow-cluster-name cluster))) | ||
| 361 | (shadow-make-fullname | 388 | (shadow-make-fullname |
| 362 | (if cluster | 389 | hup nil |
| 363 | (shadow-cluster-name cluster) | ||
| 364 | (nth 0 hup)) | ||
| 365 | (nth 1 hup) | ||
| 366 | (if suffix | 390 | (if suffix |
| 367 | (concat "~/" suffix) | 391 | (concat "~/" suffix) |
| 368 | (nth 2 hup))))) | 392 | (tramp-file-name-localname hup))))) |
| 369 | 393 | ||
| 370 | (defun shadow-same-site (pattern file) | 394 | (defun shadow-same-site (pattern file) |
| 371 | "True if the site of PATTERN and of FILE are on the same site. | 395 | "True if the site of PATTERN and of FILE are on the same site. |
| 372 | If usernames are supplied, they must also match exactly. PATTERN and FILE may | 396 | PATTERN and FILE may be Tramp vectors, or remote file names. |
| 373 | be lists of host, user, name, or ange-ftp file names. FILE may also be just a | 397 | FILE may also be just a local filename." |
| 374 | local filename." | 398 | (let ((pattern-sup (shadow-parse-name pattern)) |
| 375 | (let ((pattern-sup (shadow-parse-fullname pattern)) | ||
| 376 | (file-sup (shadow-parse-name file))) | 399 | (file-sup (shadow-parse-name file))) |
| 377 | (and | 400 | (and |
| 378 | (shadow-site-match (nth 0 pattern-sup) (nth 0 file-sup)) | 401 | (shadow-site-match |
| 379 | (or (null (nth 1 pattern-sup)) | 402 | (tramp-file-name-host pattern-sup) (tramp-file-name-host file-sup)) |
| 380 | (string-equal (nth 1 pattern-sup) (nth 1 file-sup)))))) | 403 | (or (null (tramp-file-name-user pattern-sup)) |
| 404 | (string-equal | ||
| 405 | (tramp-file-name-user pattern-sup) | ||
| 406 | (tramp-file-name-user file-sup)))))) | ||
| 381 | 407 | ||
| 382 | (defun shadow-file-match (pattern file &optional regexp) | 408 | (defun shadow-file-match (pattern file &optional regexp) |
| 383 | "Return t if PATTERN matches FILE. | 409 | "Return t if PATTERN matches FILE. |
| 384 | If REGEXP is supplied and non-nil, the file part of the pattern is a regular | 410 | If REGEXP is supplied and non-nil, the file part of the pattern is a regular |
| 385 | expression, otherwise it must match exactly. The sites and usernames must | 411 | expression, otherwise it must match exactly. The sites must |
| 386 | match---see `shadow-same-site'. The pattern must be in full ange-ftp format, | 412 | match---see `shadow-same-site'. The pattern must be in full Tramp format, |
| 387 | but the file can be any valid filename. This function does not do any | 413 | but the file can be any valid filename. This function does not do any |
| 388 | filename expansion or contraction, you must do that yourself first." | 414 | filename expansion or contraction, you must do that yourself first." |
| 389 | (let* ((pattern-sup (shadow-parse-fullname pattern)) | 415 | (let* ((pattern-sup (shadow-parse-name pattern)) |
| 390 | (file-sup (shadow-parse-name file))) | 416 | (file-sup (shadow-parse-name file))) |
| 391 | (and (shadow-same-site pattern-sup file-sup) | 417 | (and (shadow-same-site pattern-sup file-sup) |
| 392 | (if regexp | 418 | (if regexp |
| 393 | (string-match (nth 2 pattern-sup) (nth 2 file-sup)) | 419 | (string-match |
| 394 | (string-equal (nth 2 pattern-sup) (nth 2 file-sup)))))) | 420 | (tramp-file-name-localname pattern-sup) |
| 421 | (tramp-file-name-localname file-sup)) | ||
| 422 | (string-equal | ||
| 423 | (tramp-file-name-localname pattern-sup) | ||
| 424 | (tramp-file-name-localname file-sup)))))) | ||
| 395 | 425 | ||
| 396 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | 426 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; |
| 397 | ;;; User-level Commands | 427 | ;;; User-level Commands |
| @@ -405,30 +435,34 @@ one of them is sufficient to update the file on all of them. Clusters are | |||
| 405 | defined by a name, the network address of a primary host (the one we copy | 435 | defined by a name, the network address of a primary host (the one we copy |
| 406 | files to), and a regular expression that matches the hostnames of all the | 436 | files to), and a regular expression that matches the hostnames of all the |
| 407 | sites in the cluster." | 437 | sites in the cluster." |
| 408 | (interactive (list (completing-read "Cluster name: " shadow-clusters () ()))) | 438 | (interactive (list (completing-read "Cluster name: " shadow-clusters))) |
| 409 | (let* ((old (shadow-get-cluster name)) | 439 | (let* ((old (shadow-get-cluster name)) |
| 410 | (primary (read-string "Primary host: " | 440 | (primary (let (try-primary) |
| 411 | (if old (shadow-cluster-primary old) | 441 | (while (not |
| 412 | name))) | 442 | (or |
| 413 | (regexp (let (try-regexp) | 443 | (string-equal |
| 414 | (while (not | 444 | (setq try-primary |
| 415 | (string-match | ||
| 416 | (setq try-regexp | ||
| 417 | (read-string | 445 | (read-string |
| 418 | "Regexp matching all host names: " | 446 | "Primary host: " |
| 419 | (if old (shadow-cluster-regexp old) | 447 | (if old (shadow-cluster-primary old) |
| 420 | (shadow-regexp-superquote primary)))) | 448 | name))) |
| 421 | primary)) | 449 | shadow-system-name) |
| 422 | (message "Regexp doesn't include the primary host!") | 450 | (file-remote-p try-primary))) |
| 423 | (sit-for 2)) | 451 | (message "Not a valid primary!") |
| 424 | try-regexp)) | 452 | (sit-for 2)) |
| 425 | ; (username (read-no-blanks-input | 453 | try-primary)) |
| 426 | ; (format "Username (default %s): " | 454 | (regexp (let (try-regexp) |
| 427 | ; (shadow-get-user primary)) | 455 | (while (not |
| 428 | ; (if old (or (shadow-cluster-username old) "") | 456 | (string-match |
| 429 | ; (user-login-name)))) | 457 | (setq try-regexp |
| 430 | ) | 458 | (read-string |
| 431 | ; (if (string-equal "" username) (setq username nil)) | 459 | "Regexp matching all host names: " |
| 460 | (if old (shadow-cluster-regexp old) | ||
| 461 | (shadow-regexp-superquote primary)))) | ||
| 462 | primary)) | ||
| 463 | (message "Regexp doesn't include the primary host!") | ||
| 464 | (sit-for 2)) | ||
| 465 | try-regexp))) | ||
| 432 | (shadow-set-cluster name primary regexp))) | 466 | (shadow-set-cluster name primary regexp))) |
| 433 | 467 | ||
| 434 | ;;;###autoload | 468 | ;;;###autoload |
| @@ -438,20 +472,14 @@ It may have different filenames on each site. When this file is edited, the | |||
| 438 | new version will be copied to each of the other locations. Sites can be | 472 | new version will be copied to each of the other locations. Sites can be |
| 439 | specific hostnames, or names of clusters (see `shadow-define-cluster')." | 473 | specific hostnames, or names of clusters (see `shadow-define-cluster')." |
| 440 | (interactive) | 474 | (interactive) |
| 441 | (let* ((hup (shadow-parse-fullname | 475 | (let* ((hup (shadow-parse-name |
| 442 | (shadow-contract-file-name (buffer-file-name)))) | 476 | (shadow-contract-file-name (buffer-file-name)))) |
| 443 | (name (nth 2 hup)) | 477 | (name (tramp-file-name-localname hup)) |
| 444 | user site group) | 478 | site group) |
| 445 | (while (setq site (shadow-read-site)) | 479 | (while (setq site (shadow-read-site)) |
| 446 | (setq user (read-string (format "Username (default %s): " | 480 | (setq name (read-string "Filename: " name) |
| 447 | (shadow-get-user site))) | 481 | hup (shadow-parse-name (shadow-contract-file-name name)) |
| 448 | name (read-string "Filename: " name)) | 482 | group (cons (shadow-make-fullname hup site) group))) |
| 449 | (setq group (cons (shadow-make-fullname site | ||
| 450 | (if (string-equal "" user) | ||
| 451 | (shadow-get-user site) | ||
| 452 | user) | ||
| 453 | name) | ||
| 454 | group))) | ||
| 455 | (setq shadow-literal-groups (cons group shadow-literal-groups))) | 483 | (setq shadow-literal-groups (cons group shadow-literal-groups))) |
| 456 | (shadow-write-info-file)) | 484 | (shadow-write-info-file)) |
| 457 | 485 | ||
| @@ -468,19 +496,12 @@ function). Each site can be either a hostname or the name of a cluster (see | |||
| 468 | "Filename regexp: " | 496 | "Filename regexp: " |
| 469 | (if (buffer-file-name) | 497 | (if (buffer-file-name) |
| 470 | (shadow-regexp-superquote | 498 | (shadow-regexp-superquote |
| 471 | (nth 2 | 499 | (file-local-name (buffer-file-name)))))) |
| 472 | (shadow-parse-name | 500 | site sites) |
| 473 | (shadow-contract-file-name | ||
| 474 | (buffer-file-name)))))))) | ||
| 475 | site sites usernames) | ||
| 476 | (while (setq site (shadow-read-site)) | 501 | (while (setq site (shadow-read-site)) |
| 477 | (setq sites (cons site sites)) | 502 | (setq sites (cons site sites))) |
| 478 | (setq usernames | ||
| 479 | (cons (read-string (format "Username for %s: " site) | ||
| 480 | (shadow-get-user site)) | ||
| 481 | usernames))) | ||
| 482 | (setq shadow-regexp-groups | 503 | (setq shadow-regexp-groups |
| 483 | (cons (shadow-make-group regexp sites usernames) | 504 | (cons (shadow-make-group regexp sites) |
| 484 | shadow-regexp-groups)) | 505 | shadow-regexp-groups)) |
| 485 | (shadow-write-info-file))) | 506 | (shadow-write-info-file))) |
| 486 | 507 | ||
| @@ -537,14 +558,14 @@ permanently, remove the group from `shadow-literal-groups' or | |||
| 537 | ;;; Internal functions | 558 | ;;; Internal functions |
| 538 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | 559 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; |
| 539 | 560 | ||
| 540 | (defun shadow-make-group (regexp sites usernames) | 561 | (defun shadow-make-group (regexp sites) |
| 541 | "Make a description of a file group--- | 562 | "Make a description of a file group--- |
| 542 | actually a list of regexp ange-ftp file names---from REGEXP (name of file to | 563 | actually a list of regexp Tramp file names---from REGEXP (name of file to |
| 543 | be shadowed), list of SITES, and corresponding list of USERNAMES for each | 564 | be shadowed), and list of SITES" |
| 544 | site." | ||
| 545 | (if sites | 565 | (if sites |
| 546 | (cons (shadow-make-fullname (car sites) (car usernames) regexp) | 566 | (cons (shadow-make-fullname |
| 547 | (shadow-make-group regexp (cdr sites) (cdr usernames))) | 567 | (shadow-parse-name (shadow-site-primary (car sites))) nil regexp) |
| 568 | (shadow-make-group regexp (cdr sites))) | ||
| 548 | nil)) | 569 | nil)) |
| 549 | 570 | ||
| 550 | (defun shadow-copy-file (s) | 571 | (defun shadow-copy-file (s) |
| @@ -601,7 +622,9 @@ Consider them as regular expressions if third arg REGEXP is true." | |||
| 601 | (car groups)))) | 622 | (car groups)))) |
| 602 | (append (cond ((equal nonmatching (car groups)) nil) | 623 | (append (cond ((equal nonmatching (car groups)) nil) |
| 603 | (regexp | 624 | (regexp |
| 604 | (let ((realname (nth 2 (shadow-parse-fullname file)))) | 625 | (let ((realname |
| 626 | (tramp-file-name-localname | ||
| 627 | (shadow-parse-name file)))) | ||
| 605 | (mapcar | 628 | (mapcar |
| 606 | (function | 629 | (function |
| 607 | (lambda (x) | 630 | (lambda (x) |
| @@ -636,9 +659,8 @@ PAIR must be `eq' to one of the elements of that list." | |||
| 636 | Thus restores shadowfile's state from your last Emacs session. | 659 | Thus restores shadowfile's state from your last Emacs session. |
| 637 | Return t unless files were locked; then return nil." | 660 | Return t unless files were locked; then return nil." |
| 638 | (interactive) | 661 | (interactive) |
| 639 | (if (and (fboundp 'file-locked-p) | 662 | (if (or (stringp (file-locked-p shadow-info-file)) |
| 640 | (or (stringp (file-locked-p shadow-info-file)) | 663 | (stringp (file-locked-p shadow-todo-file))) |
| 641 | (stringp (file-locked-p shadow-todo-file)))) | ||
| 642 | (progn | 664 | (progn |
| 643 | (message "Shadowfile is running in another Emacs; can't have two.") | 665 | (message "Shadowfile is running in another Emacs; can't have two.") |
| 644 | (beep) | 666 | (beep) |
| @@ -647,7 +669,7 @@ Return t unless files were locked; then return nil." | |||
| 647 | (save-current-buffer | 669 | (save-current-buffer |
| 648 | (when shadow-info-file | 670 | (when shadow-info-file |
| 649 | (set-buffer (setq shadow-info-buffer | 671 | (set-buffer (setq shadow-info-buffer |
| 650 | (find-file-noselect shadow-info-file))) | 672 | (find-file-noselect shadow-info-file 'nowarn))) |
| 651 | (when (and (not (buffer-modified-p)) | 673 | (when (and (not (buffer-modified-p)) |
| 652 | (file-newer-than-file-p (make-auto-save-file-name) | 674 | (file-newer-than-file-p (make-auto-save-file-name) |
| 653 | shadow-info-file)) | 675 | shadow-info-file)) |
| @@ -680,6 +702,7 @@ defined, the old hashtable info is invalid." | |||
| 680 | (if (not shadow-info-buffer) | 702 | (if (not shadow-info-buffer) |
| 681 | (setq shadow-info-buffer (find-file-noselect shadow-info-file))) | 703 | (setq shadow-info-buffer (find-file-noselect shadow-info-file))) |
| 682 | (set-buffer shadow-info-buffer) | 704 | (set-buffer shadow-info-buffer) |
| 705 | (setq buffer-read-only nil) | ||
| 683 | (delete-region (point-min) (point-max)) | 706 | (delete-region (point-min) (point-max)) |
| 684 | (shadow-insert-var 'shadow-clusters) | 707 | (shadow-insert-var 'shadow-clusters) |
| 685 | (shadow-insert-var 'shadow-literal-groups) | 708 | (shadow-insert-var 'shadow-literal-groups) |
| @@ -692,6 +715,7 @@ With non-nil argument also saves the buffer." | |||
| 692 | (if (not shadow-todo-buffer) | 715 | (if (not shadow-todo-buffer) |
| 693 | (setq shadow-todo-buffer (find-file-noselect shadow-todo-file))) | 716 | (setq shadow-todo-buffer (find-file-noselect shadow-todo-file))) |
| 694 | (set-buffer shadow-todo-buffer) | 717 | (set-buffer shadow-todo-buffer) |
| 718 | (setq buffer-read-only nil) | ||
| 695 | (delete-region (point-min) (point-max)) | 719 | (delete-region (point-min) (point-max)) |
| 696 | (shadow-insert-var 'shadow-files-to-copy) | 720 | (shadow-insert-var 'shadow-files-to-copy) |
| 697 | (if save (shadow-save-todo-file)))) | 721 | (if save (shadow-save-todo-file)))) |
| @@ -765,24 +789,6 @@ look for files that have been changed and need to be copied to other systems." | |||
| 765 | (kill-emacs))) | 789 | (kill-emacs))) |
| 766 | 790 | ||
| 767 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | 791 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; |
| 768 | ;;; Lucid Emacs compatibility | ||
| 769 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ||
| 770 | |||
| 771 | ;; This is on hold until someone tells me about a working version of | ||
| 772 | ;; map-ynp for Lucid Emacs. | ||
| 773 | |||
| 774 | ;(when (string-match "Lucid" emacs-version) | ||
| 775 | ; (require 'symlink-fix) | ||
| 776 | ; (require 'ange-ftp) | ||
| 777 | ; (require 'map-ynp) | ||
| 778 | ; (if (not (fboundp 'file-truename)) | ||
| 779 | ; (fset 'shadow-expand-file-name | ||
| 780 | ; (symbol-function 'symlink-expand-file-name))) | ||
| 781 | ; (if (not (fboundp 'ange-ftp-ftp-name)) | ||
| 782 | ; (fset 'ange-ftp-ftp-name | ||
| 783 | ; (symbol-function 'ange-ftp-ftp-name)))) | ||
| 784 | |||
| 785 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ||
| 786 | ;;; Hook us up | 792 | ;;; Hook us up |
| 787 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | 793 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; |
| 788 | 794 | ||
| @@ -790,18 +796,10 @@ look for files that have been changed and need to be copied to other systems." | |||
| 790 | (defun shadow-initialize () | 796 | (defun shadow-initialize () |
| 791 | "Set up file shadowing." | 797 | "Set up file shadowing." |
| 792 | (interactive) | 798 | (interactive) |
| 793 | (if (null shadow-homedir) | 799 | (setq shadow-homedir |
| 794 | (setq shadow-homedir | 800 | (file-name-as-directory (shadow-expand-file-name shadow-homedir)) |
| 795 | (file-name-as-directory (shadow-expand-file-name "~")))) | 801 | shadow-info-file (shadow-expand-file-name shadow-info-file) |
| 796 | (if (null shadow-info-file) | 802 | shadow-todo-file (shadow-expand-file-name shadow-todo-file)) |
| 797 | (setq shadow-info-file | ||
| 798 | ;; FIXME: Move defaults to their defcustom. | ||
| 799 | (shadow-expand-file-name | ||
| 800 | (locate-user-emacs-file "shadows" ".shadows")))) | ||
| 801 | (if (null shadow-todo-file) | ||
| 802 | (setq shadow-todo-file | ||
| 803 | (shadow-expand-file-name | ||
| 804 | (locate-user-emacs-file "shadow_todo" ".shadow_todo")))) | ||
| 805 | (if (not (shadow-read-files)) | 803 | (if (not (shadow-read-files)) |
| 806 | (progn | 804 | (progn |
| 807 | (message "Shadowfile information files not found - aborting") | 805 | (message "Shadowfile information files not found - aborting") |
diff --git a/test/lisp/shadowfile-tests.el b/test/lisp/shadowfile-tests.el new file mode 100644 index 00000000000..5ded94480ec --- /dev/null +++ b/test/lisp/shadowfile-tests.el | |||
| @@ -0,0 +1,876 @@ | |||
| 1 | ;;; shadowfile-tests.el --- Tests of shadowfile | ||
| 2 | |||
| 3 | ;; Copyright (C) 2018 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | ;; Author: Michael Albinus <michael.albinus@gmx.de> | ||
| 6 | |||
| 7 | ;; This program is free software: you can redistribute it and/or | ||
| 8 | ;; modify it under the terms of the GNU General Public License as | ||
| 9 | ;; published by the Free Software Foundation, either version 3 of the | ||
| 10 | ;; License, or (at your option) any later version. | ||
| 11 | ;; | ||
| 12 | ;; This program is distributed in the hope that it will be useful, but | ||
| 13 | ;; WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | ||
| 15 | ;; General Public License for more details. | ||
| 16 | ;; | ||
| 17 | ;; You should have received a copy of the GNU General Public License | ||
| 18 | ;; along with this program. If not, see `http://www.gnu.org/licenses/'. | ||
| 19 | |||
| 20 | ;;; Commentary: | ||
| 21 | |||
| 22 | ;; A whole test run can be performed calling the command `shadowfile-test-all'. | ||
| 23 | |||
| 24 | ;;; Code: | ||
| 25 | |||
| 26 | (require 'ert) | ||
| 27 | (require 'shadowfile) | ||
| 28 | (require 'tramp) | ||
| 29 | |||
| 30 | ;; There is no default value on w32 systems, which could work out of the box. | ||
| 31 | (defconst shadow-test-remote-temporary-file-directory | ||
| 32 | (cond | ||
| 33 | ((getenv "REMOTE_TEMPORARY_FILE_DIRECTORY")) | ||
| 34 | ((eq system-type 'windows-nt) null-device) | ||
| 35 | (t (add-to-list | ||
| 36 | 'tramp-methods | ||
| 37 | '("mock" | ||
| 38 | (tramp-login-program "sh") | ||
| 39 | (tramp-login-args (("-i"))) | ||
| 40 | (tramp-remote-shell "/bin/sh") | ||
| 41 | (tramp-remote-shell-args ("-c")) | ||
| 42 | (tramp-connection-timeout 10))) | ||
| 43 | (add-to-list | ||
| 44 | 'tramp-default-host-alist | ||
| 45 | `("\\`mock\\'" nil ,(system-name))) | ||
| 46 | ;; Emacs' Makefile sets $HOME to a nonexistent value. Needed in | ||
| 47 | ;; batch mode only, therefore. It cannot be | ||
| 48 | ;; `temporary-directory', because the tests with "~" would fail. | ||
| 49 | (unless (and (null noninteractive) (file-directory-p "~/")) | ||
| 50 | (setenv "HOME" invocation-directory)) | ||
| 51 | (format "/mock::%s" temporary-file-directory))) | ||
| 52 | "Temporary directory for Tramp tests.") | ||
| 53 | |||
| 54 | (defconst shadow-test-info-file | ||
| 55 | (expand-file-name "shadows_test" temporary-file-directory) | ||
| 56 | "File to keep shadow information in during tests.") | ||
| 57 | |||
| 58 | (defconst shadow-test-todo-file | ||
| 59 | (expand-file-name "shadow_todo_test" temporary-file-directory) | ||
| 60 | "File to store the list of uncopied shadows in during tests.") | ||
| 61 | |||
| 62 | (ert-deftest shadow-test00-clusters () | ||
| 63 | "Check cluster definitions. | ||
| 64 | Per definition, all files are identical on the different hosts of | ||
| 65 | a cluster (or site). This is not tested here; it must be | ||
| 66 | guaranteed by the originator of a cluster definition." | ||
| 67 | (skip-unless (file-remote-p shadow-test-remote-temporary-file-directory)) | ||
| 68 | |||
| 69 | (let ((text-quoting-style 'grave) ;; We inspect the *Messages* buffer! | ||
| 70 | (inhibit-message t) | ||
| 71 | (shadow-info-file shadow-test-info-file) | ||
| 72 | (shadow-todo-file shadow-test-todo-file) | ||
| 73 | shadow-clusters | ||
| 74 | cluster primary regexp mocked-input) | ||
| 75 | (unwind-protect | ||
| 76 | ;; We must mock `read-from-minibuffer' and `read-string', in | ||
| 77 | ;; order to avoid interactive arguments. | ||
| 78 | (cl-letf* (((symbol-function 'read-from-minibuffer) | ||
| 79 | (lambda (&rest args) (pop mocked-input))) | ||
| 80 | ((symbol-function 'read-string) | ||
| 81 | (lambda (&rest args) (pop mocked-input)))) | ||
| 82 | |||
| 83 | ;; Cleanup. | ||
| 84 | (when (file-exists-p shadow-info-file) | ||
| 85 | (delete-file shadow-info-file)) | ||
| 86 | (when (file-exists-p shadow-todo-file) | ||
| 87 | (delete-file shadow-todo-file)) | ||
| 88 | |||
| 89 | ;; Define a cluster. | ||
| 90 | (setq cluster "cluster" | ||
| 91 | primary shadow-system-name | ||
| 92 | regexp (shadow-regexp-superquote primary) | ||
| 93 | mocked-input `(,cluster ,primary ,regexp)) | ||
| 94 | (call-interactively 'shadow-define-cluster) | ||
| 95 | (should | ||
| 96 | (string-equal | ||
| 97 | (shadow-cluster-name (shadow-get-cluster cluster)) cluster)) | ||
| 98 | (should | ||
| 99 | (string-equal | ||
| 100 | (shadow-cluster-primary (shadow-get-cluster cluster)) primary)) | ||
| 101 | (should | ||
| 102 | (string-equal | ||
| 103 | (shadow-cluster-regexp (shadow-get-cluster cluster)) regexp)) | ||
| 104 | (should-not (shadow-get-cluster "non-existent-cluster-name")) | ||
| 105 | |||
| 106 | ;; Test `shadow-set-cluster' and `make-shadow-cluster'. | ||
| 107 | (shadow-set-cluster cluster primary regexp) | ||
| 108 | (should | ||
| 109 | (equal (shadow-get-cluster cluster) | ||
| 110 | (make-shadow-cluster | ||
| 111 | :name cluster :primary primary :regexp regexp))) | ||
| 112 | |||
| 113 | ;; The primary must be either `shadow-system-name', or a remote file. | ||
| 114 | (setq ;; The second "cluster" is wrong. | ||
| 115 | mocked-input `(,cluster ,cluster ,primary ,regexp)) | ||
| 116 | (with-current-buffer (messages-buffer) | ||
| 117 | (narrow-to-region (point-max) (point-max))) | ||
| 118 | (call-interactively 'shadow-define-cluster) | ||
| 119 | (should | ||
| 120 | (string-match | ||
| 121 | (regexp-quote "Not a valid primary!") | ||
| 122 | (with-current-buffer (messages-buffer) (buffer-string)))) | ||
| 123 | ;; The first cluster definition is still valid. | ||
| 124 | (should | ||
| 125 | (string-equal | ||
| 126 | (shadow-cluster-name (shadow-get-cluster cluster)) cluster)) | ||
| 127 | (should | ||
| 128 | (string-equal | ||
| 129 | (shadow-cluster-primary (shadow-get-cluster cluster)) primary)) | ||
| 130 | (should | ||
| 131 | (string-equal | ||
| 132 | (shadow-cluster-regexp (shadow-get-cluster cluster)) regexp)) | ||
| 133 | |||
| 134 | ;; The regexp must match the primary name. | ||
| 135 | (setq ;; The second "cluster" is wrong. | ||
| 136 | mocked-input `(,cluster ,primary ,cluster ,regexp)) | ||
| 137 | (with-current-buffer (messages-buffer) | ||
| 138 | (narrow-to-region (point-max) (point-max))) | ||
| 139 | (call-interactively 'shadow-define-cluster) | ||
| 140 | (should | ||
| 141 | (string-match | ||
| 142 | (regexp-quote "Regexp doesn't include the primary host!") | ||
| 143 | (with-current-buffer (messages-buffer) (buffer-string)))) | ||
| 144 | ;; The first cluster definition is still valid. | ||
| 145 | (should | ||
| 146 | (string-equal | ||
| 147 | (shadow-cluster-name (shadow-get-cluster cluster)) cluster)) | ||
| 148 | (should | ||
| 149 | (string-equal | ||
| 150 | (shadow-cluster-primary (shadow-get-cluster cluster)) primary)) | ||
| 151 | (should | ||
| 152 | (string-equal | ||
| 153 | (shadow-cluster-regexp (shadow-get-cluster cluster)) regexp)) | ||
| 154 | |||
| 155 | ;; Redefine the cluster. | ||
| 156 | (setq primary | ||
| 157 | (file-remote-p shadow-test-remote-temporary-file-directory) | ||
| 158 | regexp (shadow-regexp-superquote primary) | ||
| 159 | mocked-input `(,cluster ,primary ,regexp)) | ||
| 160 | (call-interactively 'shadow-define-cluster) | ||
| 161 | (should | ||
| 162 | (string-equal | ||
| 163 | (shadow-cluster-name (shadow-get-cluster cluster)) cluster)) | ||
| 164 | (should | ||
| 165 | (string-equal | ||
| 166 | (shadow-cluster-primary (shadow-get-cluster cluster)) primary)) | ||
| 167 | (should | ||
| 168 | (string-equal | ||
| 169 | (shadow-cluster-regexp (shadow-get-cluster cluster)) regexp)) | ||
| 170 | |||
| 171 | ;; Test `shadow-set-cluster' and `make-shadow-cluster'. | ||
| 172 | (shadow-set-cluster cluster primary regexp) | ||
| 173 | (should | ||
| 174 | (equal (shadow-get-cluster cluster) | ||
| 175 | (make-shadow-cluster | ||
| 176 | :name cluster :primary primary :regexp regexp)))) | ||
| 177 | |||
| 178 | ;; Cleanup. | ||
| 179 | (with-current-buffer (messages-buffer) (widen)) | ||
| 180 | (when (file-exists-p shadow-info-file) | ||
| 181 | (delete-file shadow-info-file)) | ||
| 182 | (when (file-exists-p shadow-todo-file) | ||
| 183 | (delete-file shadow-todo-file))))) | ||
| 184 | |||
| 185 | (ert-deftest shadow-test01-sites () | ||
| 186 | "Check site definitions. | ||
| 187 | Per definition, all files are identical on the different hosts of | ||
| 188 | a cluster (or site). This is not tested here; it must be | ||
| 189 | guaranteed by the originator of a cluster definition." | ||
| 190 | (skip-unless (file-remote-p shadow-test-remote-temporary-file-directory)) | ||
| 191 | |||
| 192 | (let ((shadow-info-file shadow-test-info-file) | ||
| 193 | (shadow-todo-file shadow-test-todo-file) | ||
| 194 | shadow-clusters | ||
| 195 | cluster1 cluster2 primary1 primary2 regexp1 regexp2 mocked-input) | ||
| 196 | (unwind-protect | ||
| 197 | ;; We must mock `read-from-minibuffer' and `read-string', in | ||
| 198 | ;; order to avoid interactive arguments. | ||
| 199 | (cl-letf* (((symbol-function 'read-from-minibuffer) | ||
| 200 | (lambda (&rest args) (pop mocked-input))) | ||
| 201 | ((symbol-function 'read-string) | ||
| 202 | (lambda (&rest args) (pop mocked-input)))) | ||
| 203 | |||
| 204 | ;; Cleanup. | ||
| 205 | (when (file-exists-p shadow-info-file) | ||
| 206 | (delete-file shadow-info-file)) | ||
| 207 | (when (file-exists-p shadow-todo-file) | ||
| 208 | (delete-file shadow-todo-file)) | ||
| 209 | |||
| 210 | ;; Define a cluster. | ||
| 211 | (setq cluster1 "cluster1" | ||
| 212 | primary1 shadow-system-name | ||
| 213 | regexp1 (shadow-regexp-superquote primary1)) | ||
| 214 | (shadow-set-cluster cluster1 primary1 regexp1) | ||
| 215 | |||
| 216 | ;; A site is either a cluster identification, or a primary host. | ||
| 217 | (should (string-equal cluster1 (shadow-site-name cluster1))) | ||
| 218 | (should (string-equal primary1 (shadow-name-site primary1))) | ||
| 219 | (should | ||
| 220 | (string-equal (format "/%s:" cluster1) (shadow-name-site cluster1))) | ||
| 221 | (should (string-equal (system-name) (shadow-site-name primary1))) | ||
| 222 | (should | ||
| 223 | (string-equal | ||
| 224 | (file-remote-p shadow-test-remote-temporary-file-directory) | ||
| 225 | (shadow-name-site | ||
| 226 | (file-remote-p shadow-test-remote-temporary-file-directory)))) | ||
| 227 | (should | ||
| 228 | (string-equal | ||
| 229 | (file-remote-p shadow-test-remote-temporary-file-directory) | ||
| 230 | (shadow-site-name | ||
| 231 | (file-remote-p shadow-test-remote-temporary-file-directory)))) | ||
| 232 | |||
| 233 | (should (equal (shadow-site-cluster cluster1) | ||
| 234 | (shadow-get-cluster cluster1))) | ||
| 235 | (should (equal (shadow-site-cluster (shadow-name-site cluster1)) | ||
| 236 | (shadow-get-cluster cluster1))) | ||
| 237 | (should (equal (shadow-site-cluster primary1) | ||
| 238 | (shadow-get-cluster cluster1))) | ||
| 239 | (should (equal (shadow-site-cluster (shadow-site-name primary1)) | ||
| 240 | (shadow-get-cluster cluster1))) | ||
| 241 | (should (string-equal (shadow-site-primary cluster1) primary1)) | ||
| 242 | (should (string-equal (shadow-site-primary primary1) primary1)) | ||
| 243 | |||
| 244 | ;; `shadow-read-site' accepts "cluster", "/cluster:", | ||
| 245 | ;; "system", "/system:". It shall reject bad site names. | ||
| 246 | (setq mocked-input | ||
| 247 | `(,cluster1 ,(shadow-name-site cluster1) | ||
| 248 | ,primary1 ,(shadow-site-name primary1) | ||
| 249 | ,shadow-system-name "" "bad" "/bad:")) | ||
| 250 | (should (string-equal (shadow-read-site) cluster1)) | ||
| 251 | (should (string-equal (shadow-read-site) (shadow-name-site cluster1))) | ||
| 252 | (should (string-equal (shadow-read-site) primary1)) | ||
| 253 | (should (string-equal (shadow-read-site) (shadow-site-name primary1))) | ||
| 254 | (should (string-equal (shadow-read-site) shadow-system-name)) | ||
| 255 | (should-not (shadow-read-site)) ; "" | ||
| 256 | (should-not (shadow-read-site)) ; "bad" | ||
| 257 | (should-not (shadow-read-site)) ; "/bad:" | ||
| 258 | (should-error (shadow-read-site)) ; no input at all | ||
| 259 | |||
| 260 | ;; Define a second cluster. | ||
| 261 | (setq cluster2 "cluster2" | ||
| 262 | primary2 | ||
| 263 | (file-remote-p shadow-test-remote-temporary-file-directory) | ||
| 264 | regexp2 (format "^\\(%s\\|%s\\)$" shadow-system-name primary2)) | ||
| 265 | (shadow-set-cluster cluster2 primary2 regexp2) | ||
| 266 | |||
| 267 | ;; `shadow-site-match' shall know all different kind of site names. | ||
| 268 | (should (shadow-site-match cluster1 cluster1)) | ||
| 269 | (should (shadow-site-match primary1 primary1)) | ||
| 270 | (should (shadow-site-match cluster1 primary1)) | ||
| 271 | (should (shadow-site-match primary1 cluster1)) | ||
| 272 | (should (shadow-site-match cluster2 cluster2)) | ||
| 273 | (should (shadow-site-match primary2 primary2)) | ||
| 274 | (should (shadow-site-match cluster2 primary2)) | ||
| 275 | (should (shadow-site-match primary2 cluster2)) | ||
| 276 | |||
| 277 | ;; The regexp of `cluster2' matches the primary of | ||
| 278 | ;; `cluster1'. Not vice versa. | ||
| 279 | (should (shadow-site-match cluster2 cluster1)) | ||
| 280 | (should-not (shadow-site-match cluster1 cluster2)) | ||
| 281 | |||
| 282 | ;; If we use the primaries of a cluster, it doesn't match. | ||
| 283 | (should-not | ||
| 284 | (shadow-site-match (shadow-site-primary cluster2) cluster1)) | ||
| 285 | (should-not | ||
| 286 | (shadow-site-match (shadow-site-primary cluster1) cluster2))) | ||
| 287 | |||
| 288 | ;; Cleanup. | ||
| 289 | (when (file-exists-p shadow-info-file) | ||
| 290 | (delete-file shadow-info-file)) | ||
| 291 | (when (file-exists-p shadow-todo-file) | ||
| 292 | (delete-file shadow-todo-file))))) | ||
| 293 | |||
| 294 | (ert-deftest shadow-test02-files () | ||
| 295 | "Check file manipulation functions." | ||
| 296 | (skip-unless (file-remote-p shadow-test-remote-temporary-file-directory)) | ||
| 297 | |||
| 298 | (let ((shadow-info-file shadow-test-info-file) | ||
| 299 | (shadow-todo-file shadow-test-todo-file) | ||
| 300 | shadow-clusters | ||
| 301 | cluster primary regexp file hup) | ||
| 302 | (unwind-protect | ||
| 303 | (progn | ||
| 304 | ;; Cleanup. | ||
| 305 | (when (file-exists-p shadow-info-file) | ||
| 306 | (delete-file shadow-info-file)) | ||
| 307 | (when (file-exists-p shadow-todo-file) | ||
| 308 | (delete-file shadow-todo-file)) | ||
| 309 | |||
| 310 | ;; Define a cluster. | ||
| 311 | (setq cluster "cluster" | ||
| 312 | primary shadow-system-name | ||
| 313 | regexp (shadow-regexp-superquote primary) | ||
| 314 | file (make-temp-name | ||
| 315 | (expand-file-name | ||
| 316 | "shadowfile-tests" temporary-file-directory))) | ||
| 317 | (shadow-set-cluster cluster primary regexp) | ||
| 318 | |||
| 319 | ;; The constant structure to compare with. | ||
| 320 | (setq hup (make-tramp-file-name :host (system-name) :localname file)) | ||
| 321 | |||
| 322 | ;; The structure a local file is transformed in. | ||
| 323 | (should (equal (shadow-parse-name file) hup)) | ||
| 324 | (should (equal (shadow-parse-name (concat "/" cluster ":" file)) hup)) | ||
| 325 | (should (equal (shadow-parse-name (concat primary file)) hup)) | ||
| 326 | |||
| 327 | ;; A local file name is kept. | ||
| 328 | (should | ||
| 329 | (string-equal (shadow-local-file file) file)) | ||
| 330 | ;; A file on this cluster is also local. | ||
| 331 | (should | ||
| 332 | (string-equal | ||
| 333 | (shadow-local-file (concat "/" cluster ":" file)) file)) | ||
| 334 | ;; A file on the primary host is also local. | ||
| 335 | (should | ||
| 336 | (string-equal (shadow-local-file (concat primary file)) file)) | ||
| 337 | |||
| 338 | ;; Redefine the cluster. | ||
| 339 | (setq primary | ||
| 340 | (file-remote-p shadow-test-remote-temporary-file-directory) | ||
| 341 | regexp (shadow-regexp-superquote primary)) | ||
| 342 | (shadow-set-cluster cluster primary regexp) | ||
| 343 | |||
| 344 | ;; The structure of the local file is still the same. | ||
| 345 | (should (equal (shadow-parse-name file) hup)) | ||
| 346 | ;; The cluster name must be used. | ||
| 347 | (setf (tramp-file-name-host hup) cluster) | ||
| 348 | (should (equal (shadow-parse-name (concat "/" cluster ":" file)) hup)) | ||
| 349 | ;; The structure of a remote file is different. | ||
| 350 | (should | ||
| 351 | (equal (shadow-parse-name (concat primary file)) | ||
| 352 | (tramp-dissect-file-name (concat primary file)))) | ||
| 353 | |||
| 354 | ;; A local file is still local. | ||
| 355 | (should (shadow-local-file file)) | ||
| 356 | ;; A file on this cluster is not local. | ||
| 357 | (should-not (shadow-local-file (concat "/" cluster ":" file))) | ||
| 358 | ;; A file on the primary host is not local. | ||
| 359 | (should-not (shadow-local-file (concat primary file))) | ||
| 360 | ;; There's no error on wrong FILE. | ||
| 361 | (should-not (shadow-local-file nil))) | ||
| 362 | |||
| 363 | ;; Cleanup. | ||
| 364 | (when (file-exists-p shadow-info-file) | ||
| 365 | (delete-file shadow-info-file)) | ||
| 366 | (when (file-exists-p shadow-todo-file) | ||
| 367 | (delete-file shadow-todo-file))))) | ||
| 368 | |||
| 369 | (ert-deftest shadow-test03-expand-cluster-in-file-name () | ||
| 370 | "Check canonical file name of a cluster or site." | ||
| 371 | (skip-unless (file-remote-p shadow-test-remote-temporary-file-directory)) | ||
| 372 | |||
| 373 | (let ((shadow-info-file shadow-test-info-file) | ||
| 374 | (shadow-todo-file shadow-test-todo-file) | ||
| 375 | shadow-clusters | ||
| 376 | cluster primary regexp file1 file2) | ||
| 377 | (unwind-protect | ||
| 378 | (progn | ||
| 379 | ;; Cleanup. | ||
| 380 | (when (file-exists-p shadow-info-file) | ||
| 381 | (delete-file shadow-info-file)) | ||
| 382 | (when (file-exists-p shadow-todo-file) | ||
| 383 | (delete-file shadow-todo-file)) | ||
| 384 | |||
| 385 | ;; Define a cluster. | ||
| 386 | (setq cluster "cluster" | ||
| 387 | primary shadow-system-name | ||
| 388 | regexp (shadow-regexp-superquote primary)) | ||
| 389 | (shadow-set-cluster cluster primary regexp) | ||
| 390 | |||
| 391 | (setq file1 | ||
| 392 | (make-temp-name | ||
| 393 | (expand-file-name "shadowfile-tests" temporary-file-directory)) | ||
| 394 | file2 | ||
| 395 | (make-temp-name | ||
| 396 | (expand-file-name | ||
| 397 | "shadowfile-tests" | ||
| 398 | shadow-test-remote-temporary-file-directory))) | ||
| 399 | |||
| 400 | ;; A local file name is kept. | ||
| 401 | (should | ||
| 402 | (string-equal (shadow-expand-cluster-in-file-name file1) file1)) | ||
| 403 | ;; A remote file is kept. | ||
| 404 | (should | ||
| 405 | (string-equal (shadow-expand-cluster-in-file-name file2) file2)) | ||
| 406 | ;; A cluster name is expanded to the primary name. | ||
| 407 | (should | ||
| 408 | (string-equal | ||
| 409 | (shadow-expand-cluster-in-file-name (format "/%s:%s" cluster file1)) | ||
| 410 | (shadow-expand-cluster-in-file-name (concat primary file1)))) | ||
| 411 | ;; A primary name is expanded if it is a local file name. | ||
| 412 | (should | ||
| 413 | (string-equal | ||
| 414 | (shadow-expand-cluster-in-file-name (concat primary file1)) file1)) | ||
| 415 | |||
| 416 | ;; Redefine the cluster. | ||
| 417 | (setq primary | ||
| 418 | (file-remote-p shadow-test-remote-temporary-file-directory) | ||
| 419 | regexp (shadow-regexp-superquote primary)) | ||
| 420 | (shadow-set-cluster cluster primary regexp) | ||
| 421 | |||
| 422 | ;; A cluster name is expanded to the primary name. | ||
| 423 | (should | ||
| 424 | (string-equal | ||
| 425 | (shadow-expand-cluster-in-file-name (format "/%s:%s" cluster file1)) | ||
| 426 | (shadow-expand-cluster-in-file-name (concat primary file1)))) | ||
| 427 | ;; A primary name is not expanded if it isn't is a local file name. | ||
| 428 | (should | ||
| 429 | (string-equal | ||
| 430 | (shadow-expand-cluster-in-file-name (concat primary file1)) | ||
| 431 | (concat primary file1)))) | ||
| 432 | |||
| 433 | ;; Cleanup. | ||
| 434 | (when (file-exists-p shadow-info-file) | ||
| 435 | (delete-file shadow-info-file)) | ||
| 436 | (when (file-exists-p shadow-todo-file) | ||
| 437 | (delete-file shadow-todo-file))))) | ||
| 438 | |||
| 439 | (ert-deftest shadow-test04-contract-file-name () | ||
| 440 | "Check canonical file name of a cluster or site." | ||
| 441 | (skip-unless (file-remote-p shadow-test-remote-temporary-file-directory)) | ||
| 442 | |||
| 443 | (let ((shadow-info-file shadow-test-info-file) | ||
| 444 | (shadow-todo-file shadow-test-todo-file) | ||
| 445 | shadow-clusters | ||
| 446 | cluster primary regexp file) | ||
| 447 | (unwind-protect | ||
| 448 | (progn | ||
| 449 | ;; Cleanup. | ||
| 450 | (when (file-exists-p shadow-info-file) | ||
| 451 | (delete-file shadow-info-file)) | ||
| 452 | (when (file-exists-p shadow-todo-file) | ||
| 453 | (delete-file shadow-todo-file)) | ||
| 454 | |||
| 455 | ;; Define a cluster. | ||
| 456 | (setq cluster "cluster" | ||
| 457 | primary shadow-system-name | ||
| 458 | regexp (shadow-regexp-superquote primary) | ||
| 459 | file (make-temp-name | ||
| 460 | (expand-file-name | ||
| 461 | "shadowfile-tests" temporary-file-directory))) | ||
| 462 | (shadow-set-cluster cluster primary regexp) | ||
| 463 | |||
| 464 | ;; The cluster name is prepended for local files. | ||
| 465 | (should | ||
| 466 | (string-equal | ||
| 467 | (shadow-contract-file-name file) (concat "/cluster:" file))) | ||
| 468 | ;; A cluster file name is preserved. | ||
| 469 | (should | ||
| 470 | (string-equal | ||
| 471 | (shadow-contract-file-name (concat "/cluster:" file)) | ||
| 472 | (concat "/cluster:" file))) | ||
| 473 | ;; `shadow-system-name' is mapped to the cluster. | ||
| 474 | (should | ||
| 475 | (string-equal | ||
| 476 | (shadow-contract-file-name (concat shadow-system-name file)) | ||
| 477 | (concat "/cluster:" file))) | ||
| 478 | |||
| 479 | ;; Redefine the cluster. | ||
| 480 | (setq primary | ||
| 481 | (file-remote-p shadow-test-remote-temporary-file-directory) | ||
| 482 | regexp (shadow-regexp-superquote primary)) | ||
| 483 | (shadow-set-cluster cluster primary regexp) | ||
| 484 | |||
| 485 | ;; A remote file name is mapped to the cluster. | ||
| 486 | (should | ||
| 487 | (string-equal | ||
| 488 | (shadow-contract-file-name | ||
| 489 | (concat | ||
| 490 | (file-remote-p shadow-test-remote-temporary-file-directory) file)) | ||
| 491 | (concat "/cluster:" file)))) | ||
| 492 | |||
| 493 | ;; Cleanup. | ||
| 494 | (when (file-exists-p shadow-info-file) | ||
| 495 | (delete-file shadow-info-file)) | ||
| 496 | (when (file-exists-p shadow-todo-file) | ||
| 497 | (delete-file shadow-todo-file))))) | ||
| 498 | |||
| 499 | (ert-deftest shadow-test05-file-match () | ||
| 500 | "Check `shadow-same-site' and `shadow-file-match'." | ||
| 501 | (skip-unless (file-remote-p shadow-test-remote-temporary-file-directory)) | ||
| 502 | |||
| 503 | (let ((shadow-info-file shadow-test-info-file) | ||
| 504 | (shadow-todo-file shadow-test-todo-file) | ||
| 505 | shadow-clusters | ||
| 506 | cluster primary regexp file) | ||
| 507 | (unwind-protect | ||
| 508 | (progn | ||
| 509 | ;; Cleanup. | ||
| 510 | (when (file-exists-p shadow-info-file) | ||
| 511 | (delete-file shadow-info-file)) | ||
| 512 | (when (file-exists-p shadow-todo-file) | ||
| 513 | (delete-file shadow-todo-file)) | ||
| 514 | |||
| 515 | ;; Define a cluster. | ||
| 516 | (setq cluster "cluster" | ||
| 517 | primary shadow-system-name | ||
| 518 | regexp (shadow-regexp-superquote primary) | ||
| 519 | file (make-temp-name | ||
| 520 | (expand-file-name | ||
| 521 | "shadowfile-tests" temporary-file-directory))) | ||
| 522 | (shadow-set-cluster cluster primary regexp) | ||
| 523 | |||
| 524 | (should (shadow-same-site (shadow-parse-name "/cluster:") file)) | ||
| 525 | (should | ||
| 526 | (shadow-same-site (shadow-parse-name shadow-system-name) file)) | ||
| 527 | (should (shadow-same-site (shadow-parse-name file) file)) | ||
| 528 | |||
| 529 | (should | ||
| 530 | (shadow-file-match | ||
| 531 | (shadow-parse-name (concat "/cluster:" file)) file)) | ||
| 532 | (should | ||
| 533 | (shadow-file-match | ||
| 534 | (shadow-parse-name (concat shadow-system-name file)) file)) | ||
| 535 | (should (shadow-file-match (shadow-parse-name file) file)) | ||
| 536 | |||
| 537 | ;; Redefine the cluster. | ||
| 538 | (setq primary | ||
| 539 | (file-remote-p shadow-test-remote-temporary-file-directory) | ||
| 540 | regexp (shadow-regexp-superquote primary)) | ||
| 541 | (shadow-set-cluster cluster primary regexp) | ||
| 542 | |||
| 543 | (should | ||
| 544 | (shadow-file-match | ||
| 545 | (shadow-parse-name | ||
| 546 | (concat | ||
| 547 | (file-remote-p shadow-test-remote-temporary-file-directory) | ||
| 548 | file)) | ||
| 549 | file))) | ||
| 550 | |||
| 551 | ;; Cleanup. | ||
| 552 | (when (file-exists-p shadow-info-file) | ||
| 553 | (delete-file shadow-info-file)) | ||
| 554 | (when (file-exists-p shadow-todo-file) | ||
| 555 | (delete-file shadow-todo-file))))) | ||
| 556 | |||
| 557 | (ert-deftest shadow-test06-literal-groups () | ||
| 558 | "Check literal group definitions." | ||
| 559 | (let ((shadow-info-file shadow-test-info-file) | ||
| 560 | (shadow-todo-file shadow-test-todo-file) | ||
| 561 | shadow-clusters shadow-literal-groups | ||
| 562 | cluster1 cluster2 primary regexp file1 file2 mocked-input) | ||
| 563 | (unwind-protect | ||
| 564 | ;; We must mock `read-from-minibuffer' and `read-string', in | ||
| 565 | ;; order to avoid interactive arguments. | ||
| 566 | (cl-letf* (((symbol-function 'read-from-minibuffer) | ||
| 567 | (lambda (&rest args) (pop mocked-input))) | ||
| 568 | ((symbol-function 'read-string) | ||
| 569 | (lambda (&rest args) (pop mocked-input)))) | ||
| 570 | |||
| 571 | ;; Cleanup. | ||
| 572 | (when (file-exists-p shadow-info-file) | ||
| 573 | (delete-file shadow-info-file)) | ||
| 574 | (when (file-exists-p shadow-todo-file) | ||
| 575 | (delete-file shadow-todo-file)) | ||
| 576 | |||
| 577 | ;; Define clusters. | ||
| 578 | (setq cluster1 "cluster1" | ||
| 579 | primary shadow-system-name | ||
| 580 | regexp (shadow-regexp-superquote primary)) | ||
| 581 | (shadow-set-cluster cluster1 primary regexp) | ||
| 582 | |||
| 583 | (setq cluster2 "cluster2" | ||
| 584 | primary | ||
| 585 | (file-remote-p shadow-test-remote-temporary-file-directory) | ||
| 586 | regexp (format "^\\(%s\\|%s\\)$" shadow-system-name primary)) | ||
| 587 | (shadow-set-cluster cluster2 primary regexp) | ||
| 588 | |||
| 589 | ;; Define a literal group. | ||
| 590 | (setq file1 | ||
| 591 | (make-temp-name | ||
| 592 | (expand-file-name "shadowfile-tests" temporary-file-directory)) | ||
| 593 | file2 | ||
| 594 | (make-temp-name | ||
| 595 | (expand-file-name | ||
| 596 | "shadowfile-tests" | ||
| 597 | shadow-test-remote-temporary-file-directory)) | ||
| 598 | mocked-input `(,cluster1 ,file1 ,cluster2 ,file2 ,(kbd "RET"))) | ||
| 599 | (with-temp-buffer | ||
| 600 | (setq-local buffer-file-name file1) | ||
| 601 | (call-interactively 'shadow-define-literal-group)) | ||
| 602 | |||
| 603 | ;; `shadow-literal-groups' is a list of lists. | ||
| 604 | (should (consp shadow-literal-groups)) | ||
| 605 | (should (consp (car shadow-literal-groups))) | ||
| 606 | (should-not (cdr shadow-literal-groups)) | ||
| 607 | |||
| 608 | (should (member (format "/%s:%s" cluster1 (file-local-name file1)) | ||
| 609 | (car shadow-literal-groups))) | ||
| 610 | (should (member (format "/%s:%s" cluster2 (file-local-name file2)) | ||
| 611 | (car shadow-literal-groups)))) | ||
| 612 | |||
| 613 | ;; Cleanup. | ||
| 614 | (when (file-exists-p shadow-info-file) | ||
| 615 | (delete-file shadow-info-file)) | ||
| 616 | (when (file-exists-p shadow-todo-file) | ||
| 617 | (delete-file shadow-todo-file))))) | ||
| 618 | |||
| 619 | (ert-deftest shadow-test07-regexp-groups () | ||
| 620 | "Check regexp group definitions." | ||
| 621 | (let ((shadow-info-file shadow-test-info-file) | ||
| 622 | (shadow-todo-file shadow-test-todo-file) | ||
| 623 | shadow-clusters shadow-regexp-groups | ||
| 624 | cluster1 cluster2 primary regexp file mocked-input) | ||
| 625 | (unwind-protect | ||
| 626 | ;; We must mock `read-from-minibuffer' and `read-string', in | ||
| 627 | ;; order to avoid interactive arguments. | ||
| 628 | (cl-letf* (((symbol-function 'read-from-minibuffer) | ||
| 629 | (lambda (&rest args) (pop mocked-input))) | ||
| 630 | ((symbol-function 'read-string) | ||
| 631 | (lambda (&rest args) (pop mocked-input)))) | ||
| 632 | |||
| 633 | ;; Cleanup. | ||
| 634 | (when (file-exists-p shadow-info-file) | ||
| 635 | (delete-file shadow-info-file)) | ||
| 636 | (when (file-exists-p shadow-todo-file) | ||
| 637 | (delete-file shadow-todo-file)) | ||
| 638 | |||
| 639 | ;; Define clusters. | ||
| 640 | (setq cluster1 "cluster1" | ||
| 641 | primary shadow-system-name | ||
| 642 | regexp (shadow-regexp-superquote primary)) | ||
| 643 | (shadow-set-cluster cluster1 primary regexp) | ||
| 644 | |||
| 645 | (setq cluster2 "cluster2" | ||
| 646 | primary | ||
| 647 | (file-remote-p shadow-test-remote-temporary-file-directory) | ||
| 648 | regexp (format "^\\(%s\\|%s\\)$" shadow-system-name primary)) | ||
| 649 | (shadow-set-cluster cluster2 primary regexp) | ||
| 650 | |||
| 651 | ;; Define a regexp group. | ||
| 652 | (setq file | ||
| 653 | (make-temp-name | ||
| 654 | (expand-file-name "shadowfile-tests" temporary-file-directory)) | ||
| 655 | mocked-input `(,(shadow-regexp-superquote file) | ||
| 656 | ,cluster1 ,cluster2 ,(kbd "RET"))) | ||
| 657 | (with-temp-buffer | ||
| 658 | (setq-local buffer-file-name nil) | ||
| 659 | (call-interactively 'shadow-define-regexp-group)) | ||
| 660 | |||
| 661 | ;; `shadow-regexp-groups' is a list of lists. | ||
| 662 | (should (consp shadow-regexp-groups)) | ||
| 663 | (should (consp (car shadow-regexp-groups))) | ||
| 664 | (should-not (cdr shadow-regexp-groups)) | ||
| 665 | |||
| 666 | (should | ||
| 667 | (member | ||
| 668 | (concat | ||
| 669 | (shadow-site-primary cluster1) (shadow-regexp-superquote file)) | ||
| 670 | (car shadow-regexp-groups))) | ||
| 671 | (should | ||
| 672 | (member | ||
| 673 | (concat | ||
| 674 | (shadow-site-primary cluster2) (shadow-regexp-superquote file)) | ||
| 675 | (car shadow-regexp-groups)))) | ||
| 676 | |||
| 677 | ;; Cleanup. | ||
| 678 | (when (file-exists-p shadow-info-file) | ||
| 679 | (delete-file shadow-info-file)) | ||
| 680 | (when (file-exists-p shadow-todo-file) | ||
| 681 | (delete-file shadow-todo-file))))) | ||
| 682 | |||
| 683 | (ert-deftest shadow-test08-shadow-todo () | ||
| 684 | "Check that needed shadows are added to todo." | ||
| 685 | (let ((backup-inhibited t) | ||
| 686 | (shadow-info-file shadow-test-info-file) | ||
| 687 | (shadow-todo-file shadow-test-todo-file) | ||
| 688 | (shadow-inhibit-message t) | ||
| 689 | shadow-clusters shadow-literal-groups shadow-regexp-groups | ||
| 690 | shadow-files-to-copy | ||
| 691 | cluster1 cluster2 primary regexp file) | ||
| 692 | (unwind-protect | ||
| 693 | (progn | ||
| 694 | ;; Cleanup. | ||
| 695 | (when (file-exists-p shadow-info-file) | ||
| 696 | (delete-file shadow-info-file)) | ||
| 697 | (when (file-exists-p shadow-todo-file) | ||
| 698 | (delete-file shadow-todo-file)) | ||
| 699 | |||
| 700 | ;; Define clusters. | ||
| 701 | (setq cluster1 "cluster1" | ||
| 702 | primary shadow-system-name | ||
| 703 | regexp (shadow-regexp-superquote primary)) | ||
| 704 | (shadow-set-cluster cluster1 primary regexp) | ||
| 705 | |||
| 706 | (setq cluster2 "cluster2" | ||
| 707 | primary | ||
| 708 | (file-remote-p shadow-test-remote-temporary-file-directory) | ||
| 709 | regexp (shadow-regexp-superquote primary)) | ||
| 710 | (shadow-set-cluster cluster2 primary regexp) | ||
| 711 | |||
| 712 | ;; Define a literal group. | ||
| 713 | (setq file | ||
| 714 | (make-temp-name | ||
| 715 | (expand-file-name "shadowfile-tests" temporary-file-directory)) | ||
| 716 | shadow-literal-groups | ||
| 717 | `((,(concat "/cluster1:" file) ,(concat "/cluster2:" file)))) | ||
| 718 | |||
| 719 | ;; Save file from "cluster1" definition. | ||
| 720 | (with-temp-buffer | ||
| 721 | (setq buffer-file-name file) | ||
| 722 | (insert "foo") | ||
| 723 | (save-buffer)) | ||
| 724 | (should | ||
| 725 | (member | ||
| 726 | (cons file (shadow-contract-file-name (concat "/cluster2:" file))) | ||
| 727 | shadow-files-to-copy)) | ||
| 728 | |||
| 729 | ;; Save file from "cluster2" definition. | ||
| 730 | (with-temp-buffer | ||
| 731 | (setq buffer-file-name (concat (shadow-site-primary cluster2) file)) | ||
| 732 | (insert "foo") | ||
| 733 | (save-buffer)) | ||
| 734 | (should | ||
| 735 | (member | ||
| 736 | (cons | ||
| 737 | (concat (shadow-site-primary cluster2) file) | ||
| 738 | (shadow-contract-file-name (concat "/cluster1:" file))) | ||
| 739 | shadow-files-to-copy)) | ||
| 740 | |||
| 741 | ;; Define a regexp group. | ||
| 742 | (setq shadow-files-to-copy nil | ||
| 743 | shadow-regexp-groups | ||
| 744 | `((,(concat (shadow-site-primary cluster1) | ||
| 745 | (shadow-regexp-superquote file)) | ||
| 746 | ,(concat (shadow-site-primary cluster2) | ||
| 747 | (shadow-regexp-superquote file))))) | ||
| 748 | |||
| 749 | ;; Save file from "cluster1" definition. | ||
| 750 | (with-temp-buffer | ||
| 751 | (setq buffer-file-name file) | ||
| 752 | (insert "foo") | ||
| 753 | (save-buffer)) | ||
| 754 | (should | ||
| 755 | (member | ||
| 756 | (cons file (shadow-contract-file-name (concat "/cluster2:" file))) | ||
| 757 | shadow-files-to-copy)) | ||
| 758 | |||
| 759 | ;; Save file from "cluster2" definition. | ||
| 760 | (with-temp-buffer | ||
| 761 | (setq buffer-file-name (concat (shadow-site-primary cluster2) file)) | ||
| 762 | (insert "foo") | ||
| 763 | (save-buffer)) | ||
| 764 | (should | ||
| 765 | (member | ||
| 766 | (cons | ||
| 767 | (concat (shadow-site-primary cluster2) file) | ||
| 768 | (shadow-contract-file-name (concat "/cluster1:" file))) | ||
| 769 | shadow-files-to-copy))) | ||
| 770 | |||
| 771 | ;; Cleanup. | ||
| 772 | (when (file-exists-p shadow-info-file) | ||
| 773 | (delete-file shadow-info-file)) | ||
| 774 | (when (file-exists-p shadow-todo-file) | ||
| 775 | (delete-file shadow-todo-file)) | ||
| 776 | (when (file-exists-p file) | ||
| 777 | (delete-file file)) | ||
| 778 | (when (file-exists-p (concat (shadow-site-primary cluster2) file)) | ||
| 779 | (delete-file (concat (shadow-site-primary cluster2) file)))))) | ||
| 780 | |||
| 781 | (ert-deftest shadow-test09-shadow-copy-files () | ||
| 782 | "Check that needed shadow files are copied." | ||
| 783 | (let ((backup-inhibited t) | ||
| 784 | (shadow-info-file shadow-test-info-file) | ||
| 785 | (shadow-todo-file shadow-test-todo-file) | ||
| 786 | (shadow-inhibit-message t) | ||
| 787 | (shadow-noquery t) | ||
| 788 | shadow-clusters shadow-files-to-copy | ||
| 789 | cluster1 cluster2 primary regexp file mocked-input) | ||
| 790 | (unwind-protect | ||
| 791 | (progn | ||
| 792 | ;; Cleanup. | ||
| 793 | (when (file-exists-p shadow-info-file) | ||
| 794 | (delete-file shadow-info-file)) | ||
| 795 | (when (file-exists-p shadow-todo-file) | ||
| 796 | (delete-file shadow-todo-file)) | ||
| 797 | (when (buffer-live-p shadow-todo-buffer) | ||
| 798 | (with-current-buffer shadow-todo-buffer (erase-buffer))) | ||
| 799 | |||
| 800 | ;; Define clusters. | ||
| 801 | (setq cluster1 "cluster1" | ||
| 802 | primary shadow-system-name | ||
| 803 | regexp (shadow-regexp-superquote primary)) | ||
| 804 | (shadow-set-cluster cluster1 primary regexp) | ||
| 805 | |||
| 806 | (setq cluster2 "cluster2" | ||
| 807 | primary | ||
| 808 | (file-remote-p shadow-test-remote-temporary-file-directory) | ||
| 809 | regexp (shadow-regexp-superquote primary)) | ||
| 810 | (shadow-set-cluster cluster2 primary regexp) | ||
| 811 | |||
| 812 | ;; Define files to copy. | ||
| 813 | (setq file | ||
| 814 | (make-temp-name | ||
| 815 | (expand-file-name "shadowfile-tests" temporary-file-directory)) | ||
| 816 | shadow-literal-groups | ||
| 817 | `((,(concat "/cluster1:" file) ,(concat "/cluster2:" file))) | ||
| 818 | shadow-regexp-groups | ||
| 819 | `((,(concat (shadow-site-primary cluster1) | ||
| 820 | (shadow-regexp-superquote file)) | ||
| 821 | ,(concat (shadow-site-primary cluster2) | ||
| 822 | (shadow-regexp-superquote file)))) | ||
| 823 | mocked-input `(,(concat (shadow-site-primary cluster2) file) | ||
| 824 | ,file)) | ||
| 825 | |||
| 826 | ;; Save files. | ||
| 827 | (with-temp-buffer | ||
| 828 | (setq buffer-file-name file) | ||
| 829 | (insert "foo") | ||
| 830 | (save-buffer)) | ||
| 831 | (with-temp-buffer | ||
| 832 | (setq buffer-file-name (concat (shadow-site-primary cluster2) file)) | ||
| 833 | (insert "foo") | ||
| 834 | (save-buffer)) | ||
| 835 | |||
| 836 | ;; We must mock `write-region', in order to check proper | ||
| 837 | ;; action. | ||
| 838 | (add-function | ||
| 839 | :before (symbol-function 'write-region) | ||
| 840 | (lambda (&rest args) | ||
| 841 | (when (and (buffer-file-name) mocked-input) | ||
| 842 | (should (equal (buffer-file-name) (pop mocked-input))))) | ||
| 843 | '((name . "write-region-mock"))) | ||
| 844 | |||
| 845 | ;; Copy the files. | ||
| 846 | (shadow-copy-files 'noquery) | ||
| 847 | (should-not shadow-files-to-copy) | ||
| 848 | (with-current-buffer shadow-todo-buffer | ||
| 849 | (goto-char (point-min)) | ||
| 850 | (should | ||
| 851 | (looking-at (regexp-quote "(setq shadow-files-to-copy nil)"))))) | ||
| 852 | |||
| 853 | ;; Cleanup. | ||
| 854 | (remove-function (symbol-function 'write-region) "write-region-mock") | ||
| 855 | (when (file-exists-p shadow-info-file) | ||
| 856 | (delete-file shadow-info-file)) | ||
| 857 | (when (file-exists-p shadow-todo-file) | ||
| 858 | (delete-file shadow-todo-file)) | ||
| 859 | (when (file-exists-p file) | ||
| 860 | (delete-file file)) | ||
| 861 | (when (file-exists-p (concat (shadow-site-primary cluster2) file)) | ||
| 862 | (delete-file (concat (shadow-site-primary cluster2) file)))))) | ||
| 863 | |||
| 864 | (defun shadowfile-test-all (&optional interactive) | ||
| 865 | "Run all tests for \\[shadowfile]." | ||
| 866 | (interactive "p") | ||
| 867 | (if interactive | ||
| 868 | (ert-run-tests-interactively "^shadowfile-") | ||
| 869 | (ert-run-tests-batch "^shadowfile-"))) | ||
| 870 | |||
| 871 | (let ((shadow-info-file shadow-test-info-file) | ||
| 872 | (shadow-todo-file shadow-test-todo-file)) | ||
| 873 | (shadow-initialize)) | ||
| 874 | |||
| 875 | (provide 'shadowfile-tests) | ||
| 876 | ;;; shadowfile-tests.el ends here | ||