diff options
-rw-r--r-- | .dir-locals.el | 26 | ||||
-rw-r--r-- | README.md | 7 | ||||
-rw-r--r-- | configure.ac | 13 | ||||
-rwxr-xr-x | example/fselclient.c | 4 | ||||
-rw-r--r-- | include/fuse_common.h | 4 | ||||
-rw-r--r-- | include/fuse_lowlevel.h | 26 | ||||
-rw-r--r-- | lib/fuse.c | 9 | ||||
-rwxr-xr-x | lib/fuse_loop_mt.c | 2 | ||||
-rwxr-xr-x | lib/fuse_lowlevel.c | 155 | ||||
-rw-r--r-- | lib/fuse_session.c | 5 | ||||
-rwxr-xr-x | lib/fuse_signals.c | 1 | ||||
-rw-r--r-- | lib/mount.c | 14 | ||||
-rw-r--r-- | lib/mount_bsd.c | 2 | ||||
-rw-r--r-- | test/.gitignore | 2 | ||||
-rw-r--r-- | test/Makefile | 7 | ||||
-rw-r--r-- | test/Makefile.am | 2 | ||||
-rw-r--r-- | test/conftest.py | 85 | ||||
-rw-r--r-- | test/pytest.ini | 2 | ||||
-rw-r--r-- | test/test.c | 2 | ||||
-rwxr-xr-x | test/test_examples.py | 321 | ||||
-rwxr-xr-x | test/test_fuse.py | 32 | ||||
-rw-r--r-- | test/util.py | 38 |
22 files changed, 616 insertions, 143 deletions
diff --git a/.dir-locals.el b/.dir-locals.el new file mode 100644 index 0000000..c70a23a --- /dev/null +++ b/.dir-locals.el @@ -0,0 +1,26 @@ +((nil . ((eval . (add-hook 'before-save-hook + 'whitespace-cleanup nil t)))) + (python-mode . ((indent-tabs-mode . nil))) + (c-mode . ((c-file-style . "stroustrup") + (indent-tabs-mode . t) + (tab-width . 8) + (c-basic-offset . 8) + (c-file-offsets . + ((block-close . 0) + (brace-list-close . 0) + (brace-list-entry . 0) + (brace-list-intro . +) + (case-label . 0) + (class-close . 0) + (defun-block-intro . +) + (defun-close . 0) + (defun-open . 0) + (else-clause . 0) + (inclass . +) + (label . 0) + (statement . 0) + (statement-block-intro . +) + (statement-case-intro . +) + (statement-cont . +) + (substatement . +) + (topmost-intro . 0)))))) @@ -56,6 +56,12 @@ tarball, build and install with make -j8 make install +To run some self tests, you need a Python 3 environment with the +[py.test](http://www.pytest.org/) module installed. To run the tests, +execute + + python3 -m pytest test/ + You may also need to add `/usr/local/lib` to `/etc/ld.so.conf` and/or run *ldconfig*. If you're building from the git repository (instead of using a release tarball), you also need to run `./makeconf.sh` to @@ -111,4 +117,3 @@ https://lists.sourceforge.net/lists/listinfo/fuse-devel). Please report any bugs on the GitHub issue tracker at https://github.com/libfuse/libfuse/issues. - diff --git a/configure.ac b/configure.ac index 29a281d..7ca9f4f 100644 --- a/configure.ac +++ b/configure.ac @@ -29,13 +29,15 @@ AC_ARG_ENABLE(util, [ --enable-util Compile with util ]) AC_ARG_ENABLE(example, [ --enable-example Compile with examples ]) +AC_ARG_ENABLE(test, + [ --enable-test Compile with tests ]) AC_ARG_ENABLE(mtab, [ --disable-mtab Disable and ignore usage of /etc/mtab ]) AC_ARG_WITH(pkgconfigdir, - [ --with-pkgconfigdir=DIR pkgconfig file in DIR @<:@LIBDIR/pkgconfig@:>@], - [pkgconfigdir=$withval], - [pkgconfigdir='${libdir}/pkgconfig']) + [ --with-pkgconfigdir=DIR pkgconfig file in DIR @<:@LIBDIR/pkgconfig@:>@], + [pkgconfigdir=$withval], + [pkgconfigdir='${libdir}/pkgconfig']) AC_SUBST(pkgconfigdir) subdirs2="include" @@ -49,6 +51,9 @@ fi if test "$enable_example" != "no"; then subdirs2="$subdirs2 example"; fi +if test "$enable_test" != "no"; then + subdirs2="$subdirs2 test"; +fi if test "$enable_mtab" = "no"; then AC_DEFINE(IGNORE_MTAB, 1, [Don't update /etc/mtab]) fi @@ -120,7 +125,7 @@ if test "$arch" = linux -a "$cross_compiling" != "yes"; then fi fi -AC_CONFIG_FILES([fuse3.pc Makefile lib/Makefile util/Makefile example/Makefile include/Makefile doc/Makefile]) +AC_CONFIG_FILES([fuse3.pc Makefile lib/Makefile util/Makefile example/Makefile include/Makefile doc/Makefile test/Makefile]) AC_OUTPUT if test "$util_linux_ok" = no; then diff --git a/example/fselclient.c b/example/fselclient.c index ac8b7b0..637cb07 100755 --- a/example/fselclient.c +++ b/example/fselclient.c @@ -40,7 +40,7 @@ int main(void) { static const char hex_map[FSEL_FILES] = "0123456789ABCDEF"; int fds[FSEL_FILES]; - int i, nfds; + int i, nfds, tries; for (i = 0; i < FSEL_FILES; i++) { char name[] = { hex_map[i], '\0' }; @@ -52,7 +52,7 @@ int main(void) } nfds = fds[FSEL_FILES - 1] + 1; - while (1) { + for(tries=0; tries < 16; tries++) { static char buf[4096]; fd_set rfds; int rc; diff --git a/include/fuse_common.h b/include/fuse_common.h index d200d34..75e55e9 100644 --- a/include/fuse_common.h +++ b/include/fuse_common.h @@ -114,7 +114,7 @@ struct fuse_file_info { #define FUSE_CAP_SPLICE_READ (1 << 9) #define FUSE_CAP_FLOCK_LOCKS (1 << 10) #define FUSE_CAP_IOCTL_DIR (1 << 11) -#define FUSE_CAP_AUTO_INVAL_DATA (1 << 12) +#define FUSE_CAP_AUTO_INVAL_DATA (1 << 12) #define FUSE_CAP_READDIRPLUS (1 << 13) #define FUSE_CAP_READDIRPLUS_AUTO (1 << 14) #define FUSE_CAP_ASYNC_DIO (1 << 15) @@ -429,7 +429,7 @@ struct fuse_bufvec { }; /* Initialize bufvec with a single buffer of given size */ -#define FUSE_BUFVEC_INIT(size__) \ +#define FUSE_BUFVEC_INIT(size__) \ ((struct fuse_bufvec) { \ /* .count= */ 1, \ /* .idx = */ 0, \ diff --git a/include/fuse_lowlevel.h b/include/fuse_lowlevel.h index 89d10a7..a01dbf6 100644 --- a/include/fuse_lowlevel.h +++ b/include/fuse_lowlevel.h @@ -602,7 +602,7 @@ struct fuse_lowlevel_ops { * * fi->fh will contain the value set by the opendir method, or * will be undefined if the opendir method didn't set any value. - * + * * Returning a directory entry from readdir() does not affect * its lookup count. * @@ -932,11 +932,11 @@ struct fuse_lowlevel_ops { * kernel supports splicing from the fuse device, then the * data will be made available in pipe for supporting zero * copy data transfer. - * - * buf->count is guaranteed to be one (and thus buf->idx is - * always zero). The write_buf handler must ensure that - * bufv->off is correctly updated (reflecting the number of - * bytes read from bufv->buf[0]). + * + * buf->count is guaranteed to be one (and thus buf->idx is + * always zero). The write_buf handler must ensure that + * bufv->off is correctly updated (reflecting the number of + * bytes read from bufv->buf[0]). * * Introduced in version 2.9 * @@ -1036,7 +1036,7 @@ struct fuse_lowlevel_ops { * * fi->fh will contain the value set by the opendir method, or * will be undefined if the opendir method didn't set any value. - * + * * In contrast to readdir() (which does not affect the lookup counts), * the lookup count of every entry returned by readdirplus(), except "." * and "..", is incremented by one. @@ -1402,7 +1402,7 @@ int fuse_lowlevel_notify_poll(struct fuse_pollhandle *ph); * @return zero for success, -errno for failure */ int fuse_lowlevel_notify_inval_inode(struct fuse_chan *ch, fuse_ino_t ino, - off_t off, off_t len); + off_t off, off_t len); /** * Notify to invalidate parent attributes and the dentry matching @@ -1419,7 +1419,7 @@ int fuse_lowlevel_notify_inval_inode(struct fuse_chan *ch, fuse_ino_t ino, * @return zero for success, -errno for failure */ int fuse_lowlevel_notify_inval_entry(struct fuse_chan *ch, fuse_ino_t parent, - const char *name, size_t namelen); + const char *name, size_t namelen); /** * Notify to invalidate parent attributes and delete the dentry matching @@ -1702,14 +1702,6 @@ int fuse_session_loop_mt(struct fuse_session *se); * ----------------------------------------------------------- */ /** - * Query the file descriptor of the channel - * - * @param ch the channel - * @return the file descriptor passed to fuse_chan_new() - */ -int fuse_chan_fd(struct fuse_chan *ch); - -/** * Obtain counted reference to the channel * * @param ch the channel @@ -118,11 +118,11 @@ struct node_table { }; #define container_of(ptr, type, member) ({ \ - const typeof( ((type *)0)->member ) *__mptr = (ptr); \ - (type *)( (char *)__mptr - offsetof(type,member) );}) + const typeof( ((type *)0)->member ) *__mptr = (ptr); \ + (type *)( (char *)__mptr - offsetof(type,member) );}) #define list_entry(ptr, type, member) \ - container_of(ptr, type, member) + container_of(ptr, type, member) struct list_head { struct list_head *next; @@ -4320,7 +4320,7 @@ static int fuse_session_loop_remember(struct fuse *f) time_t next_clean; struct fuse_chan *ch = fuse_session_chan(se); struct pollfd fds = { - .fd = fuse_chan_fd(ch), + .fd = ch->fd, .events = POLLIN }; struct fuse_buf fbuf = { @@ -4831,4 +4831,3 @@ void fuse_destroy(struct fuse *f) free(f); fuse_delete_context_key(); } - diff --git a/lib/fuse_loop_mt.c b/lib/fuse_loop_mt.c index 6d7f051..036f75c 100755 --- a/lib/fuse_loop_mt.c +++ b/lib/fuse_loop_mt.c @@ -193,7 +193,7 @@ static struct fuse_chan *fuse_clone_chan(struct fuse_mt *mt) } fcntl(clonefd, F_SETFD, FD_CLOEXEC); - masterfd = fuse_chan_fd(mt->prevch); + masterfd = mt->prevch->fd; res = ioctl(clonefd, FUSE_DEV_IOC_CLONE, &masterfd); if (res == -1) { fprintf(stderr, "fuse: failed to clone device fd: %s\n", diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c index 542d7ae..91568b1 100755 --- a/lib/fuse_lowlevel.c +++ b/lib/fuse_lowlevel.c @@ -158,80 +158,11 @@ static struct fuse_req *fuse_ll_alloc_req(struct fuse_ll *f) return req; } -static int fuse_chan_recv(struct fuse_session *se, struct fuse_buf *buf, - struct fuse_chan *ch) -{ - struct fuse_ll *f = se->f; - int err; - ssize_t res; - - if (!buf->mem) { - buf->mem = malloc(f->bufsize); - if (!buf->mem) { - fprintf(stderr, - "fuse: failed to allocate read buffer\n"); - return -ENOMEM; - } - } - -restart: - res = read(fuse_chan_fd(ch), buf->mem, f->bufsize); - err = errno; - - if (fuse_session_exited(se)) - return 0; - if (res == -1) { - /* ENOENT means the operation was interrupted, it's safe - to restart */ - if (err == ENOENT) - goto restart; - - if (err == ENODEV) { - fuse_session_exit(se); - return 0; - } - /* Errors occurring during normal operation: EINTR (read - interrupted), EAGAIN (nonblocking I/O), ENODEV (filesystem - umounted) */ - if (err != EINTR && err != EAGAIN) - perror("fuse: reading device"); - return -err; - } - if ((size_t) res < sizeof(struct fuse_in_header)) { - fprintf(stderr, "short read on fuse device\n"); - return -EIO; - } - - buf->size = res; - - return res; -} - -static int fuse_chan_send(struct fuse_chan *ch, const struct iovec iov[], - size_t count) -{ - ssize_t res = writev(fuse_chan_fd(ch), iov, count); - int err = errno; - - if (res == -1) { - struct fuse_session *se = fuse_chan_session(ch); - - assert(se != NULL); - - /* ENOENT means the operation was interrupted */ - if (!fuse_session_exited(se) && err != ENOENT) - perror("fuse: writing device"); - return -err; - } - - return 0; -} - void fuse_chan_close(struct fuse_chan *ch) { - int fd = fuse_chan_fd(ch); + int fd = ch->fd; if (fd != -1) - close(fd); + close(fd); } @@ -257,9 +188,24 @@ static int fuse_send_msg(struct fuse_ll *f, struct fuse_chan *ch, } } - return fuse_chan_send(ch, iov, count); + ssize_t res = writev(ch->fd, iov, count); + int err = errno; + + if (res == -1) { + struct fuse_session *se = fuse_chan_session(ch); + + assert(se != NULL); + + /* ENOENT means the operation was interrupted */ + if (!fuse_session_exited(se) && err != ENOENT) + perror("fuse: writing device"); + return -err; + } + + return 0; } + int fuse_send_reply_iov_nofree(fuse_req_t req, int error, struct iovec *iov, int count) { @@ -831,7 +777,7 @@ static int fuse_send_data_iov(struct fuse_ll *f, struct fuse_chan *ch, splice_flags |= SPLICE_F_MOVE; res = splice(llp->pipe[0], NULL, - fuse_chan_fd(ch), NULL, out->len, splice_flags); + ch->fd, NULL, out->len, splice_flags); if (res == -1) { res = -errno; perror("fuse: splice from pipe"); @@ -2051,7 +1997,7 @@ static void do_init(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) fprintf(stderr, " max_background=%i\n", outarg.max_background); fprintf(stderr, " congestion_threshold=%i\n", - outarg.congestion_threshold); + outarg.congestion_threshold); fprintf(stderr, " time_gran=%u\n", outarg.time_gran); } @@ -2156,7 +2102,7 @@ int fuse_lowlevel_notify_poll(struct fuse_pollhandle *ph) } int fuse_lowlevel_notify_inval_inode(struct fuse_chan *ch, fuse_ino_t ino, - off_t off, off_t len) + off_t off, off_t len) { struct fuse_notify_inval_inode_out outarg; struct fuse_ll *f; @@ -2180,7 +2126,7 @@ int fuse_lowlevel_notify_inval_inode(struct fuse_chan *ch, fuse_ino_t ino, } int fuse_lowlevel_notify_inval_entry(struct fuse_chan *ch, fuse_ino_t parent, - const char *name, size_t namelen) + const char *name, size_t namelen) { struct fuse_notify_inval_entry_out outarg; struct fuse_ll *f; @@ -2751,16 +2697,16 @@ static void fuse_ll_pipe_destructor(void *data) fuse_ll_pipe_free(llp); } -#ifdef HAVE_SPLICE int fuse_session_receive_buf(struct fuse_session *se, struct fuse_buf *buf, struct fuse_chan *ch) { struct fuse_ll *f = se->f; + int err; + ssize_t res; +#ifdef HAVE_SPLICE size_t bufsize = f->bufsize; struct fuse_ll_pipe *llp; struct fuse_buf tmpbuf; - int err; - int res; if (f->conn.proto_minor < 14 || !(f->conn.want & FUSE_CAP_SPLICE_READ)) goto fallback; @@ -2782,7 +2728,7 @@ int fuse_session_receive_buf(struct fuse_session *se, struct fuse_buf *buf, goto fallback; } - res = splice(fuse_chan_fd(ch), NULL, llp->pipe[1], NULL, bufsize, 0); + res = splice(ch->fd, NULL, llp->pipe[1], NULL, bufsize, 0); err = errno; if (fuse_session_exited(se)) @@ -2855,15 +2801,48 @@ int fuse_session_receive_buf(struct fuse_session *se, struct fuse_buf *buf, return res; fallback: - return fuse_chan_recv(se, buf, ch); -} -#else -int fuse_session_receive_buf(struct fuse_session *se, struct fuse_buf *buf, - struct fuse_chan *ch) -{ - return fuse_chan_recv(se, buf, ch); -} #endif + if (!buf->mem) { + buf->mem = malloc(f->bufsize); + if (!buf->mem) { + fprintf(stderr, + "fuse: failed to allocate read buffer\n"); + return -ENOMEM; + } + } + +restart: + res = read(ch->fd, buf->mem, f->bufsize); + err = errno; + + if (fuse_session_exited(se)) + return 0; + if (res == -1) { + /* ENOENT means the operation was interrupted, it's safe + to restart */ + if (err == ENOENT) + goto restart; + + if (err == ENODEV) { + fuse_session_exit(se); + return 0; + } + /* Errors occurring during normal operation: EINTR (read + interrupted), EAGAIN (nonblocking I/O), ENODEV (filesystem + umounted) */ + if (err != EINTR && err != EAGAIN) + perror("fuse: reading device"); + return -err; + } + if ((size_t) res < sizeof(struct fuse_in_header)) { + fprintf(stderr, "short read on fuse device\n"); + return -EIO; + } + + buf->size = res; + + return res; +} #define MIN_BUFSIZE 0x21000 diff --git a/lib/fuse_session.c b/lib/fuse_session.c index 42fe5c3..6b54e43 100644 --- a/lib/fuse_session.c +++ b/lib/fuse_session.c @@ -90,11 +90,6 @@ struct fuse_chan *fuse_chan_new(int fd) return ch; } -int fuse_chan_fd(struct fuse_chan *ch) -{ - return ch->fd; -} - struct fuse_session *fuse_chan_session(struct fuse_chan *ch) { return ch->se; diff --git a/lib/fuse_signals.c b/lib/fuse_signals.c index b992b71..9fa787c 100755 --- a/lib/fuse_signals.c +++ b/lib/fuse_signals.c @@ -72,4 +72,3 @@ void fuse_remove_signal_handlers(struct fuse_session *se) set_one_signal_handler(SIGTERM, exit_handler, 1); set_one_signal_handler(SIGPIPE, SIG_IGN, 1); } - diff --git a/lib/mount.c b/lib/mount.c index de4ae74..7be7b25 100644 --- a/lib/mount.c +++ b/lib/mount.c @@ -28,12 +28,12 @@ #ifdef __NetBSD__ #include <perfuse.h> -#define MS_RDONLY MNT_RDONLY -#define MS_NOSUID MNT_NOSUID -#define MS_NODEV MNT_NODEV -#define MS_NOEXEC MNT_NOEXEC -#define MS_SYNCHRONOUS MNT_SYNCHRONOUS -#define MS_NOATIME MNT_NOATIME +#define MS_RDONLY MNT_RDONLY +#define MS_NOSUID MNT_NOSUID +#define MS_NODEV MNT_NODEV +#define MS_NOEXEC MNT_NOEXEC +#define MS_SYNCHRONOUS MNT_SYNCHRONOUS +#define MS_NOATIME MNT_NOATIME #define umount2(mnt, flags) unmount(mnt, (flags == 2) ? MNT_FORCE : 0) #endif @@ -398,7 +398,7 @@ static int fuse_mount_fusermount(const char *mountpoint, struct mount_opts *mo, rv = receive_fd(fds[1]); if (!mo->auto_unmount) { - /* with auto_unmount option fusermount will not exit until + /* with auto_unmount option fusermount will not exit until this socket is closed */ close(fds[1]); waitpid(pid, NULL, 0); /* bury zombie */ diff --git a/lib/mount_bsd.c b/lib/mount_bsd.c index 8f3acf0..6a7c958 100644 --- a/lib/mount_bsd.c +++ b/lib/mount_bsd.c @@ -43,7 +43,7 @@ struct mount_opts { char *kernel_opts; }; -#define FUSE_DUAL_OPT_KEY(templ, key) \ +#define FUSE_DUAL_OPT_KEY(templ, key) \ FUSE_OPT_KEY(templ, key), FUSE_OPT_KEY("no" templ, key) static const struct fuse_opt fuse_mount_opts[] = { diff --git a/test/.gitignore b/test/.gitignore index 7b95378..b7041be 100644 --- a/test/.gitignore +++ b/test/.gitignore @@ -1,2 +1,2 @@ -!Makefile test +__pycache__/ diff --git a/test/Makefile b/test/Makefile deleted file mode 100644 index 738dd53..0000000 --- a/test/Makefile +++ /dev/null @@ -1,7 +0,0 @@ -CC=gcc -CFLAGS=-Wall -W - -all: test - -clean: - rm -f *.o test diff --git a/test/Makefile.am b/test/Makefile.am new file mode 100644 index 0000000..55d6950 --- /dev/null +++ b/test/Makefile.am @@ -0,0 +1,2 @@ +bin_PROGRAMS = test +test_SOURCES = test.c diff --git a/test/conftest.py b/test/conftest.py new file mode 100644 index 0000000..d14350d --- /dev/null +++ b/test/conftest.py @@ -0,0 +1,85 @@ +import sys +import pytest +import time +import re + +# If a test fails, wait a moment before retrieving the captured +# stdout/stderr. When using a server process, this makes sure that we capture +# any potential output of the server that comes *after* a test has failed. For +# example, if a request handler raises an exception, the server first signals an +# error to FUSE (causing the test to fail), and then logs the exception. Without +# the extra delay, the exception will go into nowhere. +@pytest.mark.hookwrapper +def pytest_pyfunc_call(pyfuncitem): + outcome = yield + failed = outcome.excinfo is not None + if failed: + time.sleep(1) + +@pytest.fixture() +def pass_capfd(request, capfd): + '''Provide capfd object to UnitTest instances''' + request.instance.capfd = capfd + +def check_test_output(capfd): + (stdout, stderr) = capfd.readouterr() + + # Write back what we've read (so that it will still be printed. + sys.stdout.write(stdout) + sys.stderr.write(stderr) + + # Strip out false positives + for (pattern, flags, count) in capfd.false_positives: + cp = re.compile(pattern, flags) + (stdout, cnt) = cp.subn('', stdout, count=count) + if count == 0 or count - cnt > 0: + stderr = cp.sub('', stderr, count=count - cnt) + + for pattern in ('exception', 'error', 'warning', 'fatal', + 'fault', 'crash(?:ed)?', 'abort(?:ed)'): + cp = re.compile(r'\b{}\b'.format(pattern), re.IGNORECASE | re.MULTILINE) + hit = cp.search(stderr) + if hit: + raise AssertionError('Suspicious output to stderr (matched "%s")' % hit.group(0)) + hit = cp.search(stdout) + if hit: + raise AssertionError('Suspicious output to stdout (matched "%s")' % hit.group(0)) + +def register_output(self, pattern, count=1, flags=re.MULTILINE): + '''Register *pattern* as false positive for output checking + + This prevents the test from failing because the output otherwise + appears suspicious. + ''' + + self.false_positives.append((pattern, flags, count)) + +# This is a terrible hack that allows us to access the fixtures from the +# pytest_runtest_call hook. Among a lot of other hidden assumptions, it probably +# relies on tests running sequential (i.e., don't dare to use e.g. the xdist +# plugin) +current_capfd = None +@pytest.yield_fixture(autouse=True) +def save_cap_fixtures(request, capfd): + global current_capfd + capfd.false_positives = [] + + # Monkeypatch in a function to register false positives + type(capfd).register_output = register_output + + if request.config.getoption('capture') == 'no': + capfd = None + current_capfd = capfd + bak = current_capfd + yield + + # Try to catch problems with this hack (e.g. when running tests + # simultaneously) + assert bak is current_capfd + current_capfd = None + +@pytest.hookimpl(trylast=True) +def pytest_runtest_call(item): + capfd = current_capfd + if capfd is not None: + check_test_output(capfd) diff --git a/test/pytest.ini b/test/pytest.ini new file mode 100644 index 0000000..bc4af36 --- /dev/null +++ b/test/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +addopts = --verbose --assert=rewrite --tb=native -x diff --git a/test/test.c b/test/test.c index 5d5750d..2b38bb2 100644 --- a/test/test.c +++ b/test/test.c @@ -993,7 +993,7 @@ static int do_test_open_acc(int flags, const char *flags_str, int mode, int err) int res; int fd; - start_test("open_acc(%s) mode: 0%03o error: '%s'", flags_str, mode, + start_test("open_acc(%s) mode: 0%03o message: '%s'", flags_str, mode, strerror(err)); unlink(testfile); res = create_file(testfile, data, datalen); diff --git a/test/test_examples.py b/test/test_examples.py new file mode 100755 index 0000000..02d3409 --- /dev/null +++ b/test/test_examples.py @@ -0,0 +1,321 @@ +#!/usr/bin/env python3 + +if __name__ == '__main__': + import pytest + import sys + sys.exit(pytest.main([__file__] + sys.argv[1:])) + +import subprocess +import os +import sys +import pytest +import stat +import shutil +import filecmp +import errno +from tempfile import NamedTemporaryFile +from util import wait_for_mount, umount, cleanup + +basename = os.path.join(os.path.dirname(__file__), '..') +TEST_FILE = __file__ + +with open(TEST_FILE, 'rb') as fh: + TEST_DATA = fh.read() + +def name_generator(__ctr=[0]): + __ctr[0] += 1 + return 'testfile_%d' % __ctr[0] + +@pytest.mark.parametrize("name", ('hello', 'hello_ll')) +def test_hello(tmpdir, name): + mnt_dir = str(tmpdir) + cmdline = [os.path.join(basename, 'example', name), + '-f', mnt_dir ] + mount_process = subprocess.Popen(cmdline) + try: + wait_for_mount(mount_process, mnt_dir) + assert os.listdir(mnt_dir) == [ 'hello' ] + filename = os.path.join(mnt_dir, 'hello') + with open(filename, 'r') as fh: + assert fh.read() == 'Hello World!\n' + with pytest.raises(IOError) as exc_info: + open(filename, 'r+') + assert exc_info.value.errno == errno.EACCES + with pytest.raises(IOError) as exc_info: + open(filename + 'does-not-exist', 'r+') + assert exc_info.value.errno == errno.ENOENT + except: + cleanup(mnt_dir) + raise + else: + umount(mount_process, mnt_dir) + +@pytest.mark.parametrize("name", ('fusexmp', 'fusexmp_fh')) +def test_fusexmp_fh(tmpdir, name): + mnt_dir = str(tmpdir.mkdir('mnt')) + src_dir = str(tmpdir.mkdir('src')) + + cmdline = [os.path.join(basename, 'example', name), + '-f', '-o' , 'use_ino,readdir_ino,kernel_cache', + mnt_dir ] + mount_process = subprocess.Popen(cmdline) + try: + wait_for_mount(mount_process, mnt_dir) + work_dir = os.path.join(mnt_dir, src_dir) + tst_write(work_dir) + tst_mkdir(work_dir) + tst_symlink(work_dir) + tst_mknod(work_dir) + if os.getuid() == 0: + tst_chown(work_dir) + # Underlying fs may not have full nanosecond resolution + tst_utimens(work_dir, ns_tol=1000) + tst_link(work_dir) + tst_readdir(work_dir) + tst_statvfs(work_dir) + tst_truncate_path(work_dir) + tst_truncate_fd(work_dir) + tst_unlink(work_dir) + tst_passthrough(src_dir, work_dir) + except: + cleanup(mnt_dir) + raise + else: + umount(mount_process, mnt_dir) + +def test_fsel(tmpdir): + mnt_dir = str(tmpdir) + cmdline = [os.path.join(basename, 'example', 'fsel'), + '-f', mnt_dir ] + mount_process = subprocess.Popen(cmdline) + try: + wait_for_mount(mount_process, mnt_dir) + cmdline = [ os.path.join(basename, 'example', 'fselclient') ] + subprocess.check_call(cmdline, cwd=mnt_dir) + except: + cleanup(mnt_dir) + raise + else: + umount(mount_process, mnt_dir) + +def checked_unlink(filename, path, isdir=False): + fullname = os.path.join(path, filename) + if isdir: + os.rmdir(fullname) + else: + os.unlink(fullname) + with pytest.raises(OSError) as exc_info: + os.stat(fullname) + assert exc_info.value.errno == errno.ENOENT + assert filename not in os.listdir(path) + +def tst_mkdir(mnt_dir): + dirname = name_generator() + fullname = mnt_dir + "/" + dirname + os.mkdir(fullname) + fstat = os.stat(fullname) + assert stat.S_ISDIR(fstat.st_mode) + assert os.listdir(fullname) == [] + assert fstat.st_nlink in (1,2) + assert dirname in os.listdir(mnt_dir) + checked_unlink(dirname, mnt_dir, isdir=True) + +def tst_symlink(mnt_dir): + linkname = name_generator() + fullname = mnt_dir + "/" + linkname + os.symlink("/imaginary/dest", fullname) + fstat = os.lstat(fullname) + assert stat.S_ISLNK(fstat.st_mode) + assert os.readlink(fullname) == "/imaginary/dest" + assert fstat.st_nlink == 1 + assert linkname in os.listdir(mnt_dir) + checked_unlink(linkname, mnt_dir) + +def tst_mknod(mnt_dir): + filename = os.path.join(mnt_dir, name_generator()) + shutil.copyfile(TEST_FILE, filename) + fstat = os.lstat(filename) + assert stat.S_ISREG(fstat.st_mode) + assert fstat.st_nlink == 1 + assert os.path.basename(filename) in os.listdir(mnt_dir) + assert filecmp.cmp(TEST_FILE, filename, False) + checked_unlink(filename, mnt_dir) + +def tst_chown(mnt_dir): + filename = os.path.join(mnt_dir, name_generator()) + os.mkdir(filename) + fstat = os.lstat(filename) + uid = fstat.st_uid + gid = fstat.st_gid + + uid_new = uid + 1 + os.chown(filename, uid_new, -1) + fstat = os.lstat(filename) + assert fstat.st_uid == uid_new + assert fstat.st_gid == gid + + gid_new = gid + 1 + os.chown(filename, -1, gid_new) + fstat = os.lstat(filename) + assert fstat.st_uid == uid_new + assert fstat.st_gid == gid_new + + checked_unlink(filename, mnt_dir, isdir=True) + +def tst_write(mnt_dir): + name = os.path.join(mnt_dir, name_generator()) + shutil.copyfile(TEST_FILE, name) + assert filecmp.cmp(name, TEST_FILE, False) + checked_unlink(name, mnt_dir) + +def tst_unlink(mnt_dir): + name = os.path.join(mnt_dir, name_generator()) + data1 = b'foo' + data2 = b'bar' + + with open(os.path.join(mnt_dir, name), 'wb+', buffering=0) as fh: + fh.write(data1) + checked_unlink(name, mnt_dir) + fh.write(data2) + fh.seek(0) + assert fh.read() == data1+data2 + +def tst_statvfs(mnt_dir): + os.statvfs(mnt_dir) + +def tst_link(mnt_dir): + name1 = os.path.join(mnt_dir, name_generator()) + name2 = os.path.join(mnt_dir, name_generator()) + shutil.copyfile(TEST_FILE, name1) + assert filecmp.cmp(name1, TEST_FILE, False) + os.link(name1, name2) + + fstat1 = os.lstat(name1) + fstat2 = os.lstat(name2) + + assert fstat1 == fstat2 + assert fstat1.st_nlink == 2 + + assert os.path.basename(name2) in os.listdir(mnt_dir) + assert filecmp.cmp(name1, name2, False) + os.unlink(name2) + fstat1 = os.lstat(name1) + assert fstat1.st_nlink == 1 + os.unlink(name1) + +def tst_readdir(mnt_dir): + dir_ = os.path.join(mnt_dir, name_generator()) + file_ = dir_ + "/" + name_generator() + subdir = dir_ + "/" + name_generator() + subfile = subdir + "/" + name_generator() + + os.mkdir(dir_) + shutil.copyfile(TEST_FILE, file_) + os.mkdir(subdir) + shutil.copyfile(TEST_FILE, subfile) + + listdir_is = os.listdir(dir_) + listdir_is.sort() + listdir_should = [ os.path.basename(file_), os.path.basename(subdir) ] + listdir_should.sort() + assert listdir_is == listdir_should + + os.unlink(file_) + os.unlink(subfile) + os.rmdir(subdir) + os.rmdir(dir_) + +def tst_truncate_path(mnt_dir): + assert len(TEST_DATA) > 1024 + + filename = os.path.join(mnt_dir, name_generator()) + with open(filename, 'wb') as fh: + fh.write(TEST_DATA) + + fstat = os.stat(filename) + size = fstat.st_size + assert size == len(TEST_DATA) + + # Add zeros at the end + os.truncate(filename, size + 1024) + assert os.stat(filename).st_size == size + 1024 + with open(filename, 'rb') as fh: + assert fh.read(size) == TEST_DATA + assert fh.read(1025) == b'\0' * 1024 + + # Truncate data + os.truncate(filename, size - 1024) + assert os.stat(filename).st_size == size - 1024 + with open(filename, 'rb') as fh: + assert fh.read(size) == TEST_DATA[:size-1024] + + os.unlink(filename) + +def tst_truncate_fd(mnt_dir): + assert len(TEST_DATA) > 1024 + with NamedTemporaryFile('w+b', 0, dir=mnt_dir) as fh: + fd = fh.fileno() + fh.write(TEST_DATA) + fstat = os.fstat(fd) + size = fstat.st_size + assert size == len(TEST_DATA) + + # Add zeros at the end + os.ftruncate(fd, size + 1024) + assert os.fstat(fd).st_size == size + 1024 + fh.seek(0) + assert fh.read(size) == TEST_DATA + assert fh.read(1025) == b'\0' * 1024 + + # Truncate data + os.ftruncate(fd, size - 1024) + assert os.fstat(fd).st_size == size - 1024 + fh.seek(0) + assert fh.read(size) == TEST_DATA[:size-1024] + +def tst_utimens(mnt_dir, ns_tol=0): + filename = os.path.join(mnt_dir, name_generator()) + os.mkdir(filename) + fstat = os.lstat(filename) + + atime = fstat.st_atime + 42.28 + mtime = fstat.st_mtime - 42.23 + if sys.version_info < (3,3): + os.utime(filename, (atime, mtime)) + else: + atime_ns = fstat.st_atime_ns + int(42.28*1e9) + mtime_ns = fstat.st_mtime_ns - int(42.23*1e9) + os.utime(filename, None, ns=(atime_ns, mtime_ns)) + + fstat = os.lstat(filename) + + assert abs(fstat.st_atime - atime) < 1e-3 + assert abs(fstat.st_mtime - mtime) < 1e-3 + if sys.version_info >= (3,3): + assert abs(fstat.st_atime_ns - atime_ns) <= ns_tol + assert abs(fstat.st_mtime_ns - mtime_ns) <= ns_tol + + checked_unlink(filename, mnt_dir, isdir=True) + +def tst_passthrough(src_dir, mnt_dir): + name = name_generator() + src_name = os.path.join(src_dir, name) + mnt_name = os.path.join(src_dir, name) + assert name not in os.listdir(src_dir) + assert name not in os.listdir(mnt_dir) + with open(src_name, 'w') as fh: + fh.write('Hello, world') + assert name in os.listdir(src_dir) + assert name in os.listdir(mnt_dir) + assert os.stat(src_name) == os.stat(mnt_name) + + name = name_generator() + src_name = os.path.join(src_dir, name) + mnt_name = os.path.join(src_dir, name) + assert name not in os.listdir(src_dir) + assert name not in os.listdir(mnt_dir) + with open(mnt_name, 'w') as fh: + fh.write('Hello, world') + assert name in os.listdir(src_dir) + assert name in os.listdir(mnt_dir) + assert os.stat(src_name) == os.stat(mnt_name) diff --git a/test/test_fuse.py b/test/test_fuse.py new file mode 100755 index 0000000..bbba6e0 --- /dev/null +++ b/test/test_fuse.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +import pytest +import sys + +if __name__ == '__main__': + sys.exit(pytest.main([__file__] + sys.argv[1:])) + +import subprocess +import os +from util import wait_for_mount, umount, cleanup + +basename = os.path.join(os.path.dirname(__file__), '..') + +def test_fuse(tmpdir): + mnt_dir = str(tmpdir.mkdir('mnt')) + src_dir = str(tmpdir.mkdir('src')) + + cmdline = [ os.path.join(basename, 'example', 'fusexmp_fh'), + '-f', '-o' , 'use_ino,readdir_ino,kernel_cache', + mnt_dir ] + mount_process = subprocess.Popen(cmdline) + try: + wait_for_mount(mount_process, mnt_dir) + cmdline = [ os.path.join(basename, 'test', 'test'), + os.path.join(mnt_dir, src_dir), + ':' + src_dir ] + subprocess.check_call(cmdline) + except: + cleanup(mnt_dir) + raise + else: + umount(mount_process, mnt_dir) diff --git a/test/util.py b/test/util.py new file mode 100644 index 0000000..48ec995 --- /dev/null +++ b/test/util.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +import subprocess +import pytest +import os +import time + +def wait_for_mount(mount_process, mnt_dir): + elapsed = 0 + while elapsed < 30: + if os.path.ismount(mnt_dir): + return True + if mount_process.poll() is not None: + pytest.fail('file system process terminated prematurely') + time.sleep(0.1) + elapsed += 0.1 + pytest.fail("mountpoint failed to come up") + +def cleanup(mnt_dir): + subprocess.call(['fusermount', '-z', '-u', mnt_dir], + stdout=subprocess.DEVNULL, + stderr=subprocess.STDOUT) + +def umount(mount_process, mnt_dir): + subprocess.check_call(['fusermount', '-z', '-u', mnt_dir]) + assert not os.path.ismount(mnt_dir) + + # Give mount process a little while to terminate. Popen.wait(timeout) + # was only added in 3.3... + elapsed = 0 + while elapsed < 30: + code = mount_process.poll() + if code is not None: + if code == 0: + return + pytest.fail('file system process terminated with code %s' % (code,)) + time.sleep(0.1) + elapsed += 0.1 + pytest.fail('mount process did not terminate') |