13 from pathlib import Path
16 BCH_PATH = DIR / 'bcachefs'
18 VPAT = re.compile(r'ERROR SUMMARY: (\d+) errors from (\d+) contexts')
20 ENABLE_VALGRIND = os.getenv('BCACHEFS_TEST_USE_VALGRIND', 'no') == 'yes'
22 class ValgrindFailedError(Exception):
23 def __init__(self, log):
26 def check_valgrind(log):
29 print('Internal error: valgrind log did not match.')
30 print('-- valgrind log:')
32 print('-- end log --')
35 errors = int(m.group(1))
37 raise ValgrindFailedError(log)
39 def run(cmd, *args, valgrind=False, check=False):
40 """Run an external program via subprocess, optionally with valgrind.
42 This subprocess wrapper will capture the stdout and stderr. If valgrind is
43 requested, it will be checked for errors and raise a
44 ValgrindFailedError if there's a problem.
46 cmds = [cmd] + list(args)
47 valgrind = valgrind and ENABLE_VALGRIND
50 vout = tempfile.NamedTemporaryFile()
53 '--gen-suppressions=all',
54 '--suppressions=valgrind-suppressions.txt',
55 '--log-file={}'.format(vout.name)]
58 print("Running '{}'".format(cmds))
59 res = subprocess.run(cmds, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
60 encoding='utf-8', check=check)
63 check_valgrind(vout.read().decode('utf-8'))
67 def run_bch(*args, **kwargs):
68 """Wrapper to run the bcachefs binary specifically."""
69 cmds = [BCH_PATH] + list(args)
70 return run(*cmds, **kwargs)
72 def sparse_file(lpath, size):
73 """Construct a sparse file of the specified size.
75 This is typically used to create device files for bcachefs.
78 f = path.touch(mode = 0o600, exist_ok = False)
79 os.truncate(path, size)
83 def device_1g(tmpdir):
84 """Default 1g sparse file for use with bcachefs."""
85 path = tmpdir / 'dev-1g'
86 return sparse_file(path, 1024**3)
88 def format_1g(tmpdir):
89 """Format a default filesystem on a 1g device."""
90 dev = device_1g(tmpdir)
91 run_bch('format', dev, check=True)
94 def mountpoint(tmpdir):
95 """Construct a mountpoint "mnt" for tests."""
96 path = Path(tmpdir) / 'mnt'
97 path.mkdir(mode = 0o700)
101 '''Context manager to assist in verifying timestamps.
103 Records the range of times which would be valid for an encoded operation to
106 FIXME: The kernel code is currently using CLOCK_REALTIME_COARSE, but python
107 didn't expose this time API (yet). Probably the kernel shouldn't be using
108 _COARSE anyway, but this might lead to occasional errors.
110 To make sure this doesn't happen, we sleep a fraction of a second in an
111 attempt to guarantee containment.
113 N.B. this might be better tested by overriding the clock used in bcachefs.
121 self.start = time.clock_gettime(time.CLOCK_REALTIME)
125 def __exit__(self, type, value, traceback):
127 self.end = time.clock_gettime(time.CLOCK_REALTIME)
129 def contains(self, test):
130 '''True iff the test time is within the range.'''
131 return self.start <= test <= self.end
133 class FuseError(Exception):
134 def __init__(self, msg):
138 '''bcachefs fuse runner.
140 This class runs bcachefs in fusemount mode, and waits until the mount has
141 reached a point suitable for testing the filesystem.
143 bcachefs is run under valgrind by default, and is checked for errors.
146 def __init__(self, dev, mnt):
150 self.ready = threading.Event()
152 self.returncode = None
158 """Background thread which runs "bcachefs fusemount" under valgrind"""
164 vlog = tempfile.NamedTemporaryFile()
167 '--gen-suppressions=all',
168 '--suppressions=valgrind-suppressions.txt',
169 '--log-file={}'.format(vlog.name) ]
172 'fusemount', '-f', self.dev, self.mnt]
174 print("Running {}".format(cmd))
176 err = tempfile.TemporaryFile()
177 self.proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=err,
180 out1 = self.expect(self.proc.stdout, r'^Fuse mount initialized.$')
183 print("Waiting for process.")
184 (out2, _) = self.proc.communicate()
185 print("Process exited.")
187 self.returncode = self.proc.returncode
188 if self.returncode == 0:
189 errors = [ 'btree iterators leaked!',
190 'emergency read only!' ]
193 print('Debug error found in output: "{}"'.format(e))
194 self.returncode = errno.ENOMSG
196 self.stdout = out1 + out2
197 self.stderr = err.read()
198 self.vout = vlog.read().decode('utf-8')
200 def expect(self, pipe, regex):
201 """Wait for the child process to mount."""
203 c = re.compile(regex)
207 print('Expect line "{}"'.format(line.rstrip()))
213 raise FuseError('stdout did not contain regex "{}"'.format(regex))
216 print("Starting fuse thread.")
218 assert not self.thread
219 self.thread = threading.Thread(target=self.run)
223 print("Fuse is mounted.")
225 def unmount(self, timeout=None):
226 print("Unmounting fuse.")
227 run("fusermount3", "-zu", self.mnt)
230 print("Waiting for thread to exit.")
231 self.thread.join(timeout)
232 if self.thread.is_alive():
236 print("Thread was already done.")
242 check_valgrind(self.vout)
245 assert self.returncode == 0
246 assert len(self.stdout) > 0
247 assert len(self.stderr) == 0
250 res = run(BCH_PATH, 'fusemount', valgrind=False)
251 return "Please supply a mountpoint." in res.stdout