rename-tests: new test, exposes several platform bugs
authorEric Blake <ebb9@byu.net>
Sat, 26 Sep 2009 23:22:15 +0000 (17:22 -0600)
committerEric Blake <ebb9@byu.net>
Fri, 2 Oct 2009 11:56:29 +0000 (05:56 -0600)
This test passes on GNU/Linux, OpenBSD, and Cygwin 1.7.
Elsewhere, this test fails because of at least these bugs:
Solaris 10, cygwin 1.5.x, and mingw all mistakenly succeed on
rename("file","other/").  Solaris 9 and the gnulib replacement
for SunOS 4.1 mistakenly succeed on rename("file/","other").
Cygwin 1.5.x and mingw mistakenly succeed on rename("dir","d/.").
Cygwin 1.5.x and NetBSD 1.6 (even with the gnulib replacement)
mistakenly reduce the link count on rename("hard1","hard2").

* modules/rename-tests: New file.
* tests/test-rename.h: Likewise.
* tests/test-rename.c: Likewise.
* doc/posix-functions/rename.texi (rename): Improve documentation,
including bugs that will eventually be fixed in gnulib.

Signed-off-by: Eric Blake <ebb9@byu.net>
ChangeLog
doc/posix-functions/rename.texi
modules/rename-tests [new file with mode: 0644]
tests/test-rename.c [new file with mode: 0644]
tests/test-rename.h [new file with mode: 0644]

index 2e205ffc1cd31ab6cbf1e3c1c57b406cc4ab60dd..f13340f8924dcf59caec363cf316dd42978d277f 100644 (file)
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,12 @@
+2009-10-02  Eric Blake  <ebb9@byu.net>
+
+       rename-tests: new test, exposes several platform bugs
+       * modules/rename-tests: New file.
+       * tests/test-rename.h: Likewise.
+       * tests/test-rename.c: Likewise.
+       * doc/posix-functions/rename.texi (rename): Improve documentation,
+       including bugs that will eventually be fixed in gnulib.
+
 2009-10-02  Paolo Bonzini  <bonzini@gnu.org>
 
        * lib/uname.c: Include <stdlib.h>
index 84a03ab75e79394055b1c816825c325d01fa1d00..65981db9d2c19e863fc0b6908e19a945ad532242 100644 (file)
@@ -9,22 +9,51 @@ Gnulib module: rename
 Portability problems fixed by Gnulib:
 @itemize
 @item
-This function does not handle trailing slashes correctly on
-some platforms (the full rules for trailing slashes are complex):
-SunOS 4.1, mingw.
+This function does not allow trailing slashes when creating a
+destination directory, as in @code{rename("dir","new/")}:
+NetBSD 1.6.
 @item
-This function will not replace an existing destination on some
+This function does not reject trailing slashes on non-directories on
+some platforms, as in @code{rename("file","new/")}:
+Solaris 10, Cygwin 1.5.x, mingw.
+@item
+This function ignores trailing slashes on symlinks on some platforms,
+such that @code{rename("link/","new")} corrupts @file{link}:
+Solaris 9.
+@item
+This function incorrectly reduces the link count when comparing two
+spellings of a hard link on some platforms:
+NetBSD 1.6, Cygwin 1.5.x.
+@item
+This function will not always replace an existing destination on some
 platforms:
 mingw.
+@item
+This function mistakenly allows names ending in @samp{.} or @samp{..}
+on some platforms:
+Cygwin 1.5.x, mingw.
+@item
+This function does not reject attempts to rename existing directories
+and non-directories onto one another on some platforms:
+Cygwin 1.5.x, mingw.
+@item
+This function does not allow trailing slashes on source directories on
+older platforms, as in @samp{rename("dir/","new")}:
+SunOS 4.1.
 @end itemize
 
 Portability problems not fixed by Gnulib:
 @itemize
-This function will not replace a destination that is currently opened
+@item
+POSIX requires that @code{rename("symlink-to-dir/","dir2")} rename
+@file{dir} and leave @file{symlink-to-dir} dangling; likewise, it
+requires that @code{rename("dir","dangling/")} rename @file{dir} so
+that @file{dangling} is no longer a dangling symlink.  This behavior
+is counter-intuitive, so on some systems, @code{rename} fails with
+@code{ENOTDIR} if either argument is a symlink with a trailing slash:
+glibc, OpenBSD, Cygwin 1.7.
+@item
+This function will not rename a source that is currently opened
 by any process:
 mingw.
-@item
-This function mistakenly allows names ending in @samp{.} or @samp{..}
-on some platforms:
-Cygwin 1.5.x.
 @end itemize
diff --git a/modules/rename-tests b/modules/rename-tests
new file mode 100644 (file)
index 0000000..ed586d7
--- /dev/null
@@ -0,0 +1,19 @@
+Files:
+tests/test-rename.h
+tests/test-rename.c
+
+Depends-on:
+errno
+link
+lstat
+progname
+stdbool
+symlink
+sys_stat
+
+configure.ac:
+
+Makefile.am:
+TESTS += test-rename
+check_PROGRAMS += test-rename
+test_rename_LDADD = $(LDADD) @LIBINTL@
diff --git a/tests/test-rename.c b/tests/test-rename.c
new file mode 100644 (file)
index 0000000..7bfdd84
--- /dev/null
@@ -0,0 +1,53 @@
+/* Test of rename() function.
+   Copyright (C) 2009 Free Software Foundation, Inc.
+
+   This program is free software: you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 2 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>.  */
+
+#include <config.h>
+
+#include <unistd.h>
+
+#include <errno.h>
+#include <fcntl.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#define ASSERT(expr) \
+  do                                                                         \
+    {                                                                        \
+      if (!(expr))                                                           \
+        {                                                                    \
+          fprintf (stderr, "%s:%d: assertion failed\n", __FILE__, __LINE__); \
+          fflush (stderr);                                                   \
+          abort ();                                                          \
+        }                                                                    \
+    }                                                                        \
+  while (0)
+
+#define BASE "test-rename.t"
+
+#include "test-rename.h"
+
+int
+main (int argc, char **argv)
+{
+  /* Remove any garbage left from previous partial runs.  */
+  ASSERT (system ("rm -rf " BASE "*") == 0);
+
+  return test_rename (rename, true);
+}
diff --git a/tests/test-rename.h b/tests/test-rename.h
new file mode 100644 (file)
index 0000000..5dc8918
--- /dev/null
@@ -0,0 +1,444 @@
+/* Test of rename() function.
+   Copyright (C) 2009 Free Software Foundation, Inc.
+
+   This program is free software: you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 2 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>.  */
+
+/* This file is designed to test both rename(a,b) and
+   renameat(AT_FDCWD,a,AT_FDCWD,b).  FUNC is the function to test.
+   Assumes that BASE and ASSERT are already defined, and that
+   appropriate headers are already included.  If PRINT, warn before
+   skipping symlink tests with status 77.  */
+
+static int
+test_rename (int (*func) (char const *, char const *), bool print)
+{
+  /* Setup.  */
+  struct stat st;
+  int fd = creat (BASE "file", 0600);
+  ASSERT (0 <= fd);
+  ASSERT (write (fd, "hi", 2) == 2);
+  ASSERT (close (fd) == 0);
+  ASSERT (mkdir (BASE "dir", 0700) == 0);
+
+  /* Obvious errors.  */
+
+  errno = 0; /* Missing source.  */
+  ASSERT (func (BASE "missing", BASE "missing") == -1);
+  ASSERT (errno == ENOENT);
+  errno = 0;
+  ASSERT (func (BASE "missing/", BASE "missing") == -1);
+  ASSERT (errno == ENOENT);
+  errno = 0;
+  ASSERT (func (BASE "missing", BASE "missing/") == -1);
+  ASSERT (errno == ENOENT);
+  errno = 0; /* Empty operand.  */
+  ASSERT (func ("", BASE "missing") == -1);
+  ASSERT (errno == ENOENT);
+  errno = 0;
+  ASSERT (func (BASE "file", "") == -1);
+  ASSERT (errno == ENOENT);
+  errno = 0;
+  ASSERT (func (BASE "", "") == -1);
+  ASSERT (errno == ENOENT);
+
+  /* Files.  */
+  errno = 0; /* Trailing slash.  */
+  ASSERT (func (BASE "file", BASE "file2/") == -1);
+  ASSERT (errno == ENOENT || errno == ENOTDIR);
+  errno = 0;
+  ASSERT (func (BASE "file/", BASE "file2") == -1);
+  ASSERT (errno == ENOTDIR);
+  errno = 0;
+  ASSERT (stat (BASE "file2", &st) == -1);
+  ASSERT (errno == ENOENT);
+  ASSERT (func (BASE "file", BASE "file2") == 0); /* Simple rename.  */
+  errno = 0;
+  ASSERT (stat (BASE "file", &st) == -1);
+  ASSERT (errno == ENOENT);
+  memset (&st, 0, sizeof st);
+  ASSERT (stat (BASE "file2", &st) == 0);
+  ASSERT (st.st_size == 2);
+  ASSERT (close (creat (BASE "file", 0600)) == 0); /* Overwrite.  */
+  errno = 0;
+  ASSERT (func (BASE "file2", BASE "file/") == -1);
+  ASSERT (errno == ENOTDIR);
+  ASSERT (func (BASE "file2", BASE "file") == 0);
+  memset (&st, 0, sizeof st);
+  ASSERT (stat (BASE "file", &st) == 0);
+  ASSERT (st.st_size == 2);
+  errno = 0;
+  ASSERT (stat (BASE "file2", &st) == -1);
+  ASSERT (errno == ENOENT);
+
+  /* Directories.  */
+  ASSERT (func (BASE "dir", BASE "dir2/") == 0); /* Simple rename.  */
+  errno = 0;
+  ASSERT (stat (BASE "dir", &st) == -1);
+  ASSERT (errno == ENOENT);
+  ASSERT (stat (BASE "dir2", &st) == 0);
+  ASSERT (func (BASE "dir2/", BASE "dir") == 0);
+  ASSERT (stat (BASE "dir", &st) == 0);
+  errno = 0;
+  ASSERT (stat (BASE "dir2", &st) == -1);
+  ASSERT (errno == ENOENT);
+  ASSERT (func (BASE "dir", BASE "dir2") == 0);
+  errno = 0;
+  ASSERT (stat (BASE "dir", &st) == -1);
+  ASSERT (errno == ENOENT);
+  ASSERT (stat (BASE "dir2", &st) == 0);
+  ASSERT (mkdir (BASE "dir", 0700) == 0); /* Empty onto empty.  */
+  ASSERT (func (BASE "dir2", BASE "dir") == 0);
+  ASSERT (mkdir (BASE "dir2", 0700) == 0);
+  ASSERT (func (BASE "dir2", BASE "dir/") == 0);
+  ASSERT (mkdir (BASE "dir2", 0700) == 0);
+  ASSERT (func (BASE "dir2/", BASE "dir") == 0);
+  ASSERT (mkdir (BASE "dir2", 0700) == 0);
+  ASSERT (close (creat (BASE "dir/file", 0600)) == 0); /* Empty onto full.  */
+  errno = 0;
+  ASSERT (func (BASE "dir2", BASE "dir") == -1);
+  ASSERT (errno == EEXIST || errno == ENOTEMPTY);
+  errno = 0;
+  ASSERT (func (BASE "dir2/", BASE "dir") == -1);
+  ASSERT (errno == EEXIST || errno == ENOTEMPTY);
+  errno = 0;
+  ASSERT (func (BASE "dir2", BASE "dir/") == -1);
+  ASSERT (errno == EEXIST || errno == ENOTEMPTY);
+  ASSERT (func (BASE "dir", BASE "dir2") == 0); /* Full onto empty.  */
+  errno = 0;
+  ASSERT (stat (BASE "dir", &st) == -1);
+  ASSERT (errno == ENOENT);
+  ASSERT (stat (BASE "dir2/file", &st) == 0);
+  ASSERT (mkdir (BASE "dir", 0700) == 0);
+  ASSERT (func (BASE "dir2/", BASE "dir") == 0);
+  ASSERT (stat (BASE "dir/file", &st) == 0);
+  errno = 0;
+  ASSERT (stat (BASE "dir2", &st) == -1);
+  ASSERT (errno == ENOENT);
+  ASSERT (mkdir (BASE "dir2", 0700) == 0);
+  ASSERT (func (BASE "dir", BASE "dir2/") == 0);
+  errno = 0;
+  ASSERT (stat (BASE "dir", &st) == -1);
+  ASSERT (errno == ENOENT);
+  ASSERT (stat (BASE "dir2/file", &st) == 0);
+  ASSERT (unlink (BASE "dir2/file") == 0);
+  errno = 0; /* Reject trailing dot.  */
+  ASSERT (func (BASE "dir2", BASE "dir/.") == -1);
+  ASSERT (errno == EINVAL || errno == ENOENT);
+  ASSERT (mkdir (BASE "dir", 0700) == 0);
+  errno = 0;
+  ASSERT (func (BASE "dir2", BASE "dir/.") == -1);
+  ASSERT (errno == EINVAL || errno == EBUSY || errno == EISDIR);
+  errno = 0;
+  ASSERT (func (BASE "dir2/.", BASE "dir") == -1);
+  ASSERT (errno == EINVAL || errno == EBUSY);
+  ASSERT (rmdir (BASE "dir") == 0);
+  errno = 0;
+  ASSERT (func (BASE "dir2", BASE "dir/.//") == -1);
+  ASSERT (errno == EINVAL || errno == ENOENT);
+  ASSERT (mkdir (BASE "dir", 0700) == 0);
+  errno = 0;
+  ASSERT (func (BASE "dir2", BASE "dir/.//") == -1);
+  ASSERT (errno == EINVAL || errno == EBUSY || errno == EISDIR);
+  errno = 0;
+  ASSERT (func (BASE "dir2/.//", BASE "dir") == -1);
+  ASSERT (errno == EINVAL || errno == EBUSY);
+  ASSERT (rmdir (BASE "dir2") == 0);
+  errno = 0; /* Move into subdir.  */
+  ASSERT (func (BASE "dir", BASE "dir/sub") == -1);
+  ASSERT (errno == EINVAL || errno == EACCES);
+  errno = 0;
+  ASSERT (stat (BASE "dir/sub", &st) == -1);
+  ASSERT (errno == ENOENT);
+  ASSERT (mkdir (BASE "dir/sub", 0700) == 0);
+  errno = 0;
+  ASSERT (func (BASE "dir", BASE "dir/sub") == -1);
+  ASSERT (errno == EINVAL);
+  ASSERT (stat (BASE "dir/sub", &st) == 0);
+  ASSERT (rmdir (BASE "dir/sub") == 0);
+
+  /* Mixing file and directory.  */
+  errno = 0; /* File onto dir.  */
+  ASSERT (func (BASE "file", BASE "dir") == -1);
+  ASSERT (errno == EISDIR || errno == ENOTDIR);
+  errno = 0;
+  ASSERT (func (BASE "file", BASE "dir/") == -1);
+  ASSERT (errno == EISDIR || errno == ENOTDIR);
+  errno = 0; /* Dir onto file.  */
+  ASSERT (func (BASE "dir", BASE "file") == -1);
+  ASSERT (errno == ENOTDIR);
+  errno = 0;
+  ASSERT (func (BASE "dir/", BASE "file") == -1);
+  ASSERT (errno == ENOTDIR);
+
+  /* Hard links.  */
+  ASSERT (func (BASE "file", BASE "file") == 0); /* File onto self.  */
+  memset (&st, 0, sizeof st);
+  ASSERT (stat (BASE "file", &st) == 0);
+  ASSERT (st.st_size == 2);
+  ASSERT (func (BASE "dir", BASE "dir") == 0); /* Empty dir onto self.  */
+  ASSERT (stat (BASE "dir", &st) == 0);
+  ASSERT (close (creat (BASE "dir/file", 0600)) == 0);
+  ASSERT (func (BASE "dir", BASE "dir") == 0); /* Full dir onto self.  */
+  ASSERT (unlink (BASE "dir/file") == 0);
+  {
+    /*  Not all file systems support link.  Mingw doesn't have
+        reliable st_nlink on hard links, but our implementation does
+        fail with EPERM on poor file systems, and we can detect the
+        inferior stat() via st_ino.  Cygwin 1.5.x copies rather than
+        links files on those file systems, but there, st_nlink and
+        st_ino are reliable.  */
+    int ret = link (BASE "file", BASE "file2");
+    if (!ret)
+      {
+        memset (&st, 0, sizeof st);
+        ASSERT (stat (BASE "file2", &st) == 0);
+        if (st.st_ino && st.st_nlink != 2)
+          {
+            ASSERT (unlink (BASE "file2") == 0);
+            errno = EPERM;
+            ret = -1;
+          }
+      }
+    if (ret == -1)
+      {
+        /* If the device does not support hard links, errno is
+           EPERM on Linux, EOPNOTSUPP on FreeBSD.  */
+        switch (errno)
+          {
+          case EPERM:
+          case EOPNOTSUPP:
+            if (print)
+              fputs ("skipping test: "
+                     "hard links not supported on this file system\n",
+                     stderr);
+            ASSERT (unlink (BASE "file") == 0);
+            ASSERT (rmdir (BASE "dir") == 0);
+            return 77;
+          default:
+            perror ("link");
+            return 1;
+          }
+      }
+    ASSERT (ret == 0);
+  }
+  ASSERT (func (BASE "file", BASE "file2") == 0); /* File onto hard link.  */
+  memset (&st, 0, sizeof st);
+  ASSERT (stat (BASE "file", &st) == 0);
+  ASSERT (st.st_size == 2);
+  memset (&st, 0, sizeof st);
+  ASSERT (stat (BASE "file2", &st) == 0);
+  ASSERT (st.st_size == 2);
+  ASSERT (unlink (BASE "file2") == 0);
+
+  /* Symlinks.  */
+  if (symlink (BASE "file", BASE "link1"))
+    {
+      if (print)
+        fputs ("skipping test: symlinks not supported on this filesystem\n",
+               stderr);
+      ASSERT (unlink (BASE "file") == 0);
+      ASSERT (rmdir (BASE "dir") == 0);
+      return 77;
+    }
+  ASSERT (func (BASE "link1", BASE "link2") == 0); /* Simple rename.  */
+  ASSERT (stat (BASE "file", &st) == 0);
+  errno = 0;
+  ASSERT (lstat (BASE "link1", &st) == -1);
+  ASSERT (errno == ENOENT);
+  memset (&st, 0, sizeof st);
+  ASSERT (lstat (BASE "link2", &st) == 0);
+  ASSERT (S_ISLNK (st.st_mode));
+  ASSERT (symlink (BASE "nowhere", BASE "link1") == 0); /* Overwrite.  */
+  ASSERT (func (BASE "link2", BASE "link1") == 0);
+  memset (&st, 0, sizeof st);
+  ASSERT (stat (BASE "link1", &st) == 0);
+  ASSERT (st.st_size == 2);
+  errno = 0;
+  ASSERT (lstat (BASE "link2", &st) == -1);
+  ASSERT (errno == ENOENT);
+  ASSERT (symlink (BASE "link2", BASE "link2") == 0); /* Symlink loop.  */
+  ASSERT (func (BASE "link2", BASE "link2") == 0);
+  errno = 0;
+  ASSERT (func (BASE "link2/", BASE "link2") == -1);
+  ASSERT (errno == ELOOP || errno == ENOTDIR);
+  ASSERT (func (BASE "link2", BASE "link3") == 0);
+  ASSERT (unlink (BASE "link3") == 0);
+  ASSERT (symlink (BASE "nowhere", BASE "link2") == 0); /* Dangling link.  */
+  ASSERT (func (BASE "link2", BASE "link3") == 0);
+  errno = 0;
+  ASSERT (lstat (BASE "link2", &st) == -1);
+  ASSERT (errno == ENOENT);
+  memset (&st, 0, sizeof st);
+  ASSERT (lstat (BASE "link3", &st) == 0);
+  errno = 0; /* Trailing slash on dangling.  */
+  ASSERT (func (BASE "link3/", BASE "link2") == -1);
+  ASSERT (errno == ENOENT || errno == ENOTDIR);
+  errno = 0;
+  ASSERT (func (BASE "link3", BASE "link2/") == -1);
+  ASSERT (errno == ENOENT || errno == ENOTDIR);
+  errno = 0;
+  ASSERT (lstat (BASE "link2", &st) == -1);
+  ASSERT (errno == ENOENT);
+  memset (&st, 0, sizeof st);
+  ASSERT (lstat (BASE "link3", &st) == 0);
+  errno = 0; /* Trailing slash on link to file.  */
+  ASSERT (func (BASE "link1/", BASE "link2") == -1);
+  ASSERT (errno == ENOTDIR);
+  errno = 0;
+  ASSERT (func (BASE "link1", BASE "link3/") == -1);
+  ASSERT (errno == ENOENT || errno == ENOTDIR);
+
+  /* Mixing symlink and file.  */
+  ASSERT (close (creat (BASE "file2", 0600)) == 0); /* File onto link.  */
+  ASSERT (func (BASE "file2", BASE "link3") == 0);
+  errno = 0;
+  ASSERT (stat (BASE "file2", &st) == -1);
+  ASSERT (errno == ENOENT);
+  memset (&st, 0, sizeof st);
+  ASSERT (lstat (BASE "link3", &st) == 0);
+  ASSERT (S_ISREG (st.st_mode));
+  ASSERT (unlink (BASE "link3") == 0);
+  ASSERT (symlink (BASE "nowhere", BASE "link2") == 0); /* Link onto file.  */
+  ASSERT (close (creat (BASE "file2", 0600)) == 0);
+  ASSERT (func (BASE "link2", BASE "file2") == 0);
+  errno = 0;
+  ASSERT (lstat (BASE "link2", &st) == -1);
+  ASSERT (errno == ENOENT);
+  memset (&st, 0, sizeof st);
+  ASSERT (lstat (BASE "file2", &st) == 0);
+  ASSERT (S_ISLNK (st.st_mode));
+  ASSERT (unlink (BASE "file2") == 0);
+  errno = 0; /* Trailing slash.  */
+  ASSERT (func (BASE "file/", BASE "link1") == -1);
+  ASSERT (errno == ENOTDIR);
+  errno = 0;
+  ASSERT (func (BASE "file", BASE "link1/") == -1);
+  ASSERT (errno == ENOTDIR || errno == ENOENT);
+  errno = 0;
+  ASSERT (func (BASE "link1/", BASE "file") == -1);
+  ASSERT (errno == ENOTDIR);
+  errno = 0;
+  ASSERT (func (BASE "link1", BASE "file/") == -1);
+  ASSERT (errno == ENOTDIR || errno == ENOENT);
+  memset (&st, 0, sizeof st);
+  ASSERT (lstat (BASE "file", &st) == 0);
+  ASSERT (S_ISREG (st.st_mode));
+  memset (&st, 0, sizeof st);
+  ASSERT (lstat (BASE "link1", &st) == 0);
+  ASSERT (S_ISLNK (st.st_mode));
+
+  /* Mixing symlink and directory.  */
+  errno = 0; /* Directory onto link.  */
+  ASSERT (func (BASE "dir", BASE "link1") == -1);
+  ASSERT (errno == ENOTDIR);
+  errno = 0;
+  ASSERT (func (BASE "dir/", BASE "link1") == -1);
+  ASSERT (errno == ENOTDIR);
+  errno = 0;
+  ASSERT (func (BASE "dir", BASE "link1/") == -1);
+  ASSERT (errno == ENOTDIR);
+  errno = 0; /* Link onto directory.  */
+  ASSERT (func (BASE "link1", BASE "dir") == -1);
+  ASSERT (errno == EISDIR || errno == ENOTDIR);
+  errno = 0;
+  ASSERT (func (BASE "link1", BASE "dir/") == -1);
+  ASSERT (errno == EISDIR || errno == ENOTDIR);
+  errno = 0;
+  ASSERT (func (BASE "link1/", BASE "dir") == -1);
+  ASSERT (errno == ENOTDIR);
+  memset (&st, 0, sizeof st);
+  ASSERT (lstat (BASE "link1", &st) == 0);
+  ASSERT (S_ISLNK (st.st_mode));
+  memset (&st, 0, sizeof st);
+  ASSERT (lstat (BASE "dir", &st) == 0);
+  ASSERT (S_ISDIR (st.st_mode));
+
+  /* POSIX requires rename("link-to-dir/","other") to rename "dir" and
+     leave "link-to-dir" dangling, but GNU rejects this.  POSIX
+     requires rename("dir","dangling/") to create the directory so
+     that "dangling/" now resolves, but GNU rejects this.  While we
+     prefer GNU behavior, we don't enforce it.  However, we do test
+     that the system either follows POSIX in both cases, or follows
+     GNU.  */
+  {
+    int result;
+    ASSERT (symlink (BASE "dir2", BASE "link2") == 0);
+    errno = 0;
+    result = func (BASE "dir", BASE "link2/");
+    if (result == 0)
+      {
+        /* POSIX.  */
+        errno = 0;
+        ASSERT (lstat (BASE "dir", &st) == -1);
+        ASSERT (errno == ENOENT);
+        memset (&st, 0, sizeof st);
+        ASSERT (lstat (BASE "dir2", &st) == 0);
+        ASSERT (S_ISDIR (st.st_mode));
+        memset (&st, 0, sizeof st);
+        ASSERT (lstat (BASE "link2", &st) == 0);
+        ASSERT (S_ISLNK (st.st_mode));
+        ASSERT (func (BASE "link2/", BASE "dir") == 0);
+        memset (&st, 0, sizeof st);
+        ASSERT (lstat (BASE "dir", &st) == 0);
+        ASSERT (S_ISDIR (st.st_mode));
+        errno = 0;
+        ASSERT (lstat (BASE "dir2", &st) == -1);
+        ASSERT (errno == ENOENT);
+        memset (&st, 0, sizeof st);
+        ASSERT (lstat (BASE "link2", &st) == 0);
+        ASSERT (S_ISLNK (st.st_mode));
+      }
+    else
+      {
+        /* GNU.  */
+        ASSERT (result == -1);
+        ASSERT (errno == ENOTDIR);
+        memset (&st, 0, sizeof st);
+        ASSERT (lstat (BASE "dir", &st) == 0);
+        ASSERT (S_ISDIR (st.st_mode));
+        errno = 0;
+        ASSERT (lstat (BASE "dir2", &st) == -1);
+        ASSERT (errno == ENOENT);
+        memset (&st, 0, sizeof st);
+        ASSERT (lstat (BASE "link2", &st) == 0);
+        ASSERT (S_ISLNK (st.st_mode));
+        ASSERT (unlink (BASE "link2") == 0);
+        ASSERT (symlink (BASE "dir", BASE "link2") == 0);
+        errno = 0; /* OpenBSD notices that link2/ and dir are the same.  */
+        result = func (BASE "link2/", BASE "dir");
+        if (result) /* GNU/Linux rejects attempts to use link2/.  */
+          {
+            ASSERT (result == -1);
+            ASSERT (errno == ENOTDIR);
+          }
+        memset (&st, 0, sizeof st);
+        ASSERT (lstat (BASE "dir", &st) == 0);
+        ASSERT (S_ISDIR (st.st_mode));
+        errno = 0;
+        ASSERT (lstat (BASE "dir2", &st) == -1);
+        ASSERT (errno == ENOENT);
+        memset (&st, 0, sizeof st);
+        ASSERT (lstat (BASE "link2", &st) == 0);
+        ASSERT (S_ISLNK (st.st_mode));
+      }
+  }
+
+  /* Clean up.  */
+  ASSERT (unlink (BASE "file") == 0);
+  ASSERT (rmdir (BASE "dir") == 0);
+  ASSERT (unlink (BASE "link1") == 0);
+  ASSERT (unlink (BASE "link2") == 0);
+
+  return 0;
+}