]> git.sesse.net Git - bcachefs-tools-debian/blob - tests/util.py
Merge pull request #38 from jnsaff/patch-1
[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                '--log-file={}'.format(vout.name)]
53         cmds = vcmd + cmds
54
55     print("Running '{}'".format(cmds))
56     res = subprocess.run(cmds, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
57                          encoding='utf-8', check=check)
58
59     if valgrind:
60         check_valgrind(vout.read().decode('utf-8'))
61
62     return res
63
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)
68
69 def sparse_file(lpath, size):
70     """Construct a sparse file of the specified size.
71
72     This is typically used to create device files for bcachefs.
73     """
74     path = Path(lpath)
75     f = path.touch(mode = 0o600, exist_ok = False)
76     os.truncate(path, size)
77
78     return path
79
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)
84
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)
89     return dev
90
91 def mountpoint(tmpdir):
92     """Construct a mountpoint "mnt" for tests."""
93     path = Path(tmpdir) / 'mnt'
94     path.mkdir(mode = 0o700)
95     return path
96
97 class Timestamp:
98     '''Context manager to assist in verifying timestamps.
99
100     Records the range of times which would be valid for an encoded operation to
101     use.
102
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.
106
107     To make sure this doesn't happen, we sleep a fraction of a second in an
108     attempt to guarantee containment.
109
110     N.B. this might be better tested by overriding the clock used in bcachefs.
111
112     '''
113     def __init__(self):
114         self.start = None
115         self.end = None
116
117     def __enter__(self):
118         self.start = time.clock_gettime(time.CLOCK_REALTIME)
119         time.sleep(0.1)
120         return self
121
122     def __exit__(self, type, value, traceback):
123         time.sleep(0.1)
124         self.end = time.clock_gettime(time.CLOCK_REALTIME)
125
126     def contains(self, test):
127         '''True iff the test time is within the range.'''
128         return self.start <= test <= self.end
129
130 class FuseError(Exception):
131     def __init__(self, msg):
132         self.msg = msg
133
134 class BFuse:
135     '''bcachefs fuse runner.
136
137     This class runs bcachefs in fusemount mode, and waits until the mount has
138     reached a point suitable for testing the filesystem.
139
140     bcachefs is run under valgrind by default, and is checked for errors.
141     '''
142
143     def __init__(self, dev, mnt):
144         self.thread = None
145         self.dev = dev
146         self.mnt = mnt
147         self.ready = threading.Event()
148         self.proc = None
149         self.returncode = None
150         self.stdout = None
151         self.stderr = None
152         self.vout = None
153
154     def run(self):
155         """Background thread which runs "bcachefs fusemount" under valgrind"""
156
157         vlog = None
158         cmd = []
159
160         if ENABLE_VALGRIND:
161             vlog = tempfile.NamedTemporaryFile()
162             cmd += [ 'valgrind',
163                      '--leak-check=full',
164                      '--log-file={}'.format(vlog.name) ]
165
166         cmd += [ BCH_PATH,
167                  'fusemount', '-f', self.dev, self.mnt]
168
169         print("Running {}".format(cmd))
170
171         err = tempfile.TemporaryFile()
172         self.proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=err,
173                                      encoding='utf-8')
174
175         out1 = self.expect(self.proc.stdout, r'^Fuse mount initialized.$')
176         self.ready.set()
177
178         print("Waiting for process.")
179         (out2, _) = self.proc.communicate()
180         print("Process exited.")
181
182         self.stdout = out1 + out2
183         self.stderr = err.read()
184         self.returncode = self.proc.returncode
185         self.vout = vlog.read().decode('utf-8')
186
187     def expect(self, pipe, regex):
188         """Wait for the child process to mount."""
189
190         c = re.compile(regex)
191
192         out = ""
193         for line in pipe:
194             print('Expect line "{}"'.format(line.rstrip()))
195             out += line
196             if c.match(line):
197                 print("Matched.")
198                 return out
199
200         raise FuseError('stdout did not contain regex "{}"'.format(regex))
201
202     def mount(self):
203         print("Starting fuse thread.")
204
205         assert not self.thread
206         self.thread = threading.Thread(target=self.run)
207         self.thread.start()
208
209         self.ready.wait()
210         print("Fuse is mounted.")
211
212     def unmount(self, timeout=None):
213         print("Unmounting fuse.")
214         run("fusermount3", "-zu", self.mnt)
215
216         if self.thread:
217             print("Waiting for thread to exit.")
218             self.thread.join(timeout)
219             if self.thread.is_alive():
220                 self.proc.kill()
221                 self.thread.join()
222         else:
223             print("Thread was already done.")
224
225         self.thread = None
226         self.ready.clear()
227
228         if self.vout:
229             check_valgrind(self.vout)
230
231     def verify(self):
232         assert self.returncode == 0
233         assert len(self.stdout) > 0
234         assert len(self.stderr) == 0
235
236 def have_fuse():
237     res = run(BCH_PATH, 'fusemount', valgrind=False)
238     return "Please supply a mountpoint." in res.stdout