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