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