5 This library contains FioTest objects that provide convenient means to run
6 different sorts of fio tests.
8 It also contains a test runner that runs an array of dictionary objects
20 from pathlib import Path
21 from fiotestcommon import get_file, SUCCESS_DEFAULT
25 """Base for all fio tests."""
27 def __init__(self, exe_path, success, testnum, artifact_root):
28 self.success = success
29 self.testnum = testnum
32 self.failure_reason = ''
33 self.parameters = None
36 'artifacts': artifact_root,
37 'test_dir': os.path.join(artifact_root, \
41 'cmd': os.path.join(self.paths['test_dir'], \
42 f"{os.path.basename(self.paths['exe'])}.command"),
43 'stdout': os.path.join(self.paths['test_dir'], \
44 f"{os.path.basename(self.paths['exe'])}.stdout"),
45 'stderr': os.path.join(self.paths['test_dir'], \
46 f"{os.path.basename(self.paths['exe'])}.stderr"),
47 'exitcode': os.path.join(self.paths['test_dir'], \
48 f"{os.path.basename(self.paths['exe'])}.exitcode"),
51 def setup(self, parameters):
52 """Setup instance variables for test."""
54 self.parameters = parameters
55 if not os.path.exists(self.paths['test_dir']):
56 os.mkdir(self.paths['test_dir'])
61 raise NotImplementedError()
63 def check_result(self):
64 """Check test results."""
66 raise NotImplementedError()
69 class FioExeTest(FioTest):
70 """Test consists of an executable binary or script"""
73 """Execute the binary or script described by this instance."""
75 command = [self.paths['exe']] + self.parameters
76 with open(self.filenames['cmd'], "w+",
77 encoding=locale.getpreferredencoding()) as command_file:
78 command_file.write(" ".join(command))
81 with open(self.filenames['stdout'], "w+",
82 encoding=locale.getpreferredencoding()) as stdout_file, \
83 open(self.filenames['stderr'], "w+",
84 encoding=locale.getpreferredencoding()) as stderr_file, \
85 open(self.filenames['exitcode'], "w+",
86 encoding=locale.getpreferredencoding()) as exitcode_file:
88 # Avoid using subprocess.run() here because when a timeout occurs,
89 # fio will be stopped with SIGKILL. This does not give fio a
90 # chance to clean up and means that child processes may continue
91 # running and submitting IO.
92 proc = subprocess.Popen(command,
95 cwd=self.paths['test_dir'],
96 universal_newlines=True)
97 proc.communicate(timeout=self.success['timeout'])
98 exitcode_file.write(f'{proc.returncode}\n')
99 logging.debug("Test %d: return code: %d", self.testnum, proc.returncode)
100 self.output['proc'] = proc
101 except subprocess.TimeoutExpired:
105 self.output['failure'] = 'timeout'
111 self.output['failure'] = 'exception'
112 self.output['exc_info'] = sys.exc_info()
114 def check_result(self):
115 """Check results of test run."""
117 if 'proc' not in self.output:
118 if self.output['failure'] == 'timeout':
119 self.failure_reason = f"{self.failure_reason} timeout,"
121 assert self.output['failure'] == 'exception'
122 self.failure_reason = f'{self.failure_reason} exception: ' + \
123 f'{self.output["exc_info"][0]}, {self.output["exc_info"][1]}'
128 if 'zero_return' in self.success:
129 if self.success['zero_return']:
130 if self.output['proc'].returncode != 0:
132 self.failure_reason = f"{self.failure_reason} non-zero return code,"
134 if self.output['proc'].returncode == 0:
135 self.failure_reason = f"{self.failure_reason} zero return code,"
138 stderr_size = os.path.getsize(self.filenames['stderr'])
139 if 'stderr_empty' in self.success:
140 if self.success['stderr_empty']:
142 self.failure_reason = f"{self.failure_reason} stderr not empty,"
146 self.failure_reason = f"{self.failure_reason} stderr empty,"
150 class FioJobFileTest(FioExeTest):
151 """Test consists of a fio job with options in a job file."""
153 def __init__(self, fio_path, fio_job, success, testnum, artifact_root,
154 fio_pre_job=None, fio_pre_success=None,
155 output_format="normal"):
156 """Construct a FioJobFileTest which is a FioExeTest consisting of a
157 single fio job file with an optional setup step.
159 fio_path: location of fio executable
160 fio_job: location of fio job file
161 success: Definition of test success
163 artifact_root: root directory for artifacts
164 fio_pre_job: fio job for preconditioning
165 fio_pre_success: Definition of test success for fio precon job
166 output_format: normal (default), json, jsonplus, or terse
169 self.fio_job = fio_job
170 self.fio_pre_job = fio_pre_job
171 self.fio_pre_success = fio_pre_success if fio_pre_success else success
172 self.output_format = output_format
173 self.precon_failed = False
174 self.json_data = None
176 super().__init__(fio_path, success, testnum, artifact_root)
178 def setup(self, parameters=None):
179 """Setup instance variables for fio job test."""
181 self.filenames['fio_output'] = f"{os.path.basename(self.fio_job)}.output"
184 f"--output-format={self.output_format}",
185 f"--output={self.filenames['fio_output']}",
189 super().setup(fio_args)
191 # Update the filenames from the default
192 self.filenames['cmd'] = os.path.join(self.paths['test_dir'],
193 f"{os.path.basename(self.fio_job)}.command")
194 self.filenames['stdout'] = os.path.join(self.paths['test_dir'],
195 f"{os.path.basename(self.fio_job)}.stdout")
196 self.filenames['stderr'] = os.path.join(self.paths['test_dir'],
197 f"{os.path.basename(self.fio_job)}.stderr")
198 self.filenames['exitcode'] = os.path.join(self.paths['test_dir'],
199 f"{os.path.basename(self.fio_job)}.exitcode")
201 def run_pre_job(self):
202 """Run fio job precondition step."""
204 precon = FioJobFileTest(self.paths['exe'], self.fio_pre_job,
205 self.fio_pre_success,
207 self.paths['artifacts'],
208 output_format=self.output_format)
211 precon.check_result()
212 self.precon_failed = not precon.passed
213 self.failure_reason = precon.failure_reason
216 """Run fio job test."""
221 if not self.precon_failed:
224 logging.debug("Test %d: precondition step failed", self.testnum)
226 def get_file_fail(self, filename):
227 """Safely read a file and fail the test upon error."""
231 with open(filename, "r", encoding=locale.getpreferredencoding()) as output_file:
232 file_data = output_file.read()
234 self.failure_reason += f" unable to read file {filename}"
239 def check_result(self):
240 """Check fio job results."""
242 if self.precon_failed:
244 self.failure_reason = f"{self.failure_reason} precondition step failed,"
247 super().check_result()
252 if 'json' not in self.output_format:
255 file_data = self.get_file_fail(os.path.join(self.paths['test_dir'],
256 self.filenames['fio_output']))
261 # Sometimes fio informational messages are included at the top of the
262 # JSON output, especially under Windows. Try to decode output as JSON
263 # data, skipping everything until the first {
265 lines = file_data.splitlines()
266 file_data = '\n'.join(lines[lines.index("{"):])
268 self.json_data = json.loads(file_data)
269 except json.JSONDecodeError:
270 self.failure_reason = f"{self.failure_reason} unable to decode JSON data,"
274 class FioJobCmdTest(FioExeTest):
275 """This runs a fio job with options specified on the command line."""
277 def __init__(self, fio_path, success, testnum, artifact_root, fio_opts, basename=None):
279 self.basename = basename if basename else os.path.basename(fio_path)
280 self.fio_opts = fio_opts
281 self.json_data = None
282 self.iops_log_lines = None
284 super().__init__(fio_path, success, testnum, artifact_root)
286 filename_stub = os.path.join(self.paths['test_dir'], f"{self.basename}{self.testnum:03d}")
287 self.filenames['cmd'] = f"{filename_stub}.command"
288 self.filenames['stdout'] = f"{filename_stub}.stdout"
289 self.filenames['stderr'] = f"{filename_stub}.stderr"
290 self.filenames['output'] = os.path.abspath(f"{filename_stub}.output")
291 self.filenames['exitcode'] = f"{filename_stub}.exitcode"
292 self.filenames['iopslog'] = os.path.abspath(f"{filename_stub}")
297 if 'output-format' in self.fio_opts and 'json' in \
298 self.fio_opts['output-format']:
299 if not self.get_json():
300 print('Unable to decode JSON data')
303 if any('--write_iops_log=' in param for param in self.parameters):
306 def get_iops_log(self):
307 """Read IOPS log from the first job."""
309 log_filename = self.filenames['iopslog'] + "_iops.1.log"
310 with open(log_filename, 'r', encoding=locale.getpreferredencoding()) as iops_file:
311 self.iops_log_lines = iops_file.read()
314 """Convert fio JSON output into a python JSON object"""
316 filename = self.filenames['output']
317 with open(filename, 'r', encoding=locale.getpreferredencoding()) as file:
318 file_data = file.read()
321 # Sometimes fio informational messages are included at the top of the
322 # JSON output, especially under Windows. Try to decode output as JSON
323 # data, lopping off up to the first four lines
325 lines = file_data.splitlines()
327 file_data = '\n'.join(lines[i:])
329 self.json_data = json.loads(file_data)
330 except json.JSONDecodeError:
338 def check_empty(job):
340 Make sure JSON data is empty.
342 Some data structures should be empty. This function makes sure that they are.
344 job JSON object that we need to check for emptiness
347 return job['total_ios'] == 0 and \
348 job['slat_ns']['N'] == 0 and \
349 job['clat_ns']['N'] == 0 and \
350 job['lat_ns']['N'] == 0
352 def check_all_ddirs(self, ddir_nonzero, job):
354 Iterate over the data directions and check whether each is
355 appropriately empty or not.
359 ddirlist = ['read', 'write', 'trim']
361 for ddir in ddirlist:
362 if ddir in ddir_nonzero:
363 if self.check_empty(job[ddir]):
364 print(f"Unexpected zero {ddir} data found in output")
367 if not self.check_empty(job[ddir]):
368 print(f"Unexpected {ddir} data found in output")
374 def run_fio_tests(test_list, test_env, args):
376 Run tests as specified in test_list.
383 for config in test_list:
384 if (args.skip and config['test_id'] in args.skip) or \
385 (args.run_only and config['test_id'] not in args.run_only):
386 skipped = skipped + 1
387 print(f"Test {config['test_id']} SKIPPED (User request)")
390 if issubclass(config['test_class'], FioJobFileTest):
391 if config['pre_job']:
392 fio_pre_job = os.path.join(test_env['fio_root'], 't', 'jobs',
396 if config['pre_success']:
397 fio_pre_success = config['pre_success']
399 fio_pre_success = None
400 if 'output_format' in config:
401 output_format = config['output_format']
403 output_format = 'normal'
404 test = config['test_class'](
405 test_env['fio_path'],
406 os.path.join(test_env['fio_root'], 't', 'jobs', config['job']),
409 test_env['artifact_root'],
410 fio_pre_job=fio_pre_job,
411 fio_pre_success=fio_pre_success,
412 output_format=output_format)
415 elif issubclass(config['test_class'], FioJobCmdTest):
416 if not 'success' in config:
417 config['success'] = SUCCESS_DEFAULT
418 test = config['test_class'](test_env['fio_path'],
421 test_env['artifact_root'],
423 test_env['basename'])
424 desc = config['test_id']
426 elif issubclass(config['test_class'], FioExeTest):
427 exe_path = os.path.join(test_env['fio_root'], config['exe'])
429 if config['parameters']:
430 parameters = [p.format(fio_path=test_env['fio_path'], nvmecdev=args.nvmecdev)
431 for p in config['parameters']]
432 if Path(exe_path).suffix == '.py' and platform.system() == "Windows":
433 parameters.insert(0, exe_path)
434 exe_path = "python.exe"
435 if config['test_id'] in test_env['pass_through']:
436 parameters += test_env['pass_through'][config['test_id']].split()
437 test = config['test_class'](
441 test_env['artifact_root'])
444 print(f"Test {config['test_id']} FAILED: unable to process test config")
448 if 'requirements' in config and not args.skip_req:
450 for req in config['requirements']:
451 reqs_met, reason = req()
452 logging.debug("Test %d: Requirement '%s' met? %s", config['test_id'], reason,
457 print(f"Test {config['test_id']} SKIPPED ({reason}) {desc}")
458 skipped = skipped + 1
462 test.setup(parameters)
465 except KeyboardInterrupt:
467 except Exception as e:
469 test.failure_reason += str(e)
470 logging.debug("Test %d exception:\n%s\n", config['test_id'], traceback.format_exc())
475 result = f"FAILED: {test.failure_reason}"
477 contents, _ = get_file(test.filenames['stderr'])
478 logging.debug("Test %d: stderr:\n%s", config['test_id'], contents)
479 contents, _ = get_file(test.filenames['stdout'])
480 logging.debug("Test %d: stdout:\n%s", config['test_id'], contents)
481 print(f"Test {config['test_id']} {result} {desc}")
483 print(f"{passed} test(s) passed, {failed} failed, {skipped} skipped")
485 return passed, failed, skipped