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