]> git.sesse.net Git - bcachefs-tools-debian/blob - tests/util.py
Disable pristine-tar option in gbp.conf, since there is no pristine-tar branch.
[bcachefs-tools-debian] / tests / util.py
1 #!/usr/bin/python3
2
3 import errno
4 import os
5 import re
6 import subprocess
7 import tempfile
8 import threading
9 import time
10
11 from pathlib import Path
12
13 BASE_PATH= os.path.dirname(__file__)
14 BCH_PATH = os.path.abspath(os.path.join(BASE_PATH, '../target/release', 'bcachefs'))
15 VALGRIND_PATH= os.path.abspath(os.path.join(BASE_PATH,
16     'valgrind-suppressions.txt'))
17
18 VPAT = re.compile(r'ERROR SUMMARY: (\d+) errors from (\d+) contexts')
19
20 ENABLE_VALGRIND = os.getenv('BCACHEFS_TEST_USE_VALGRIND', 'no') == 'yes'
21
22 class ValgrindFailedError(Exception):
23     def __init__(self, log):
24         self.log = log
25
26 def check_valgrind(log):
27     m = VPAT.search(log)
28     if m is None:
29         print('Internal error: valgrind log did not match.')
30         print('-- valgrind log:')
31         print(log)
32         print('-- end log --')
33         assert False
34
35     errors = int(m.group(1))
36     if errors > 0:
37         raise ValgrindFailedError(log)
38
39 def run(cmd, *args, valgrind=False, check=False):
40     """Run an external program via subprocess, optionally with valgrind.
41
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.
45     """
46     cmds = [cmd] + list(args)
47     valgrind = valgrind and ENABLE_VALGRIND
48
49     print("Running '{}'".format(cmds))
50     if valgrind:
51         vout = tempfile.NamedTemporaryFile()
52         vcmd = ['valgrind',
53                '--leak-check=full',
54                '--gen-suppressions=all',
55                '--suppressions={}'.format(VALGRIND_PATH),
56                '--log-file={}'.format(vout.name)]
57         cmds = vcmd + cmds
58
59         res = subprocess.run(cmds, stdout=subprocess.PIPE,
60                 stderr=subprocess.PIPE, encoding='utf-8', check=check)
61         check_valgrind(vout.read().decode('utf-8'))
62     else:
63         res = subprocess.run(cmds, stdout=subprocess.PIPE,
64                 stderr=subprocess.PIPE, encoding='utf-8', check=check)
65
66     return res
67
68 def run_bch(*args, **kwargs):
69     """Wrapper to run the bcachefs binary specifically."""
70     cmds = [BCH_PATH] + list(args)
71     return run(*cmds, **kwargs)
72
73 def sparse_file(lpath, size):
74     """Construct a sparse file of the specified size.
75
76     This is typically used to create device files for bcachefs.
77     """
78     path = Path(lpath)
79     path.touch(mode = 0o600, exist_ok = False)
80     os.truncate(path, size)
81
82     return path
83
84 def device_1g(tmpdir):
85     """Default 1g sparse file for use with bcachefs."""
86     path = tmpdir / 'dev-1g'
87     return sparse_file(path, 1024**3)
88
89 def format_1g(tmpdir):
90     """Format a default filesystem on a 1g device."""
91     dev = device_1g(tmpdir)
92     run_bch('format', dev, check=True)
93     return dev
94
95 def mountpoint(tmpdir):
96     """Construct a mountpoint "mnt" for tests."""
97     path = Path(tmpdir) / 'mnt'
98     path.mkdir(mode = 0o700)
99     return path
100
101 class Timestamp:
102     '''Context manager to assist in verifying timestamps.
103
104     Records the range of times which would be valid for an encoded operation to
105     use.
106
107     FIXME: The kernel code is currently using CLOCK_REALTIME_COARSE, but python
108     didn't expose this time API (yet).  Probably the kernel shouldn't be using
109     _COARSE anyway, but this might lead to occasional errors.
110
111     To make sure this doesn't happen, we sleep a fraction of a second in an
112     attempt to guarantee containment.
113
114     N.B. this might be better tested by overriding the clock used in bcachefs.
115
116     '''
117     def __init__(self):
118         self.start = None
119         self.end = None
120
121     def __enter__(self):
122         self.start = time.clock_gettime(time.CLOCK_REALTIME)
123         time.sleep(0.1)
124         return self
125
126     def __exit__(self, type, value, traceback):
127         time.sleep(0.1)
128         self.end = time.clock_gettime(time.CLOCK_REALTIME)
129
130     def contains(self, test):
131         '''True iff the test time is within the range.'''
132         return self.start <= test <= self.end
133
134 class FuseError(Exception):
135     def __init__(self, msg):
136         self.msg = msg
137
138 class BFuse:
139     '''bcachefs fuse runner.
140
141     This class runs bcachefs in fusemount mode, and waits until the mount has
142     reached a point suitable for testing the filesystem.
143
144     bcachefs is run under valgrind by default, and is checked for errors.
145     '''
146
147     def __init__(self, dev, mnt):
148         self.thread = None
149         self.dev = dev
150         self.mnt = mnt
151         self.ready = threading.Event()
152         self.proc = None
153         self.returncode = None
154         self.stdout = None
155         self.stderr = None
156         self.vout = None
157
158     def run(self):
159         """Background thread which runs "bcachefs fusemount" under valgrind"""
160
161         vlog = None
162         cmd = []
163
164         if ENABLE_VALGRIND:
165             vlog = tempfile.NamedTemporaryFile()
166             cmd += [ 'valgrind',
167                      '--leak-check=full',
168                      '--gen-suppressions=all',
169                      '--suppressions=valgrind-suppressions.txt',
170                      '--log-file={}'.format(vlog.name) ]
171
172         cmd += [ BCH_PATH,
173                  'fusemount', '-f', self.dev, self.mnt]
174
175         print("Running {}".format(cmd))
176
177         err = tempfile.TemporaryFile()
178         self.proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=err,
179                                      encoding='utf-8')
180
181         out1 = self.expect(self.proc.stdout, r'^Fuse mount initialized.$')
182         self.ready.set()
183
184         print("Waiting for process.")
185         (out2, _) = self.proc.communicate()
186         print("Process exited.")
187
188         self.returncode = self.proc.returncode
189         if self.returncode == 0:
190             errors = [ 'btree iterators leaked!',
191                        'emergency read only!' ]
192             for e in errors:
193                 if e in out2:
194                     print('Debug error found in output: "{}"'.format(e))
195                     self.returncode = errno.ENOMSG
196
197         self.stdout = out1 + out2
198         self.stderr = err.read()
199         if vlog:
200             self.vout = vlog.read().decode('utf-8')
201
202     def expect(self, pipe, regex):
203         """Wait for the child process to mount."""
204
205         c = re.compile(regex)
206
207         out = ""
208         for line in pipe:
209             print('Expect line "{}"'.format(line.rstrip()))
210             out += line
211             if c.match(line):
212                 print("Matched.")
213                 return out
214
215         raise FuseError('stdout did not contain regex "{}"'.format(regex))
216
217     def mount(self):
218         print("Starting fuse thread.")
219
220         assert not self.thread
221         self.thread = threading.Thread(target=self.run)
222         self.thread.start()
223
224         self.ready.wait()
225         print("Fuse is mounted.")
226
227     def unmount(self, timeout=None):
228         print("Unmounting fuse.")
229         run("fusermount3", "-zu", self.mnt)
230
231         if self.thread:
232             print("Waiting for thread to exit.")
233             self.thread.join(timeout)
234             if self.thread.is_alive():
235                 if self.proc:
236                     self.proc.kill()
237                 self.thread.join()
238         else:
239             print("Thread was already done.")
240
241         self.thread = None
242         self.ready.clear()
243
244         if self.vout:
245             check_valgrind(self.vout)
246
247     def verify(self):
248         # avoid throwing exception in assertion
249         assert self.stdout is not None
250         assert self.stderr is not None
251         assert self.returncode == 0
252         assert len(self.stdout) > 0
253         assert len(self.stderr) == 0
254
255 def have_fuse():
256     res = run(BCH_PATH, 'fusemount', valgrind=False)
257     return "Please supply a mountpoint." in res.stdout