;;;; filesys.test --- test file system functions -*- scheme -*- ;;;; ;;;; Copyright (C) 2004, 2006, 2013, 2019, 2021 Free Software Foundation, Inc. ;;;; Copyright (C) 2021 Maxime Devos ;;;; ;;;; This library is free software; you can redistribute it and/or ;;;; modify it under the terms of the GNU Lesser General Public ;;;; License as published by the Free Software Foundation; either ;;;; version 3 of the License, or (at your option) any later version. ;;;; ;;;; This library 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 ;;;; Lesser General Public License for more details. ;;;; ;;;; You should have received a copy of the GNU Lesser General Public ;;;; License along with this library; if not, write to the Free Software ;;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA (define-module (test-suite test-filesys) #:use-module (test-suite lib) #:use-module (test-suite guile-test) #:use-module (ice-9 threads) #:use-module (ice-9 match) #:use-module (rnrs io ports) #:use-module (rnrs bytevectors)) (define (test-file) (data-file-name "filesys-test.tmp")) (define (test-symlink) (data-file-name "filesys-test-link.tmp")) (define (test-directory) (data-file-name "filesys-test-dir.tmp")) (define (test-directory2) (data-file-name "filesys-test-dir2.tmp")) ;;; ;;; copy-file ;;; (with-test-prefix "copy-file" ;; return next prospective file descriptor number (define (next-fd) (let ((fd (dup 0))) (close fd) fd)) ;; in guile 1.6.4 and earlier, copy-file didn't close the input fd when ;; the output could not be opened (pass-if "fd leak when dest unwritable" (let ((old-next (next-fd))) (false-if-exception (copy-file "/dev/null" "no/such/dir/foo")) (= old-next (next-fd))))) ;;; ;;; lstat ;;; (with-test-prefix "lstat" (pass-if "normal file" (call-with-output-file (test-file) (lambda (port) (display "hello" port))) (eqv? 5 (stat:size (lstat (test-file))))) (call-with-output-file (test-file) (lambda (port) (display "hello" port))) (false-if-exception (delete-file (test-symlink))) (if (not (false-if-exception (begin (symlink (test-file) (test-symlink)) #t))) (display "cannot create symlink, lstat test skipped\n") (pass-if "symlink" ;; not much to test, except that it works (->bool (lstat (test-symlink)))))) ;;; ;;; opendir and friends ;;; (with-test-prefix "opendir" (with-test-prefix "root directory" (let ((d (opendir "/"))) (pass-if "not empty" (string? (readdir d))) (pass-if "all entries are strings" (let more () (let ((f (readdir d))) (cond ((string? f) (more)) ((eof-object? f) #t) (else #f))))) (closedir d)))) ;;; ;;; stat ;;; (with-test-prefix "stat" (with-test-prefix "filename" (pass-if "size" (call-with-output-file (test-file) (lambda (port) (display "hello" port))) (eqv? 5 (stat:size (stat (test-file)))))) (with-test-prefix "file descriptor" (pass-if "size" (call-with-output-file (test-file) (lambda (port) (display "hello" port))) (let* ((fd (open-fdes (test-file) O_RDONLY)) (st (stat fd))) (close-fdes fd) (eqv? 5 (stat:size st))))) (with-test-prefix "port" (pass-if "size" (call-with-output-file (test-file) (lambda (port) (display "hello" port))) (let* ((port (open-file (test-file) "r+")) (st (stat port))) (close-port port) (eqv? 5 (stat:size st)))))) (with-test-prefix "statat" ;; file-exists? from (ice-9 boot) dereferences symbolic links ;; (a bug?). (define (file-exists? filename) (catch 'system-error (lambda () (lstat filename) #t) (lambda args (if (= (system-error-errno args) ENOENT) ;; For the purposes of the following tests, ;; it is safe to ignore errors like EPERM, but a correct ;; implementation would return #t for that error. #f (apply throw args))))) (define (maybe-delete-directory) (when (file-exists? (test-directory)) (for-each (lambda (filename) (define full-name (in-vicinity (test-directory) filename)) (when (file-exists? full-name) (delete-file full-name))) '("test-file" "test-symlink")) (rmdir (test-directory)))) (define (skip-unless-defined . things) (for-each (lambda (thing) (unless (defined? thing) (throw 'unsupported))) things)) (maybe-delete-directory) (mkdir (test-directory)) (call-with-output-file (in-vicinity (test-directory) "test-file") (lambda (port) (display "hello" port))) ;; Return #true if the symlink was created, #false otherwise. (define (maybe-create-symlink) (if (file-exists? (in-vicinity (test-directory) "test-symlink")) #t (false-if-exception (symlink "test-file" (in-vicinity (test-directory) "test-symlink"))))) (pass-if-equal "regular file" 5 (skip-unless-defined 'statat) (call-with-port (open (test-directory) O_RDONLY) (lambda (port) (stat:size (statat port "test-file"))))) (pass-if-equal "regular file, AT_SYMLINK_NOFOLLOW" 5 (skip-unless-defined 'statat 'AT_SYMLINK_NOFOLLOW) (call-with-port (open (test-directory) O_RDONLY) (lambda (port) (stat:size (statat port "test-file" AT_SYMLINK_NOFOLLOW))))) (pass-if-equal "symbolic links are dereferenced" '(regular 5) ;; Not all systems support symlinks. (skip-unless-defined 'statat 'symlink) (unless (maybe-create-symlink) (throw 'unresolved)) (call-with-port (open (test-directory) O_RDONLY) (lambda (port) (define result (statat port "test-symlink")) (list (stat:type result) (stat:size result))))) (pass-if-equal "symbolic links are not dereferenced" `(symlink ,(string-length "test-file")) ;; Not all systems support symlinks. (skip-unless-defined 'statat 'symlink) (unless (maybe-create-symlink) (throw 'unresolved)) (call-with-port (open (test-directory) O_RDONLY) (lambda (port) (define result (statat port "test-symlink" AT_SYMLINK_NOFOLLOW)) (list (stat:type result) (stat:size result))))) (maybe-delete-directory)) (with-test-prefix "sendfile" (let* ((file (search-path %load-path "ice-9/boot-9.scm")) (len (stat:size (stat file))) (ref (call-with-input-file file get-bytevector-all))) (pass-if-equal "file" (cons len ref) (let* ((result (call-with-input-file file (lambda (input) (call-with-output-file (test-file) (lambda (output) (sendfile output input len 0)))))) (out (call-with-input-file (test-file) get-bytevector-all))) (cons result out))) (pass-if-equal "file with offset" (cons (- len 777) (call-with-input-file file (lambda (input) (seek input 777 SEEK_SET) (get-bytevector-all input)))) (let* ((result (call-with-input-file file (lambda (input) (call-with-output-file (test-file) (lambda (output) (sendfile output input (- len 777) 777)))))) (out (call-with-input-file (test-file) get-bytevector-all))) (cons result out))) (pass-if-equal "file with offset past the end" (cons (- len 777) (call-with-input-file file (lambda (input) (seek input 777 SEEK_SET) (get-bytevector-all input)))) (let* ((result (call-with-input-file file (lambda (input) (call-with-output-file (test-file) (lambda (output) (sendfile output input len 777)))))) (out (call-with-input-file (test-file) get-bytevector-all))) (cons result out))) (pass-if-equal "file with offset near the end" (cons 77 (call-with-input-file file (lambda (input) (seek input (- len 77) SEEK_SET) (get-bytevector-all input)))) (let* ((result (call-with-input-file file (lambda (input) (call-with-output-file (test-file) (lambda (output) (sendfile output input len (- len 77))))))) (out (call-with-input-file (test-file) get-bytevector-all))) (cons result out))) (pass-if-equal "pipe" (cons len ref) (if (provided? 'threads) (let* ((in+out (pipe)) (child (call-with-new-thread (lambda () (call-with-input-file file (lambda (input) (let ((result (sendfile (cdr in+out) (fileno input) len 0))) (close-port (cdr in+out)) result))))))) (let ((out (get-bytevector-all (car in+out)))) (close-port (car in+out)) (cons (join-thread child) out))) (throw 'unresolved))) (pass-if-equal "pipe with offset" (cons (- len 777) (call-with-input-file file (lambda (input) (seek input 777 SEEK_SET) (get-bytevector-all input)))) (if (provided? 'threads) (let* ((in+out (pipe)) (child (call-with-new-thread (lambda () (call-with-input-file file (lambda (input) (let ((result (sendfile (cdr in+out) (fileno input) (- len 777) 777))) (close-port (cdr in+out)) result))))))) (let ((out (get-bytevector-all (car in+out)))) (close-port (car in+out)) (cons (join-thread child) out))) (throw 'unresolved))))) (with-test-prefix "basename" (pass-if-equal "/" "/" (basename "/")) (pass-if-equal "//" "/" (basename "//")) (pass-if-equal "a/b/c" "c" (basename "a/b/c"))) (delete-file (test-file)) (when (file-exists? (test-symlink)) (delete-file (test-symlink))) (with-test-prefix "mkdtemp" (pass-if-exception "number arg" exception:wrong-type-arg (if (not (defined? 'mkdtemp)) (throw 'unresolved) (mkdtemp 123))) (pass-if "template prefix is preserved" (if (not (defined? 'mkdtemp)) (throw 'unresolved) (let* ((template "T-XXXXXX") (name (mkdtemp template))) (false-if-exception (rmdir name)) (and (string? name) (string-contains name "T-") (= (string-length name) 8))))) (pass-if-exception "invalid template" exception:system-error (if (not (defined? 'mkdtemp)) (throw 'unresolved) (mkdtemp "T-AAAAAA"))) (pass-if "directory created" (if (not (defined? 'mkdtemp)) (throw 'unresolved) (let* ((template "T-XXXXXX") (name (mkdtemp template))) (let* ((_stat (stat name)) (result (eqv? 'directory (stat:type _stat)))) (false-if-exception (rmdir name)) result))))) ;;; ;;; chmodat ;;; (with-test-prefix "chmodat" (call-with-output-file (test-file) (const #f)) (chmod (test-file) #o000) (pass-if-equal "regular file" #o300 (unless (defined? 'chmodat) (throw 'unsupported)) (call-with-port (open (dirname (test-file)) O_RDONLY) (lambda (port) (chmodat port (test-file) #o300))) (stat:perms (stat (test-file)))) (chmod (test-file) #o000) (pass-if-equal "regular file, AT_SYMLINK_NOFOLLOW" #o300 (unless (and (defined? 'chmodat) (defined? 'AT_SYMLINK_NOFOLLOW)) (throw 'unsupported)) (call-with-port (open (dirname (test-file)) O_RDONLY) (lambda (port) (catch 'system-error (lambda () (chmodat port (basename (test-file)) #o300 AT_SYMLINK_NOFOLLOW)) (lambda args (close-port port) ;; AT_SYMLINK_NOFOLLOW is not supported on Linux (at least Linux ;; 5.11.2 with the btrfs file system), even for regular files. (if (= ENOTSUP (system-error-errno args)) (begin (display "fchmodat doesn't support AT_SYMLINK_NOFOLLOW\n") (throw 'unresolved)) (apply throw args)))))) (stat:perms (stat (test-file)))) (pass-if-exception "not a port" exception:wrong-type-arg (chmodat "bogus" (test-file) #o300)) (pass-if-exception "not a file port" exception:wrong-type-arg (chmodat (open-input-string "") (test-file) #o300)) (pass-if-exception "closed port" exception:wrong-type-arg (chmodat (call-with-port (open "." O_RDONLY) identity) (test-file) #o300)) (delete-file (test-file))) (with-test-prefix "chdir" (pass-if-equal "current directory" (getcwd) (begin (chdir ".") (getcwd))) (define file (search-path %load-path "ice-9/boot-9.scm")) (pass-if-equal "test directory" (dirname file) (let ((olddir (getcwd)) (dir #f)) (chdir (dirname file)) (set! dir (getcwd)) (chdir olddir) dir)) (pass-if-equal "test directory, via port" (dirname file) (unless (provided? 'chdir-port) (throw 'unresolved)) (let ((olddir (getcwd)) (port (open (dirname file) O_RDONLY)) (dir #f)) (chdir port) (set! dir (getcwd)) (chdir olddir) dir)) (pass-if-exception "closed port" exception:wrong-type-arg (unless (provided? 'chdir-port) (throw 'unresolved)) (let ((port (open (dirname file) O_RDONLY)) (olddir (getcwd))) (close-port port) (chdir port) (chdir olddir))) ; should not be reached (pass-if-exception "not a port or file name" exception:wrong-type-arg (chdir '(stuff))) (pass-if-exception "non-file port" exception:wrong-type-arg (chdir (open-input-string "")))) (with-test-prefix "readlink" (false-if-exception (delete-file (test-symlink))) (false-if-exception (delete-file (test-file))) (call-with-output-file (test-file) (lambda (port) (display "hello" port))) (if (not (false-if-exception (begin (symlink (test-file) (test-symlink)) #t))) (display "cannot create symlink, some readlink tests skipped\n") (let () (pass-if-equal "file name of symlink" (test-file) (readlink (test-symlink))) (pass-if-equal "port representing a symlink" (test-file) (let () (unless (and (provided? 'readlink-port) (defined? 'O_NOFOLLOW) (defined? 'O_PATH) (not (= 0 O_NOFOLLOW)) (not (= 0 O_PATH))) (throw 'unsupported)) (define port (open (test-symlink) (logior O_NOFOLLOW O_PATH))) (define points-to (false-if-exception (readlink port))) (close-port port) points-to)) (pass-if-exception "not a port or file name" exception:wrong-type-arg (readlink '(stuff))))) (pass-if-equal "port representing a regular file" EINVAL (call-with-input-file (test-file) (lambda (port) (unless (provided? 'readlink-port) (throw 'unsupported)) (catch 'system-error (lambda () (readlink port) (close-port port) ; should be unreachable #f) (lambda args (close-port port) ;; At least Linux 5.10.46 returns ENOENT instead of EINVAL. ;; Possibly surprising, but it is documented in some man ;; pages and it doesn't appear to be an accident: ;; . (define error (system-error-errno args)) (if (= error ENOENT) EINVAL error)))))) (pass-if-exception "non-file port" exception:wrong-type-arg (readlink (open-input-string ""))) (pass-if-exception "closed port" exception:wrong-type-arg (let ((port (open-file (test-file) "r"))) (close-port port) (readlink port))) (false-if-exception (delete-file (test-symlink))) (false-if-exception (delete-file (test-file)))) (with-test-prefix "symlinkat" (pass-if-equal "create" (test-file) (unless (defined? 'symlinkat) (throw 'unsupported)) (call-with-port (open "." O_RDONLY) (lambda (port) (symlinkat port (test-file) (test-symlink)) (readlink (test-symlink))))) (false-if-exception (delete-file (test-symlink))) (pass-if-exception "not a port" exception:wrong-type-arg (unless (defined? 'symlinkat) (throw 'unsupported)) (symlinkat "bogus" (test-file) (test-symlink))) (pass-if-exception "not a file port" exception:wrong-type-arg (unless (defined? 'symlinkat) (throw 'unsupported)) (symlinkat (open-input-string "") (test-file) (test-symlink))) (pass-if-exception "closed port" exception:wrong-type-arg (unless (defined? 'symlinkat) (throw 'unsupported)) (symlinkat (call-with-port (open "." O_RDONLY) identity) (test-file) (test-symlink)))) (with-test-prefix "mkdirat" (define (skip-if-unsupported) (unless (defined? 'mkdirat) (throw 'unsupported))) (define (maybe-delete-directory) (when (file-exists? (test-directory)) (rmdir (test-directory)))) (maybe-delete-directory) (pass-if-equal "create" 'directory (skip-if-unsupported) (call-with-port (open "." O_RDONLY) (lambda (port) (mkdirat port (test-directory)) (stat:type (stat (test-directory)))))) (maybe-delete-directory) (pass-if-equal "explicit perms" (logand #o111 (lognot (umask))) (skip-if-unsupported) (call-with-port (open "." O_RDONLY) (lambda (port) (mkdirat port (test-directory) #o111) (stat:perms (stat (test-directory)))))) (maybe-delete-directory) (pass-if-equal "create, implicit perms" (logand #o777 (lognot (umask))) (skip-if-unsupported) (call-with-port (open "." O_RDONLY) (lambda (port) (mkdirat port (test-directory)) (stat:perms (stat (test-directory)))))) (maybe-delete-directory)) (with-test-prefix "rename-file-at" (define (skip-if-unsupported) (unless (defined? 'rename-file-at) (throw 'unsupported))) (pass-if-equal "current working directory" '(#f "hello") (skip-if-unsupported) ;; Create a file in the test directory (call-with-output-file "filesys-test-a.tmp" (lambda (port) (display "hello" port))) ;; Try to rename it (rename-file-at #f "filesys-test-a.tmp" #f "filesys-test-b.tmp") ;; Verify it exists under the new name, and not under the old name (list (file-exists? "filesys-test-a.tmp") (call-with-input-file "filesys-test-b.tmp" get-string-all))) (false-if-exception (delete-file "filesys-test-a.tmp")) (false-if-exception (delete-file "filesys-test-b.tmp")) (pass-if-equal "two ports" '(#f "hello") (skip-if-unsupported) (mkdir (test-directory)) (mkdir (test-directory2)) ;; Create a file in the first directory (call-with-output-file (in-vicinity (test-directory) "a") (lambda (port) (display "hello" port))) (let ((port1 (open (test-directory) O_RDONLY)) (port2 (open (test-directory2) O_RDONLY))) ;; Try to rename it (rename-file-at port1 "a" port2 "b") (close-port port1) (close-port port2) ;; Verify it exists under the new name, and not under the old name (list (file-exists? (in-vicinity (test-directory) "a")) (call-with-input-file (in-vicinity (test-directory2) "b") get-string-all)))) (false-if-exception (delete-file (in-vicinity (test-directory) "a"))) (false-if-exception (delete-file (in-vicinity (test-directory2) "b"))) (false-if-exception (rmdir (test-directory))) (false-if-exception (rmdir (test-directory2))) (pass-if-equal "port and current working directory" '(#f "hello") (skip-if-unsupported) (mkdir (test-directory)) ;; Create a file in (test-directory) (call-with-output-file (in-vicinity (test-directory) "a") (lambda (port) (display "hello" port))) (let ((port (open (test-directory) O_RDONLY))) ;; Try to rename it (rename-file-at port "a" #f (basename (test-file))) (close-port port) ;; Verify it exists under the new name, and not under the old name. (list (file-exists? (in-vicinity (test-directory) "a")) (call-with-input-file (test-file) get-string-all)))) (false-if-exception (delete-file (in-vicinity (test-directory) "a"))) (false-if-exception (rmdir (test-directory))) (false-if-exception (delete-file (test-file))) (pass-if-equal "current working directory and port" '(#f "hello") (skip-if-unsupported) (mkdir (test-directory)) ;; Create a file in the working directory (call-with-output-file (test-file) (lambda (port) (display "hello" port))) (let ((port (open (test-directory) O_RDONLY))) ;; Try to rename it (rename-file-at #f (basename (test-file)) port "b") (close-port port) ;; Verify it exists under the new name, and not under the old name. (list (file-exists? (test-file)) (call-with-input-file (in-vicinity (test-directory) "b") get-string-all)))) (false-if-exception (delete-file (in-vicinity (test-directory) "b"))) (false-if-exception (delete-file (test-file))) (false-if-exception (rmdir (test-directory))) (pass-if-exception "not a file port (1)" exception:wrong-type-arg (skip-if-unsupported) (rename-file-at (open-input-string "") "some" #f "thing")) (pass-if-exception "not a file port (2)" exception:wrong-type-arg (skip-if-unsupported) (rename-file-at #f "some" (open-input-string "") "thing")) (pass-if-exception "closed port (1)" exception:wrong-type-arg (skip-if-unsupported) (rename-file-at (call-with-port (open "." O_RDONLY) identity) "some" #f "thing")) (pass-if-exception "closed port (2)" exception:wrong-type-arg (skip-if-unsupported) (rename-file-at #f "some" (call-with-port (open "." O_RDONLY) identity) "thing")) (pass-if-exception "not a string (1)" exception:wrong-type-arg (skip-if-unsupported) (rename-file-at #f 'what #f "thing")) (pass-if-exception "not a string (2)" exception:wrong-type-arg (skip-if-unsupported) (rename-file-at #f "some" #f 'what))) (with-test-prefix "delete-file-at" (define (skip-if-unsupported) (when (not (and (defined? 'delete-file-at) (defined? 'AT_REMOVEDIR))) (throw 'unsupported))) (define (create-test-file) (call-with-output-file (test-file) identity)) (define (create-test-directory) (mkdir (test-directory))) (define (delete-test-file) (when (file-exists? (test-file)) (delete-file (test-file)))) (define (delete-test-directory) (when (file-exists? (test-directory)) (rmdir (test-directory)))) (pass-if-equal "regular file" #f (skip-if-unsupported) (create-test-file) (call-with-port (open (dirname (test-file)) O_RDONLY) (lambda (port) (delete-file-at port (basename (test-file))))) (file-exists? (test-file))) (delete-test-file) (pass-if-equal "regular file, explicit flags" #f (skip-if-unsupported) (create-test-file) (call-with-port (open (dirname (test-file)) O_RDONLY) (lambda (port) (delete-file-at port (basename (test-file)) 0))) (file-exists? (test-file))) (delete-test-file) (pass-if-equal "directory, explicit flags" #f (skip-if-unsupported) (create-test-directory) (call-with-port (open (dirname (test-directory)) O_RDONLY) (lambda (port) (delete-file-at port (basename (test-directory)) AT_REMOVEDIR))) (file-exists? (test-directory))) (delete-test-directory) (pass-if-exception "not a port" exception:wrong-type-arg (skip-if-unsupported) (delete-file-at 'bogus "irrelevant")) (pass-if-exception "not a file port" exception:wrong-type-arg (skip-if-unsupported) (delete-file-at (open-input-string "") "irrelevant")) (pass-if-exception "closed port" exception:wrong-type-arg (skip-if-unsupported) (delete-file-at (call-with-port (open "." O_RDONLY) identity) "irrelevant"))) (with-test-prefix "openat" (define (skip-if-unsupported) (unless (defined? 'openat) (throw 'unsupported))) (define file (search-path %load-path "ice-9/boot-9.scm")) (define (call-with-relatively-opened-file directory-arguments file-arguments proc) (call-with-port (apply open directory-arguments) (lambda (directory) (call-with-port (apply openat directory file-arguments) (lambda (port) (proc port)))))) (pass-if-equal "mode read-only" "r" (skip-if-unsupported) (call-with-relatively-opened-file (list (dirname file) O_RDONLY) (list (basename file) O_RDONLY) (lambda (port) (port-mode port)))) (pass-if-equal "port-revealed count" 0 (skip-if-unsupported) (call-with-relatively-opened-file (list (dirname file) O_RDONLY) (list (basename file) O_RDONLY) (lambda (port) (port-revealed port)))) (when (file-exists? (test-file)) (delete-file (test-file))) (pass-if-equal "O_CREAT/O_WRONLY" (list #t (logand (lognot (umask)) #o666) "w") (skip-if-unsupported) (call-with-relatively-opened-file (list (dirname (test-file)) O_RDONLY) (list (basename (test-file)) (logior O_WRONLY O_CREAT)) (lambda (port) (list (file-exists? (test-file)) (stat:perms (stat (test-file))) (port-mode port))))) (when (file-exists? (test-file)) (delete-file (test-file))) (pass-if-equal "O_CREAT/O_WRONLY, non-default mode" (list #t (logand (lognot (umask)) #o700) "w") (skip-if-unsupported) (call-with-relatively-opened-file (list (dirname (test-file)) O_RDONLY) (list (basename (test-file)) (logior O_WRONLY O_CREAT) #o700) (lambda (port) (list (file-exists? (test-file)) (stat:perms (stat (test-file))) (port-mode port))))) (pass-if-exception "closed port" exception:wrong-type-arg (skip-if-unsupported) (openat (call-with-port (open "." O_RDONLY) identity) "." O_RDONLY)) (pass-if-exception "non-file port" exception:wrong-type-arg (skip-if-unsupported) (openat (open-input-string "") "." O_RDONLY)) (pass-if-exception "not a port" exception:wrong-type-arg (skip-if-unsupported) (openat "not a port" "." O_RDONLY)) (when (file-exists? (test-file)) (delete-file (test-file))))