Commit 8df4f51956ba711ca69e93b8c93366ba7eaaab0b

Edward Thomson 2020-05-23T15:04:54

clar: copy files internally instead of /bin/cp clar has historically shelled out to `/bin/cp` to copy test fixtures into a sandbox. This has two deficiencies: 1. It's slower than simply opening the source and destination and copying them in a read/write loop. On my Mac, the `/bin/cp` based approach takes ~2:40 for a full test pass. Using a read/write loop to copy the files ourselves takes ~1:50. 2. It's noisy. Since the leak detector follows fork/exec, we'll end up running the leak detector on `/bin/cp`. This would be fine, except that the leak detector spams the console on startup and shutdown, so it adds a _lot_ of additional information to the test runs that is useless. By not forking and using this internal system, we see much less output.

diff --git a/tests/clar/fs.h b/tests/clar/fs.h
index 7c7dde6..6968742 100644
--- a/tests/clar/fs.h
+++ b/tests/clar/fs.h
@@ -1,3 +1,6 @@
+/* fcopyfile on macOS is slower than a simple read/write loop? */
+#undef USE_FCOPYFILE
+
 #ifdef _WIN32
 
 #define RM_RETRY_COUNT	5
@@ -254,6 +257,16 @@ cl_fs_cleanup(void)
 
 #include <errno.h>
 #include <string.h>
+#include <limits.h>
+#include <dirent.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+
+#if defined(__APPLE__) || defined(__FreeBSD__)
+# include <copyfile.h>
+#endif
 
 static int
 shell_out(char * const argv[])
@@ -281,31 +294,143 @@ shell_out(char * const argv[])
 	return WEXITSTATUS(status);
 }
 
+static void basename_r(const char **out, int *out_len, const char *in)
+{
+	size_t in_len = strlen(in), start_pos;
+
+	for (in_len = strlen(in); in_len; in_len--) {
+		if (in[in_len - 1] != '/')
+			break;
+	}
+
+	for (start_pos = in_len; start_pos; start_pos--) {
+		if (in[start_pos - 1] == '/')
+			break;
+	}
+
+	cl_assert(in_len - start_pos < INT_MAX);
+
+	if (in_len - start_pos > 0) {
+		*out = &in[start_pos];
+		*out_len = (in_len - start_pos);
+	} else {
+		*out = "/";
+		*out_len = 1;
+	}
+}
+
+static char *joinpath(const char *dir, const char *base, int base_len)
+{
+	char *out;
+	int len;
+
+	if (base_len == -1) {
+		size_t bl = strlen(base);
+
+		cl_assert(bl < INT_MAX);
+		base_len = (int)bl;
+	}
+
+	len = strlen(dir) + base_len + 2;
+	cl_assert(len > 0);
+
+	cl_assert(out = malloc(len));
+	cl_assert(snprintf(out, len, "%s/%.*s", dir, base_len, base) < len);
+
+	return out;
+}
+
 static void
-fs_copy(const char *_source, const char *dest)
+fs_copydir_helper(const char *source, const char *dest, int dmode)
 {
-	char *argv[5];
-	char *source;
-	size_t source_len;
+	DIR *source_dir;
+	struct dirent *d;
 
-	source = strdup(_source);
-	source_len = strlen(source);
+	mkdir(dest, dmode);
 
-	if (source[source_len - 1] == '/')
-		source[source_len - 1] = 0;
+	cl_assert_(source_dir = opendir(source), "Could not open source dir");
+	while ((d = (errno = 0, readdir(source_dir))) != NULL) {
+		char *child;
 
-	argv[0] = "/bin/cp";
-	argv[1] = "-R";
-	argv[2] = source;
-	argv[3] = (char *)dest;
-	argv[4] = NULL;
+		if (!strcmp(d->d_name, ".") || !strcmp(d->d_name, ".."))
+			continue;
 
-	cl_must_pass_(
-		shell_out(argv),
-		"Failed to copy test fixtures to sandbox"
-	);
+		child = joinpath(source, d->d_name, -1);
+		fs_copy(child, dest);
+		free(child);
+	}
+
+	cl_assert_(errno == 0, "Failed to iterate source dir");
+
+	closedir(source_dir);
+}
+
+static void
+fs_copyfile_helper(const char *source, const char *dest, int dmode)
+{
+	int in, out;
+
+	cl_must_pass((in = open(source, O_RDONLY)));
+	cl_must_pass((out = open(dest, O_WRONLY|O_CREAT|O_TRUNC, dmode)));
+
+#if USE_FCOPYFILE && (defined(__APPLE__) || defined(__FreeBSD__))
+	cl_must_pass(fcopyfile(in, out, 0, COPYFILE_DATA));
+#else
+	{
+		char buf[131072];
+		ssize_t ret;
+
+		while ((ret = read(in, buf, sizeof(buf))) > 0) {
+			size_t len = (size_t)ret;
+
+			while (len && (ret = write(out, buf, len)) > 0) {
+				cl_assert(ret <= (ssize_t)len);
+				len -= ret;
+			}
+			cl_assert(ret >= 0);
+		}
+		cl_assert(ret == 0);
+	}
+#endif
+
+	close(in);
+	close(out);
+}
+
+static void
+fs_copy(const char *source, const char *_dest)
+{
+	char *dbuf = NULL;
+	const char *dest;
+	struct stat source_st, dest_st;
+
+	cl_must_pass_(lstat(source, &source_st), "Failed to stat copy source");
+
+	if (lstat(_dest, &dest_st) == 0) {
+		const char *base;
+		int base_len;
+
+		/* Target exists and is directory; append basename */
+		cl_assert(S_ISDIR(dest_st.st_mode));
+
+		basename_r(&base, &base_len, source);
+		cl_assert(base_len < INT_MAX);
+
+		dbuf = joinpath(_dest, base, base_len);
+		dest = dbuf;
+	} else if (errno != ENOENT) {
+		cl_fail("Cannot copy; cannot stat destination");
+	} else {
+		dest = _dest;
+	}
+
+	if (S_ISDIR(source_st.st_mode)) {
+		fs_copydir_helper(source, dest, source_st.st_mode);
+	} else {
+		fs_copyfile_helper(source, dest, source_st.st_mode);
+	}
 
-	free(source);
+	free(dbuf);
 }
 
 static void