#!/usr/bin/python3
import os
+import pytest
import re
import subprocess
import tempfile
+import threading
+import time
+
from pathlib import Path
DIR = Path('..')
"""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