12 from pathlib import Path
15 BCH_PATH = DIR / 'bcachefs'
17 VPAT = re.compile(r'ERROR SUMMARY: (\d+) errors from (\d+) contexts')
19 ENABLE_VALGRIND = os.getenv('BCACHEFS_TEST_USE_VALGRIND', 'no') == 'yes'
21 class ValgrindFailedError(Exception):
22 def __init__(self, log):
25 def check_valgrind(logfile):
26 log = logfile.read().decode('utf-8')
28 assert m is not None, 'Internal error: valgrind log did not match.'
30 errors = int(m.group(1))
32 raise ValgrindFailedError(log)
34 def run(cmd, *args, valgrind=False, check=False):
35 """Run an external program via subprocess, optionally with valgrind.
37 This subprocess wrapper will capture the stdout and stderr. If valgrind is
38 requested, it will be checked for errors and raise a
39 ValgrindFailedError if there's a problem.
41 cmds = [cmd] + list(args)
42 valgrind = valgrind and ENABLE_VALGRIND
45 vout = tempfile.NamedTemporaryFile()
48 '--log-file={}'.format(vout.name)]
51 print("Running '{}'".format(cmds))
52 res = subprocess.run(cmds, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
53 encoding='utf-8', check=check)
60 def run_bch(*args, **kwargs):
61 """Wrapper to run the bcachefs binary specifically."""
62 cmds = [BCH_PATH] + list(args)
63 return run(*cmds, **kwargs)
65 def sparse_file(lpath, size):
66 """Construct a sparse file of the specified size.
68 This is typically used to create device files for bcachefs.
71 f = path.touch(mode = 0o600, exist_ok = False)
72 os.truncate(path, size)
76 def device_1g(tmpdir):
77 """Default 1g sparse file for use with bcachefs."""
78 path = tmpdir / 'dev-1g'
79 return sparse_file(path, 1024**3)
81 def format_1g(tmpdir):
82 """Format a default filesystem on a 1g device."""
83 dev = device_1g(tmpdir)
84 run_bch('format', dev, check=True)
87 def mountpoint(tmpdir):
88 """Construct a mountpoint "mnt" for tests."""
89 path = Path(tmpdir) / 'mnt'
90 path.mkdir(mode = 0o700)
94 '''Context manager to assist in verifying timestamps.
96 Records the range of times which would be valid for an encoded operation to
99 FIXME: The kernel code is currently using CLOCK_REALTIME_COARSE, but python
100 didn't expose this time API (yet). Probably the kernel shouldn't be using
101 _COARSE anyway, but this might lead to occasional errors.
103 To make sure this doesn't happen, we sleep a fraction of a second in an
104 attempt to guarantee containment.
106 N.B. this might be better tested by overriding the clock used in bcachefs.
114 self.start = time.clock_gettime(time.CLOCK_REALTIME)
118 def __exit__(self, type, value, traceback):
120 self.end = time.clock_gettime(time.CLOCK_REALTIME)
122 def contains(self, test):
123 '''True iff the test time is within the range.'''
124 return self.start <= test <= self.end
126 class FuseError(Exception):
127 def __init__(self, msg):
131 '''bcachefs fuse runner.
133 This class runs bcachefs in fusemount mode, and waits until the mount has
134 reached a point suitable for testing the filesystem.
136 bcachefs is run under valgrind by default, and is checked for errors.
139 def __init__(self, dev, mnt):
143 self.ready = threading.Event()
145 self.returncode = None
151 """Background thread which runs "bcachefs fusemount" under valgrind"""
157 vout = tempfile.NamedTemporaryFile()
160 '--log-file={}'.format(vout.name) ]
163 'fusemount', '-f', self.dev, self.mnt]
165 print("Running {}".format(cmd))
167 err = tempfile.TemporaryFile()
168 self.proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=err,
171 out1 = self.expect(self.proc.stdout, r'^Fuse mount initialized.$')
174 print("Waiting for process.")
175 (out2, _) = self.proc.communicate()
176 print("Process exited.")
178 self.stdout = out1 + out2
179 self.stderr = err.read()
180 self.returncode = self.proc.returncode
183 def expect(self, pipe, regex):
184 """Wait for the child process to mount."""
186 c = re.compile(regex)
190 print('Expect line "{}"'.format(line.rstrip()))
196 raise FuseError('stdout did not contain regex "{}"'.format(regex))
199 print("Starting fuse thread.")
201 assert not self.thread
202 self.thread = threading.Thread(target=self.run)
206 print("Fuse is mounted.")
208 def unmount(self, timeout=None):
209 print("Unmounting fuse.")
210 run("fusermount3", "-zu", self.mnt)
211 print("Waiting for thread to exit.")
214 self.thread.join(timeout)
215 if self.thread.is_alive():
223 check_valgrind(self.vout)
226 assert self.returncode == 0
227 assert len(self.stdout) > 0
228 assert len(self.stderr) == 0
231 res = run(BCH_PATH, 'fusemount', valgrind=False)
232 return "Please supply a mountpoint." in res.stdout