t/fiotestlib: use 'with' for opening files
[fio.git] / t / fiotestlib.py
CommitLineData
fb551941
VF
1#!/usr/bin/env python3
2"""
3fiotestlib.py
4
5This library contains FioTest objects that provide convenient means to run
6different sorts of fio tests.
7
8It also contains a test runner that runs an array of dictionary objects
9describing fio tests.
10"""
11
12import os
13import sys
14import json
15import locale
16import logging
17import platform
18import traceback
19import subprocess
20from pathlib import Path
ecd6d30f 21from fiotestcommon import get_file
fb551941
VF
22
23
24class FioTest():
25 """Base for all fio tests."""
26
68b3a741
VF
27 def __init__(self, exe_path, success):
28 self.paths = {'exe': exe_path}
fb551941
VF
29 self.success = success
30 self.output = {}
fb551941 31 self.testnum = None
fb551941
VF
32 self.passed = True
33 self.failure_reason = ''
68b3a741
VF
34 self.filenames = {}
35 self.parameters = None
fb551941 36
68b3a741 37 def setup(self, artifact_root, testnum, parameters):
fb551941
VF
38 """Setup instance variables for test."""
39
fb551941 40 self.testnum = testnum
68b3a741
VF
41 self.parameters = parameters
42 self.paths['artifacts'] = artifact_root
43 self.paths['test_dir'] = os.path.join(artifact_root, f"{testnum:04d}")
44 if not os.path.exists(self.paths['test_dir']):
45 os.mkdir(self.paths['test_dir'])
46
47 self.filenames['cmd'] = os.path.join(self.paths['test_dir'],
48 f"{os.path.basename(self.paths['exe'])}.command")
49 self.filenames['stdout'] = os.path.join(self.paths['test_dir'],
50 f"{os.path.basename(self.paths['exe'])}.stdout")
51 self.filenames['stderr'] = os.path.join(self.paths['test_dir'],
52 f"{os.path.basename(self.paths['exe'])}.stderr")
53 self.filenames['exitcode'] = os.path.join(self.paths['test_dir'],
54 f"{os.path.basename(self.paths['exe'])}.exitcode")
fb551941
VF
55
56 def run(self):
57 """Run the test."""
58
59 raise NotImplementedError()
60
61 def check_result(self):
62 """Check test results."""
63
64 raise NotImplementedError()
65
66
67class FioExeTest(FioTest):
68 """Test consists of an executable binary or script"""
69
68b3a741 70 def __init__(self, exe_path, success):
fb551941
VF
71 """Construct a FioExeTest which is a FioTest consisting of an
72 executable binary or script.
73
74 exe_path: location of executable binary or script
fb551941
VF
75 success: Definition of test success
76 """
77
68b3a741 78 FioTest.__init__(self, exe_path, success)
fb551941
VF
79
80 def run(self):
81 """Execute the binary or script described by this instance."""
82
68b3a741 83 command = [self.paths['exe']] + self.parameters
1cf0ba9d
VF
84 with open(self.filenames['cmd'], "w+",
85 encoding=locale.getpreferredencoding()) as command_file:
86 command_file.write(f"{command}\n")
87
fb551941 88 try:
1cf0ba9d
VF
89 with open(self.filenames['stdout'], "w+",
90 encoding=locale.getpreferredencoding()) as stdout_file, \
91 open(self.filenames['stderr'], "w+",
92 encoding=locale.getpreferredencoding()) as stderr_file, \
93 open(self.filenames['exitcode'], "w+",
94 encoding=locale.getpreferredencoding()) as exitcode_file:
95 proc = None
96 # Avoid using subprocess.run() here because when a timeout occurs,
97 # fio will be stopped with SIGKILL. This does not give fio a
98 # chance to clean up and means that child processes may continue
99 # running and submitting IO.
100 proc = subprocess.Popen(command,
101 stdout=stdout_file,
102 stderr=stderr_file,
103 cwd=self.paths['test_dir'],
104 universal_newlines=True)
105 proc.communicate(timeout=self.success['timeout'])
106 exitcode_file.write(f'{proc.returncode}\n')
107 logging.debug("Test %d: return code: %d", self.testnum, proc.returncode)
108 self.output['proc'] = proc
fb551941
VF
109 except subprocess.TimeoutExpired:
110 proc.terminate()
111 proc.communicate()
112 assert proc.poll()
113 self.output['failure'] = 'timeout'
114 except Exception:
115 if proc:
116 if not proc.poll():
117 proc.terminate()
118 proc.communicate()
119 self.output['failure'] = 'exception'
120 self.output['exc_info'] = sys.exc_info()
fb551941
VF
121
122 def check_result(self):
123 """Check results of test run."""
124
125 if 'proc' not in self.output:
126 if self.output['failure'] == 'timeout':
127 self.failure_reason = f"{self.failure_reason} timeout,"
128 else:
129 assert self.output['failure'] == 'exception'
130 self.failure_reason = '{0} exception: {1}, {2}'.format(
131 self.failure_reason, self.output['exc_info'][0],
132 self.output['exc_info'][1])
133
134 self.passed = False
135 return
136
137 if 'zero_return' in self.success:
138 if self.success['zero_return']:
139 if self.output['proc'].returncode != 0:
140 self.passed = False
141 self.failure_reason = f"{self.failure_reason} non-zero return code,"
142 else:
143 if self.output['proc'].returncode == 0:
144 self.failure_reason = f"{self.failure_reason} zero return code,"
145 self.passed = False
146
68b3a741 147 stderr_size = os.path.getsize(self.filenames['stderr'])
fb551941
VF
148 if 'stderr_empty' in self.success:
149 if self.success['stderr_empty']:
150 if stderr_size != 0:
151 self.failure_reason = f"{self.failure_reason} stderr not empty,"
152 self.passed = False
153 else:
154 if stderr_size == 0:
155 self.failure_reason = f"{self.failure_reason} stderr empty,"
156 self.passed = False
157
158
0dc6e911 159class FioJobFileTest(FioExeTest):
68b3a741 160 """Test consists of a fio job with options in a job file."""
fb551941
VF
161
162 def __init__(self, fio_path, fio_job, success, fio_pre_job=None,
163 fio_pre_success=None, output_format="normal"):
0dc6e911 164 """Construct a FioJobFileTest which is a FioExeTest consisting of a
fb551941
VF
165 single fio job file with an optional setup step.
166
167 fio_path: location of fio executable
168 fio_job: location of fio job file
169 success: Definition of test success
170 fio_pre_job: fio job for preconditioning
171 fio_pre_success: Definition of test success for fio precon job
172 output_format: normal (default), json, jsonplus, or terse
173 """
174
175 self.fio_job = fio_job
176 self.fio_pre_job = fio_pre_job
177 self.fio_pre_success = fio_pre_success if fio_pre_success else success
178 self.output_format = output_format
179 self.precon_failed = False
180 self.json_data = None
68b3a741
VF
181
182 FioExeTest.__init__(self, fio_path, success)
183
184 def setup(self, artifact_root, testnum, parameters=None):
185 """Setup instance variables for fio job test."""
186
187 self.filenames['fio_output'] = f"{os.path.basename(self.fio_job)}.output"
188 fio_args = [
fb551941
VF
189 "--max-jobs=16",
190 f"--output-format={self.output_format}",
68b3a741 191 f"--output={self.filenames['fio_output']}",
fb551941
VF
192 self.fio_job,
193 ]
fb551941 194
68b3a741 195 super().setup(artifact_root, testnum, fio_args)
fb551941 196
68b3a741
VF
197 # Update the filenames from the default
198 self.filenames['cmd'] = os.path.join(self.paths['test_dir'],
199 f"{os.path.basename(self.fio_job)}.command")
200 self.filenames['stdout'] = os.path.join(self.paths['test_dir'],
201 f"{os.path.basename(self.fio_job)}.stdout")
202 self.filenames['stderr'] = os.path.join(self.paths['test_dir'],
203 f"{os.path.basename(self.fio_job)}.stderr")
204 self.filenames['exitcode'] = os.path.join(self.paths['test_dir'],
205 f"{os.path.basename(self.fio_job)}.exitcode")
fb551941
VF
206
207 def run_pre_job(self):
208 """Run fio job precondition step."""
209
68b3a741 210 precon = FioJobFileTest(self.paths['exe'], self.fio_pre_job,
fb551941
VF
211 self.fio_pre_success,
212 output_format=self.output_format)
68b3a741 213 precon.setup(self.paths['artifacts'], self.testnum)
fb551941
VF
214 precon.run()
215 precon.check_result()
216 self.precon_failed = not precon.passed
217 self.failure_reason = precon.failure_reason
218
219 def run(self):
220 """Run fio job test."""
221
222 if self.fio_pre_job:
223 self.run_pre_job()
224
225 if not self.precon_failed:
226 super().run()
227 else:
228 logging.debug("Test %d: precondition step failed", self.testnum)
229
fb551941
VF
230 def get_file_fail(self, filename):
231 """Safely read a file and fail the test upon error."""
232 file_data = None
233
234 try:
235 with open(filename, "r", encoding=locale.getpreferredencoding()) as output_file:
236 file_data = output_file.read()
237 except OSError:
238 self.failure_reason += f" unable to read file {filename}"
239 self.passed = False
240
241 return file_data
242
243 def check_result(self):
244 """Check fio job results."""
245
246 if self.precon_failed:
247 self.passed = False
248 self.failure_reason = f"{self.failure_reason} precondition step failed,"
249 return
250
251 super().check_result()
252
253 if not self.passed:
254 return
255
256 if 'json' not in self.output_format:
257 return
258
68b3a741
VF
259 file_data = self.get_file_fail(os.path.join(self.paths['test_dir'],
260 self.filenames['fio_output']))
fb551941
VF
261 if not file_data:
262 return
263
264 #
265 # Sometimes fio informational messages are included at the top of the
266 # JSON output, especially under Windows. Try to decode output as JSON
267 # data, skipping everything until the first {
268 #
269 lines = file_data.splitlines()
270 file_data = '\n'.join(lines[lines.index("{"):])
271 try:
272 self.json_data = json.loads(file_data)
273 except json.JSONDecodeError:
274 self.failure_reason = f"{self.failure_reason} unable to decode JSON data,"
275 self.passed = False
276
277
278def run_fio_tests(test_list, test_env, args):
279 """
280 Run tests as specified in test_list.
281 """
282
283 passed = 0
284 failed = 0
285 skipped = 0
286
287 for config in test_list:
288 if (args.skip and config['test_id'] in args.skip) or \
289 (args.run_only and config['test_id'] not in args.run_only):
290 skipped = skipped + 1
291 print(f"Test {config['test_id']} SKIPPED (User request)")
292 continue
293
0dc6e911 294 if issubclass(config['test_class'], FioJobFileTest):
fb551941
VF
295 if config['pre_job']:
296 fio_pre_job = os.path.join(test_env['fio_root'], 't', 'jobs',
297 config['pre_job'])
298 else:
299 fio_pre_job = None
300 if config['pre_success']:
301 fio_pre_success = config['pre_success']
302 else:
303 fio_pre_success = None
304 if 'output_format' in config:
305 output_format = config['output_format']
306 else:
307 output_format = 'normal'
308 test = config['test_class'](
309 test_env['fio_path'],
310 os.path.join(test_env['fio_root'], 't', 'jobs', config['job']),
311 config['success'],
312 fio_pre_job=fio_pre_job,
313 fio_pre_success=fio_pre_success,
314 output_format=output_format)
315 desc = config['job']
68b3a741 316 parameters = []
fb551941
VF
317 elif issubclass(config['test_class'], FioExeTest):
318 exe_path = os.path.join(test_env['fio_root'], config['exe'])
68b3a741 319 parameters = []
fb551941
VF
320 if config['parameters']:
321 parameters = [p.format(fio_path=test_env['fio_path'], nvmecdev=args.nvmecdev)
322 for p in config['parameters']]
fb551941
VF
323 if Path(exe_path).suffix == '.py' and platform.system() == "Windows":
324 parameters.insert(0, exe_path)
325 exe_path = "python.exe"
326 if config['test_id'] in test_env['pass_through']:
327 parameters += test_env['pass_through'][config['test_id']].split()
68b3a741 328 test = config['test_class'](exe_path, config['success'])
fb551941
VF
329 desc = config['exe']
330 else:
331 print(f"Test {config['test_id']} FAILED: unable to process test config")
332 failed = failed + 1
333 continue
334
335 if not args.skip_req:
336 reqs_met = True
337 for req in config['requirements']:
338 reqs_met, reason = req()
339 logging.debug("Test %d: Requirement '%s' met? %s", config['test_id'], reason,
340 reqs_met)
341 if not reqs_met:
342 break
343 if not reqs_met:
344 print(f"Test {config['test_id']} SKIPPED ({reason}) {desc}")
345 skipped = skipped + 1
346 continue
347
348 try:
68b3a741 349 test.setup(test_env['artifact_root'], config['test_id'], parameters)
fb551941
VF
350 test.run()
351 test.check_result()
352 except KeyboardInterrupt:
353 break
354 except Exception as e:
355 test.passed = False
356 test.failure_reason += str(e)
357 logging.debug("Test %d exception:\n%s\n", config['test_id'], traceback.format_exc())
358 if test.passed:
359 result = "PASSED"
360 passed = passed + 1
361 else:
362 result = f"FAILED: {test.failure_reason}"
363 failed = failed + 1
68b3a741 364 contents, _ = get_file(test.filenames['stderr'])
fb551941 365 logging.debug("Test %d: stderr:\n%s", config['test_id'], contents)
68b3a741 366 contents, _ = get_file(test.filenames['stdout'])
fb551941
VF
367 logging.debug("Test %d: stdout:\n%s", config['test_id'], contents)
368 print(f"Test {config['test_id']} {result} {desc}")
369
370 print(f"{passed} test(s) passed, {failed} failed, {skipped} skipped")
371
372 return passed, failed, skipped