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
25 """Base for all fio tests."""
27 def __init__(self, exe_path, success):
28 self.paths = {'exe': exe_path}
29 self.success = success
33 self.failure_reason = ''
35 self.parameters = None
37 def setup(self, artifact_root, testnum, parameters):
38 """Setup instance variables for test."""
40 self.testnum = testnum
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'])
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")
59 raise NotImplementedError()
61 def check_result(self):
62 """Check test results."""
64 raise NotImplementedError()
67 class FioExeTest(FioTest):
68 """Test consists of an executable binary or script"""
70 def __init__(self, exe_path, success):
71 """Construct a FioExeTest which is a FioTest consisting of an
72 executable binary or script.
74 exe_path: location of executable binary or script
75 success: Definition of test success
78 FioTest.__init__(self, exe_path, success)
81 """Execute the binary or script described by this instance."""
83 command = [self.paths['exe']] + self.parameters
84 with open(self.filenames['cmd'], "w+",
85 encoding=locale.getpreferredencoding()) as command_file:
86 command_file.write(f"{command}\n")
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:
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,
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
109 except subprocess.TimeoutExpired:
113 self.output['failure'] = 'timeout'
119 self.output['failure'] = 'exception'
120 self.output['exc_info'] = sys.exc_info()
122 def check_result(self):
123 """Check results of test run."""
125 if 'proc' not in self.output:
126 if self.output['failure'] == 'timeout':
127 self.failure_reason = f"{self.failure_reason} timeout,"
129 assert self.output['failure'] == 'exception'
130 self.failure_reason = f'{self.failure_reason} exception: ' + \
131 f'{self.output["exc_info"][0]}, {self.output["exc_info"][1]}'
136 if 'zero_return' in self.success:
137 if self.success['zero_return']:
138 if self.output['proc'].returncode != 0:
140 self.failure_reason = f"{self.failure_reason} non-zero return code,"
142 if self.output['proc'].returncode == 0:
143 self.failure_reason = f"{self.failure_reason} zero return code,"
146 stderr_size = os.path.getsize(self.filenames['stderr'])
147 if 'stderr_empty' in self.success:
148 if self.success['stderr_empty']:
150 self.failure_reason = f"{self.failure_reason} stderr not empty,"
154 self.failure_reason = f"{self.failure_reason} stderr empty,"
158 class FioJobFileTest(FioExeTest):
159 """Test consists of a fio job with options in a job file."""
161 def __init__(self, fio_path, fio_job, success, fio_pre_job=None,
162 fio_pre_success=None, output_format="normal"):
163 """Construct a FioJobFileTest which is a FioExeTest consisting of a
164 single fio job file with an optional setup step.
166 fio_path: location of fio executable
167 fio_job: location of fio job file
168 success: Definition of test success
169 fio_pre_job: fio job for preconditioning
170 fio_pre_success: Definition of test success for fio precon job
171 output_format: normal (default), json, jsonplus, or terse
174 self.fio_job = fio_job
175 self.fio_pre_job = fio_pre_job
176 self.fio_pre_success = fio_pre_success if fio_pre_success else success
177 self.output_format = output_format
178 self.precon_failed = False
179 self.json_data = None
181 FioExeTest.__init__(self, fio_path, success)
183 def setup(self, artifact_root, testnum, parameters=None):
184 """Setup instance variables for fio job test."""
186 self.filenames['fio_output'] = f"{os.path.basename(self.fio_job)}.output"
189 f"--output-format={self.output_format}",
190 f"--output={self.filenames['fio_output']}",
194 super().setup(artifact_root, testnum, fio_args)
196 # Update the filenames from the default
197 self.filenames['cmd'] = os.path.join(self.paths['test_dir'],
198 f"{os.path.basename(self.fio_job)}.command")
199 self.filenames['stdout'] = os.path.join(self.paths['test_dir'],
200 f"{os.path.basename(self.fio_job)}.stdout")
201 self.filenames['stderr'] = os.path.join(self.paths['test_dir'],
202 f"{os.path.basename(self.fio_job)}.stderr")
203 self.filenames['exitcode'] = os.path.join(self.paths['test_dir'],
204 f"{os.path.basename(self.fio_job)}.exitcode")
206 def run_pre_job(self):
207 """Run fio job precondition step."""
209 precon = FioJobFileTest(self.paths['exe'], self.fio_pre_job,
210 self.fio_pre_success,
211 output_format=self.output_format)
212 precon.setup(self.paths['artifacts'], self.testnum)
214 precon.check_result()
215 self.precon_failed = not precon.passed
216 self.failure_reason = precon.failure_reason
219 """Run fio job test."""
224 if not self.precon_failed:
227 logging.debug("Test %d: precondition step failed", self.testnum)
229 def get_file_fail(self, filename):
230 """Safely read a file and fail the test upon error."""
234 with open(filename, "r", encoding=locale.getpreferredencoding()) as output_file:
235 file_data = output_file.read()
237 self.failure_reason += f" unable to read file {filename}"
242 def check_result(self):
243 """Check fio job results."""
245 if self.precon_failed:
247 self.failure_reason = f"{self.failure_reason} precondition step failed,"
250 super().check_result()
255 if 'json' not in self.output_format:
258 file_data = self.get_file_fail(os.path.join(self.paths['test_dir'],
259 self.filenames['fio_output']))
264 # Sometimes fio informational messages are included at the top of the
265 # JSON output, especially under Windows. Try to decode output as JSON
266 # data, skipping everything until the first {
268 lines = file_data.splitlines()
269 file_data = '\n'.join(lines[lines.index("{"):])
271 self.json_data = json.loads(file_data)
272 except json.JSONDecodeError:
273 self.failure_reason = f"{self.failure_reason} unable to decode JSON data,"
277 def run_fio_tests(test_list, test_env, args):
279 Run tests as specified in test_list.
286 for config in test_list:
287 if (args.skip and config['test_id'] in args.skip) or \
288 (args.run_only and config['test_id'] not in args.run_only):
289 skipped = skipped + 1
290 print(f"Test {config['test_id']} SKIPPED (User request)")
293 if issubclass(config['test_class'], FioJobFileTest):
294 if config['pre_job']:
295 fio_pre_job = os.path.join(test_env['fio_root'], 't', 'jobs',
299 if config['pre_success']:
300 fio_pre_success = config['pre_success']
302 fio_pre_success = None
303 if 'output_format' in config:
304 output_format = config['output_format']
306 output_format = 'normal'
307 test = config['test_class'](
308 test_env['fio_path'],
309 os.path.join(test_env['fio_root'], 't', 'jobs', config['job']),
311 fio_pre_job=fio_pre_job,
312 fio_pre_success=fio_pre_success,
313 output_format=output_format)
316 elif issubclass(config['test_class'], FioExeTest):
317 exe_path = os.path.join(test_env['fio_root'], config['exe'])
319 if config['parameters']:
320 parameters = [p.format(fio_path=test_env['fio_path'], nvmecdev=args.nvmecdev)
321 for p in config['parameters']]
322 if Path(exe_path).suffix == '.py' and platform.system() == "Windows":
323 parameters.insert(0, exe_path)
324 exe_path = "python.exe"
325 if config['test_id'] in test_env['pass_through']:
326 parameters += test_env['pass_through'][config['test_id']].split()
327 test = config['test_class'](exe_path, config['success'])
330 print(f"Test {config['test_id']} FAILED: unable to process test config")
334 if not args.skip_req:
336 for req in config['requirements']:
337 reqs_met, reason = req()
338 logging.debug("Test %d: Requirement '%s' met? %s", config['test_id'], reason,
343 print(f"Test {config['test_id']} SKIPPED ({reason}) {desc}")
344 skipped = skipped + 1
348 test.setup(test_env['artifact_root'], config['test_id'], parameters)
351 except KeyboardInterrupt:
353 except Exception as e:
355 test.failure_reason += str(e)
356 logging.debug("Test %d exception:\n%s\n", config['test_id'], traceback.format_exc())
361 result = f"FAILED: {test.failure_reason}"
363 contents, _ = get_file(test.filenames['stderr'])
364 logging.debug("Test %d: stderr:\n%s", config['test_id'], contents)
365 contents, _ = get_file(test.filenames['stdout'])
366 logging.debug("Test %d: stdout:\n%s", config['test_id'], contents)
367 print(f"Test {config['test_id']} {result} {desc}")
369 print(f"{passed} test(s) passed, {failed} failed, {skipped} skipped")
371 return passed, failed, skipped