;;;; filesys.test --- test file system functions -*- scheme -*-
;;;; 
;;;; Copyright (C) 2004, 2006, 2013, 2019, 2021 Free Software Foundation, Inc.
;;;; Copyright (C) 2021 Maxime Devos <maximedevos@telenet.be>
;;;; 
;;;; 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:
            ;; <https://elixir.bootlin.com/linux/v5.10.46/source/fs/stat.c#L419>.
            (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))))
