diff options
Diffstat (limited to 'tests/util.py')
-rw-r--r-- | tests/util.py | 139 |
1 files changed, 139 insertions, 0 deletions
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 |