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 '--gen-suppressions=all',
53 '--suppressions=valgrind-suppressions.txt',
54 '--log-file={}'.format(vout.name)]
57 print("Running '{}'".format(cmds))
58 res = subprocess.run(cmds, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
59 encoding='utf-8', check=check)
62 check_valgrind(vout.read().decode('utf-8'))
66 def run_bch(*args, **kwargs):
67 """Wrapper to run the bcachefs binary specifically."""
68 cmds = [BCH_PATH] + list(args)
69 return run(*cmds, **kwargs)
71 def sparse_file(lpath, size):
72 """Construct a sparse file of the specified size.
74 This is typically used to create device files for bcachefs.
77 f = path.touch(mode = 0o600, exist_ok = False)
78 os.truncate(path, size)
82 def device_1g(tmpdir):
83 """Default 1g sparse file for use with bcachefs."""
84 path = tmpdir / 'dev-1g'
85 return sparse_file(path, 1024**3)
87 def format_1g(tmpdir):
88 """Format a default filesystem on a 1g device."""
89 dev = device_1g(tmpdir)
90 run_bch('format', dev, check=True)
93 def mountpoint(tmpdir):
94 """Construct a mountpoint "mnt" for tests."""
95 path = Path(tmpdir) / 'mnt'
96 path.mkdir(mode = 0o700)
100 '''Context manager to assist in verifying timestamps.
102 Records the range of times which would be valid for an encoded operation to
105 FIXME: The kernel code is currently using CLOCK_REALTIME_COARSE, but python
106 didn't expose this time API (yet). Probably the kernel shouldn't be using
107 _COARSE anyway, but this might lead to occasional errors.
109 To make sure this doesn't happen, we sleep a fraction of a second in an
110 attempt to guarantee containment.
112 N.B. this might be better tested by overriding the clock used in bcachefs.
120 self.start = time.clock_gettime(time.CLOCK_REALTIME)
124 def __exit__(self, type, value, traceback):
126 self.end = time.clock_gettime(time.CLOCK_REALTIME)
128 def contains(self, test):
129 '''True iff the test time is within the range.'''
130 return self.start <= test <= self.end
132 class FuseError(Exception):
133 def __init__(self, msg):
137 '''bcachefs fuse runner.
139 This class runs bcachefs in fusemount mode, and waits until the mount has
140 reached a point suitable for testing the filesystem.
142 bcachefs is run under valgrind by default, and is checked for errors.
145 def __init__(self, dev, mnt):
149 self.ready = threading.Event()
151 self.returncode = None
157 """Background thread which runs "bcachefs fusemount" under valgrind"""
163 vlog = tempfile.NamedTemporaryFile()
166 '--gen-suppressions=all',
167 '--suppressions=valgrind-suppressions.txt',
168 '--log-file={}'.format(vlog.name) ]
171 'fusemount', '-f', self.dev, self.mnt]
173 print("Running {}".format(cmd))
175 err = tempfile.TemporaryFile()
176 self.proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=err,
179 out1 = self.expect(self.proc.stdout, r'^Fuse mount initialized.$')
182 print("Waiting for process.")
183 (out2, _) = self.proc.communicate()
184 print("Process exited.")
186 self.stdout = out1 + out2
187 self.stderr = err.read()
188 self.returncode = self.proc.returncode
189 self.vout = vlog.read().decode('utf-8')
191 def expect(self, pipe, regex):
192 """Wait for the child process to mount."""
194 c = re.compile(regex)
198 print('Expect line "{}"'.format(line.rstrip()))
204 raise FuseError('stdout did not contain regex "{}"'.format(regex))
207 print("Starting fuse thread.")
209 assert not self.thread
210 self.thread = threading.Thread(target=self.run)
214 print("Fuse is mounted.")
216 def unmount(self, timeout=None):
217 print("Unmounting fuse.")
218 run("fusermount3", "-zu", self.mnt)
221 print("Waiting for thread to exit.")
222 self.thread.join(timeout)
223 if self.thread.is_alive():
227 print("Thread was already done.")
233 check_valgrind(self.vout)
236 assert self.returncode == 0
237 assert len(self.stdout) > 0
238 assert len(self.stderr) == 0
241 res = run(BCH_PATH, 'fusemount', valgrind=False)
242 return "Please supply a mountpoint." in res.stdout