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(log):
28 print('Internal error: valgrind log did not match.')
29 print('-- valgrind log:')
31 print('-- end log --')
34 errors = int(m.group(1))
36 raise ValgrindFailedError(log)
38 def run(cmd, *args, valgrind=False, check=False):
39 """Run an external program via subprocess, optionally with valgrind.
41 This subprocess wrapper will capture the stdout and stderr. If valgrind is
42 requested, it will be checked for errors and raise a
43 ValgrindFailedError if there's a problem.
45 cmds = [cmd] + list(args)
46 valgrind = valgrind and ENABLE_VALGRIND
49 vout = tempfile.NamedTemporaryFile()
52 '--log-file={}'.format(vout.name)]
55 print("Running '{}'".format(cmds))
56 res = subprocess.run(cmds, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
57 encoding='utf-8', check=check)
60 check_valgrind(vout.read().decode('utf-8'))
64 def run_bch(*args, **kwargs):
65 """Wrapper to run the bcachefs binary specifically."""
66 cmds = [BCH_PATH] + list(args)
67 return run(*cmds, **kwargs)
69 def sparse_file(lpath, size):
70 """Construct a sparse file of the specified size.
72 This is typically used to create device files for bcachefs.
75 f = path.touch(mode = 0o600, exist_ok = False)
76 os.truncate(path, size)
80 def device_1g(tmpdir):
81 """Default 1g sparse file for use with bcachefs."""
82 path = tmpdir / 'dev-1g'
83 return sparse_file(path, 1024**3)
85 def format_1g(tmpdir):
86 """Format a default filesystem on a 1g device."""
87 dev = device_1g(tmpdir)
88 run_bch('format', dev, check=True)
91 def mountpoint(tmpdir):
92 """Construct a mountpoint "mnt" for tests."""
93 path = Path(tmpdir) / 'mnt'
94 path.mkdir(mode = 0o700)
98 '''Context manager to assist in verifying timestamps.
100 Records the range of times which would be valid for an encoded operation to
103 FIXME: The kernel code is currently using CLOCK_REALTIME_COARSE, but python
104 didn't expose this time API (yet). Probably the kernel shouldn't be using
105 _COARSE anyway, but this might lead to occasional errors.
107 To make sure this doesn't happen, we sleep a fraction of a second in an
108 attempt to guarantee containment.
110 N.B. this might be better tested by overriding the clock used in bcachefs.
118 self.start = time.clock_gettime(time.CLOCK_REALTIME)
122 def __exit__(self, type, value, traceback):
124 self.end = time.clock_gettime(time.CLOCK_REALTIME)
126 def contains(self, test):
127 '''True iff the test time is within the range.'''
128 return self.start <= test <= self.end
130 class FuseError(Exception):
131 def __init__(self, msg):
135 '''bcachefs fuse runner.
137 This class runs bcachefs in fusemount mode, and waits until the mount has
138 reached a point suitable for testing the filesystem.
140 bcachefs is run under valgrind by default, and is checked for errors.
143 def __init__(self, dev, mnt):
147 self.ready = threading.Event()
149 self.returncode = None
155 """Background thread which runs "bcachefs fusemount" under valgrind"""
161 vlog = tempfile.NamedTemporaryFile()
164 '--log-file={}'.format(vlog.name) ]
167 'fusemount', '-f', self.dev, self.mnt]
169 print("Running {}".format(cmd))
171 err = tempfile.TemporaryFile()
172 self.proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=err,
175 out1 = self.expect(self.proc.stdout, r'^Fuse mount initialized.$')
178 print("Waiting for process.")
179 (out2, _) = self.proc.communicate()
180 print("Process exited.")
182 self.stdout = out1 + out2
183 self.stderr = err.read()
184 self.returncode = self.proc.returncode
185 self.vout = vlog.read().decode('utf-8')
187 def expect(self, pipe, regex):
188 """Wait for the child process to mount."""
190 c = re.compile(regex)
194 print('Expect line "{}"'.format(line.rstrip()))
200 raise FuseError('stdout did not contain regex "{}"'.format(regex))
203 print("Starting fuse thread.")
205 assert not self.thread
206 self.thread = threading.Thread(target=self.run)
210 print("Fuse is mounted.")
212 def unmount(self, timeout=None):
213 print("Unmounting fuse.")
214 run("fusermount3", "-zu", self.mnt)
217 print("Waiting for thread to exit.")
218 self.thread.join(timeout)
219 if self.thread.is_alive():
223 print("Thread was already done.")
229 check_valgrind(self.vout)
232 assert self.returncode == 0
233 assert len(self.stdout) > 0
234 assert len(self.stderr) == 0
237 res = run(BCH_PATH, 'fusemount', valgrind=False)
238 return "Please supply a mountpoint." in res.stdout