diff options
-rw-r--r-- | cmd_fusemount.c | 3 | ||||
-rw-r--r-- | tests/conftest.py | 19 | ||||
-rw-r--r-- | tests/test_basic.py | 12 | ||||
-rw-r--r-- | tests/test_fixture.py | 16 | ||||
-rw-r--r-- | tests/test_fuse.py | 221 | ||||
-rw-r--r-- | tests/util.py | 139 |
6 files changed, 402 insertions, 8 deletions
diff --git a/cmd_fusemount.c b/cmd_fusemount.c index de03ca18..96a2339d 100644 --- a/cmd_fusemount.c +++ b/cmd_fusemount.c @@ -1243,6 +1243,9 @@ int cmd_fusemount(int argc, char *argv[]) if (fuse_session_mount(se, fuse_opts.mountpoint)) die("fuse_mount err: %m"); + /* This print statement is a trigger for tests. */ + printf("Fuse mount initialized.\n"); + fuse_daemonize(fuse_opts.foreground); ret = fuse_session_loop(se); diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..c656eda0 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,19 @@ +#!/usr/bin/python3 +# +# pytest fixture definitions. + +import pytest +import util + +@pytest.fixture +def bfuse(tmpdir): + '''A test requesting a "bfuse" is given one via this fixture.''' + + dev = util.format_1g(tmpdir) + mnt = util.mountpoint(tmpdir) + bf = util.BFuse(dev, mnt) + + yield bf + + if bf.returncode is None: + bf.unmount(timeout=5.0) diff --git a/tests/test_basic.py b/tests/test_basic.py index 9cd7b2f8..f219f07e 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -21,8 +21,7 @@ def test_format(tmpdir): assert len(ret.stderr) == 0 def test_fsck(tmpdir): - dev = util.device_1g(tmpdir) - util.run_bch('format', dev, valgrind=False, check=True) + dev = util.format_1g(tmpdir) ret = util.run_bch('fsck', dev, valgrind=True) @@ -31,8 +30,7 @@ def test_fsck(tmpdir): assert len(ret.stderr) == 0 def test_list(tmpdir): - dev = util.device_1g(tmpdir) - util.run_bch('format', dev, valgrind=False, check=True) + dev = util.format_1g(tmpdir) ret = util.run_bch('list', dev, valgrind=True) @@ -42,8 +40,7 @@ def test_list(tmpdir): assert len(ret.stdout.splitlines()) == 2 def test_list_inodes(tmpdir): - dev = util.device_1g(tmpdir) - util.run_bch('format', dev, valgrind=False, check=True) + dev = util.format_1g(tmpdir) ret = util.run_bch('list', '-b', 'inodes', dev, valgrind=True) @@ -52,8 +49,7 @@ def test_list_inodes(tmpdir): assert len(ret.stdout.splitlines()) == (2 + 2) # 2 inodes on clean format def test_list_dirent(tmpdir): - dev = util.device_1g(tmpdir) - util.run_bch('format', dev, valgrind=False, check=True) + dev = util.format_1g(tmpdir) ret = util.run_bch('list', '-b', 'dirents', dev, valgrind=True) diff --git a/tests/test_fixture.py b/tests/test_fixture.py index 8dfb392f..74a896ba 100644 --- a/tests/test_fixture.py +++ b/tests/test_fixture.py @@ -5,6 +5,7 @@ import pytest import signal import subprocess +import time import util from pathlib import Path @@ -51,3 +52,18 @@ def test_read_after_free(): def test_write_after_free(): with pytest.raises(util.ValgrindFailedError): ret = util.run(helper, 'write_after_free', valgrind=True) + +def test_mountpoint(tmpdir): + path = util.mountpoint(tmpdir) + assert str(path)[-4:] == '/mnt' + assert path.is_dir() + +def test_timestamp(): + t1 = time.clock_gettime(time.CLOCK_REALTIME) + with util.Timestamp() as ts: + t2 = time.clock_gettime(time.CLOCK_REALTIME) + t3 = time.clock_gettime(time.CLOCK_REALTIME) + + assert not ts.contains(t1) + assert ts.contains(t2) + assert not ts.contains(t3) diff --git a/tests/test_fuse.py b/tests/test_fuse.py new file mode 100644 index 00000000..877fd64c --- /dev/null +++ b/tests/test_fuse.py @@ -0,0 +1,221 @@ +#!/usr/bin/python3 +# +# Tests of the fuse mount functionality. + +import os +import util + +def test_mount(bfuse): + bfuse.mount() + bfuse.unmount() + bfuse.verify() + +def test_lostfound(bfuse): + bfuse.mount() + + lf = bfuse.mnt / "lost+found" + assert lf.is_dir() + + st = lf.stat() + assert st.st_mode == 0o40700 + + bfuse.unmount() + bfuse.verify() + +def test_create(bfuse): + bfuse.mount() + + path = bfuse.mnt / "file" + + with util.Timestamp() as ts: + fd = os.open(path, os.O_CREAT, 0o700) + + assert fd >= 0 + + os.close(fd) + assert path.is_file() + + # Verify file. + st = path.stat() + assert st.st_mode == 0o100700 + assert st.st_mtime == st.st_ctime + assert st.st_mtime == st.st_atime + assert ts.contains(st.st_mtime) + + # Verify dir. + dst = bfuse.mnt.stat() + assert dst.st_mtime == dst.st_ctime + assert ts.contains(dst.st_mtime) + + bfuse.unmount() + bfuse.verify() + +def test_mkdir(bfuse): + bfuse.mount() + + path = bfuse.mnt / "dir" + + with util.Timestamp() as ts: + os.mkdir(path, 0o700) + + assert path.is_dir() + + # Verify child. + st = path.stat() + assert st.st_mode == 0o40700 + assert st.st_mtime == st.st_ctime + assert st.st_mtime == st.st_atime + assert ts.contains(st.st_mtime) + + # Verify parent. + dst = bfuse.mnt.stat() + assert dst.st_mtime == dst.st_ctime + assert ts.contains(dst.st_mtime) + + bfuse.unmount() + bfuse.verify() + +def test_unlink(bfuse): + bfuse.mount() + + path = bfuse.mnt / "file" + path.touch(mode=0o600, exist_ok=False) + + with util.Timestamp() as ts: + os.unlink(path) + + assert not path.exists() + + # Verify dir. + dst = bfuse.mnt.stat() + assert dst.st_mtime == dst.st_ctime + assert ts.contains(dst.st_mtime) + + bfuse.unmount() + bfuse.verify() + +def test_rmdir(bfuse): + bfuse.mount() + + path = bfuse.mnt / "dir" + path.mkdir(mode=0o700, exist_ok=False) + + with util.Timestamp() as ts: + os.rmdir(path) + + assert not path.exists() + + # Verify dir. + dst = bfuse.mnt.stat() + assert dst.st_mtime == dst.st_ctime + assert ts.contains(dst.st_mtime) + + bfuse.unmount() + bfuse.verify() + +def test_rename(bfuse): + bfuse.mount() + + srcdir = bfuse.mnt + + path = srcdir / "file" + path.touch(mode=0o600, exist_ok=False) + + destdir = srcdir / "dir" + destdir.mkdir(mode=0o700, exist_ok=False) + + destpath = destdir / "file" + + path_pre_st = path.stat() + + with util.Timestamp() as ts: + os.rename(path, destpath) + + assert not path.exists() + assert destpath.is_file() + + # Verify dirs. + src_st = srcdir.stat() + assert src_st.st_mtime == src_st.st_ctime + assert ts.contains(src_st.st_mtime) + + dest_st = destdir.stat() + assert dest_st.st_mtime == dest_st.st_ctime + assert ts.contains(dest_st.st_mtime) + + # Verify file. + path_post_st = destpath.stat() + assert path_post_st.st_mtime == path_pre_st.st_mtime + assert path_post_st.st_atime == path_pre_st.st_atime + assert ts.contains(path_post_st.st_ctime) + + bfuse.unmount() + bfuse.verify() + +def test_link(bfuse): + bfuse.mount() + + srcdir = bfuse.mnt + + path = srcdir / "file" + path.touch(mode=0o600, exist_ok=False) + + destdir = srcdir / "dir" + destdir.mkdir(mode=0o700, exist_ok=False) + + destpath = destdir / "file" + + path_pre_st = path.stat() + srcdir_pre_st = srcdir.stat() + + with util.Timestamp() as ts: + os.link(path, destpath) + + assert path.exists() + assert destpath.is_file() + + # Verify source dir is unchanged. + srcdir_post_st = srcdir.stat() + assert srcdir_pre_st == srcdir_post_st + + # Verify dest dir. + destdir_st = destdir.stat() + assert destdir_st.st_mtime == destdir_st.st_ctime + assert ts.contains(destdir_st.st_mtime) + + # Verify file. + path_post_st = path.stat() + destpath_post_st = destpath.stat() + assert path_post_st == destpath_post_st + + assert path_post_st.st_mtime == path_pre_st.st_mtime + assert path_post_st.st_atime == path_pre_st.st_atime + assert ts.contains(path_post_st.st_ctime) + + bfuse.unmount() + bfuse.verify() + +def test_write(bfuse): + bfuse.mount() + + path = bfuse.mnt / "file" + path.touch(mode=0o600, exist_ok=False) + + pre_st = path.stat() + + fd = os.open(path, os.O_WRONLY) + assert fd >= 0 + + with util.Timestamp() as ts: + written = os.write(fd, b'test') + + os.close(fd) + + assert written == 4 + + post_st = path.stat() + assert post_st.st_atime == pre_st.st_atime + assert post_st.st_mtime == post_st.st_ctime + assert ts.contains(post_st.st_mtime) + + assert path.read_bytes() == b'test' diff --git a/tests/util.py b/tests/util.py index 6eea103d..18d60020 100644 --- a/tests/util.py +++ b/tests/util.py @@ -1,9 +1,13 @@ #!/usr/bin/python3 import os +import pytest import re import subprocess import tempfile +import threading +import time + from pathlib import Path DIR = Path('..') @@ -69,3 +73,138 @@ def device_1g(tmpdir): """Default 1g sparse file for use with bcachefs.""" path = tmpdir / 'dev-1g' return sparse_file(path, 1024**3) + +def format_1g(tmpdir): + """Format a default filesystem on a 1g device.""" + dev = device_1g(tmpdir) + run_bch('format', dev, check=True) + return dev + +def mountpoint(tmpdir): + """Construct a mountpoint "mnt" for tests.""" + path = Path(tmpdir) / 'mnt' + path.mkdir(mode = 0o700) + return path + +class Timestamp: + '''Context manager to assist in verifying timestamps. + + Records the range of times which would be valid for an encoded operation to + use. + + FIXME: The kernel code is currently using CLOCK_REALTIME_COARSE, but python + didn't expose this time API (yet). Probably the kernel shouldn't be using + _COARSE anyway, but this might lead to occasional errors. + + To make sure this doesn't happen, we sleep a fraction of a second in an + attempt to guarantee containment. + + N.B. this might be better tested by overriding the clock used in bcachefs. + + ''' + def __init__(self): + self.start = None + self.end = None + + def __enter__(self): + self.start = time.clock_gettime(time.CLOCK_REALTIME) + time.sleep(0.1) + return self + + def __exit__(self, type, value, traceback): + time.sleep(0.1) + self.end = time.clock_gettime(time.CLOCK_REALTIME) + + def contains(self, test): + '''True iff the test time is within the range.''' + return self.start <= test <= self.end + +class FuseError(Exception): + def __init__(self, msg): + self.msg = msg + +class BFuse(threading.Thread): + '''bcachefs fuse runner. + + This class runs bcachefs in fusemount mode, and waits until the mount has + reached a point suitable for testing the filesystem. + + bcachefs is run under valgrind by default, and is checked for errors. + ''' + + def __init__(self, dev, mnt): + threading.Thread.__init__(self) + self.dev = dev + self.mnt = mnt + self.ready = threading.Event() + self.proc = None + self.returncode = None + self.stdout = None + self.stderr = None + self.vout = None + + def run(self): + """Background thread which runs "bcachefs fusemount" under valgrind""" + + vout = tempfile.NamedTemporaryFile() + cmd = [ 'valgrind', + '--leak-check=full', + '--log-file={}'.format(vout.name), + BCH_PATH, + 'fusemount', '-f', self.dev, self.mnt] + + print("Running {}".format(cmd)) + + err = tempfile.TemporaryFile() + self.proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=err, + encoding='utf-8') + + out1 = self.expect(self.proc.stdout, r'^Fuse mount initialized.$') + self.ready.set() + + print("Waiting for process.") + (out2, _) = self.proc.communicate() + print("Process exited.") + + self.stdout = out1 + out2 + self.stderr = err.read() + self.returncode = self.proc.returncode + self.vout = vout + + def expect(self, pipe, regex): + """Wait for the child process to mount.""" + + c = re.compile(regex) + + out = "" + for line in pipe: + print('Expect line "{}"'.format(line.rstrip())) + out += line + if c.match(line): + print("Matched.") + return out + + raise FuseError('stdout did not contain regex "{}"'.format(regex)) + + def mount(self): + print("Starting fuse thread.") + self.start() + self.ready.wait() + print("Fuse is mounted.") + + def unmount(self, timeout=None): + print("Unmounting fuse.") + run("fusermount3", "-zu", self.mnt) + print("Waiting for thread to exit.") + + self.join(timeout) + if self.isAlive(): + self.proc.kill() + self.join() + + check_valgrind(self.vout) + + def verify(self): + assert self.returncode == 0 + assert len(self.stdout) > 0 + assert len(self.stderr) == 0 |