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
24 """Base for all fio tests."""
26 def __init__(self, exe_path, parameters, success):
27 self.exe_path = exe_path
28 self.parameters = parameters
29 self.success = success
31 self.artifact_root = None
35 self.failure_reason = ''
36 self.command_file = None
37 self.stdout_file = None
38 self.stderr_file = None
39 self.exitcode_file = None
41 def setup(self, artifact_root, testnum):
42 """Setup instance variables for test."""
44 self.artifact_root = artifact_root
45 self.testnum = testnum
46 self.test_dir = os.path.join(artifact_root, f"{testnum:04d}")
47 if not os.path.exists(self.test_dir):
48 os.mkdir(self.test_dir)
50 self.command_file = os.path.join( self.test_dir,
51 f"{os.path.basename(self.exe_path)}.command")
52 self.stdout_file = os.path.join( self.test_dir,
53 f"{os.path.basename(self.exe_path)}.stdout")
54 self.stderr_file = os.path.join( self.test_dir,
55 f"{os.path.basename(self.exe_path)}.stderr")
56 self.exitcode_file = os.path.join( self.test_dir,
57 f"{os.path.basename(self.exe_path)}.exitcode")
62 raise NotImplementedError()
64 def check_result(self):
65 """Check test results."""
67 raise NotImplementedError()
70 class FioExeTest(FioTest):
71 """Test consists of an executable binary or script"""
73 def __init__(self, exe_path, parameters, success):
74 """Construct a FioExeTest which is a FioTest consisting of an
75 executable binary or script.
77 exe_path: location of executable binary or script
78 parameters: list of parameters for executable
79 success: Definition of test success
82 FioTest.__init__(self, exe_path, parameters, success)
85 """Execute the binary or script described by this instance."""
87 command = [self.exe_path] + self.parameters
88 command_file = open(self.command_file, "w+",
89 encoding=locale.getpreferredencoding())
90 command_file.write(f"{command}\n")
93 stdout_file = open(self.stdout_file, "w+",
94 encoding=locale.getpreferredencoding())
95 stderr_file = open(self.stderr_file, "w+",
96 encoding=locale.getpreferredencoding())
97 exitcode_file = open(self.exitcode_file, "w+",
98 encoding=locale.getpreferredencoding())
101 # Avoid using subprocess.run() here because when a timeout occurs,
102 # fio will be stopped with SIGKILL. This does not give fio a
103 # chance to clean up and means that child processes may continue
104 # running and submitting IO.
105 proc = subprocess.Popen(command,
109 universal_newlines=True)
110 proc.communicate(timeout=self.success['timeout'])
111 exitcode_file.write(f'{proc.returncode}\n')
112 logging.debug("Test %d: return code: %d", self.testnum, proc.returncode)
113 self.output['proc'] = proc
114 except subprocess.TimeoutExpired:
118 self.output['failure'] = 'timeout'
124 self.output['failure'] = 'exception'
125 self.output['exc_info'] = sys.exc_info()
129 exitcode_file.close()
131 def check_result(self):
132 """Check results of test run."""
134 if 'proc' not in self.output:
135 if self.output['failure'] == 'timeout':
136 self.failure_reason = f"{self.failure_reason} timeout,"
138 assert self.output['failure'] == 'exception'
139 self.failure_reason = '{0} exception: {1}, {2}'.format(
140 self.failure_reason, self.output['exc_info'][0],
141 self.output['exc_info'][1])
146 if 'zero_return' in self.success:
147 if self.success['zero_return']:
148 if self.output['proc'].returncode != 0:
150 self.failure_reason = f"{self.failure_reason} non-zero return code,"
152 if self.output['proc'].returncode == 0:
153 self.failure_reason = f"{self.failure_reason} zero return code,"
156 stderr_size = os.path.getsize(self.stderr_file)
157 if 'stderr_empty' in self.success:
158 if self.success['stderr_empty']:
160 self.failure_reason = f"{self.failure_reason} stderr not empty,"
164 self.failure_reason = f"{self.failure_reason} stderr empty,"
168 class FioJobFileTest(FioExeTest):
169 """Test consists of a fio job"""
171 def __init__(self, fio_path, fio_job, success, fio_pre_job=None,
172 fio_pre_success=None, output_format="normal"):
173 """Construct a FioJobFileTest which is a FioExeTest consisting of a
174 single fio job file with an optional setup step.
176 fio_path: location of fio executable
177 fio_job: location of fio job file
178 success: Definition of test success
179 fio_pre_job: fio job for preconditioning
180 fio_pre_success: Definition of test success for fio precon job
181 output_format: normal (default), json, jsonplus, or terse
184 self.fio_job = fio_job
185 self.fio_pre_job = fio_pre_job
186 self.fio_pre_success = fio_pre_success if fio_pre_success else success
187 self.output_format = output_format
188 self.precon_failed = False
189 self.json_data = None
190 self.fio_output = f"{os.path.basename(self.fio_job)}.output"
193 f"--output-format={self.output_format}",
194 f"--output={self.fio_output}",
197 FioExeTest.__init__(self, fio_path, self.fio_args, success)
199 def setup(self, artifact_root, testnum):
200 """Setup instance variables for fio job test."""
202 super().setup(artifact_root, testnum)
204 self.command_file = os.path.join(self.test_dir,
205 f"{os.path.basename(self.fio_job)}.command")
206 self.stdout_file = os.path.join(self.test_dir,
207 f"{os.path.basename(self.fio_job)}.stdout")
208 self.stderr_file = os.path.join(self.test_dir,
209 f"{os.path.basename(self.fio_job)}.stderr")
210 self.exitcode_file = os.path.join(self.test_dir,
211 f"{os.path.basename(self.fio_job)}.exitcode")
213 def run_pre_job(self):
214 """Run fio job precondition step."""
216 precon = FioJobFileTest(self.exe_path, self.fio_pre_job,
217 self.fio_pre_success,
218 output_format=self.output_format)
219 precon.setup(self.artifact_root, self.testnum)
221 precon.check_result()
222 self.precon_failed = not precon.passed
223 self.failure_reason = precon.failure_reason
226 """Run fio job test."""
231 if not self.precon_failed:
234 logging.debug("Test %d: precondition step failed", self.testnum)
237 def get_file(cls, filename):
238 """Safely read a file."""
243 with open(filename, "r", encoding=locale.getpreferredencoding()) as output_file:
244 file_data = output_file.read()
248 return file_data, success
250 def get_file_fail(self, filename):
251 """Safely read a file and fail the test upon error."""
255 with open(filename, "r", encoding=locale.getpreferredencoding()) as output_file:
256 file_data = output_file.read()
258 self.failure_reason += f" unable to read file {filename}"
263 def check_result(self):
264 """Check fio job results."""
266 if self.precon_failed:
268 self.failure_reason = f"{self.failure_reason} precondition step failed,"
271 super().check_result()
276 if 'json' not in self.output_format:
279 file_data = self.get_file_fail(os.path.join(self.test_dir, self.fio_output))
284 # Sometimes fio informational messages are included at the top of the
285 # JSON output, especially under Windows. Try to decode output as JSON
286 # data, skipping everything until the first {
288 lines = file_data.splitlines()
289 file_data = '\n'.join(lines[lines.index("{"):])
291 self.json_data = json.loads(file_data)
292 except json.JSONDecodeError:
293 self.failure_reason = f"{self.failure_reason} unable to decode JSON data,"
297 def run_fio_tests(test_list, test_env, args):
299 Run tests as specified in test_list.
306 for config in test_list:
307 if (args.skip and config['test_id'] in args.skip) or \
308 (args.run_only and config['test_id'] not in args.run_only):
309 skipped = skipped + 1
310 print(f"Test {config['test_id']} SKIPPED (User request)")
313 if issubclass(config['test_class'], FioJobFileTest):
314 if config['pre_job']:
315 fio_pre_job = os.path.join(test_env['fio_root'], 't', 'jobs',
319 if config['pre_success']:
320 fio_pre_success = config['pre_success']
322 fio_pre_success = None
323 if 'output_format' in config:
324 output_format = config['output_format']
326 output_format = 'normal'
327 test = config['test_class'](
328 test_env['fio_path'],
329 os.path.join(test_env['fio_root'], 't', 'jobs', config['job']),
331 fio_pre_job=fio_pre_job,
332 fio_pre_success=fio_pre_success,
333 output_format=output_format)
335 elif issubclass(config['test_class'], FioExeTest):
336 exe_path = os.path.join(test_env['fio_root'], config['exe'])
337 if config['parameters']:
338 parameters = [p.format(fio_path=test_env['fio_path'], nvmecdev=args.nvmecdev)
339 for p in config['parameters']]
342 if Path(exe_path).suffix == '.py' and platform.system() == "Windows":
343 parameters.insert(0, exe_path)
344 exe_path = "python.exe"
345 if config['test_id'] in test_env['pass_through']:
346 parameters += test_env['pass_through'][config['test_id']].split()
347 test = config['test_class'](exe_path, parameters,
351 print(f"Test {config['test_id']} FAILED: unable to process test config")
355 if not args.skip_req:
357 for req in config['requirements']:
358 reqs_met, reason = req()
359 logging.debug("Test %d: Requirement '%s' met? %s", config['test_id'], reason,
364 print(f"Test {config['test_id']} SKIPPED ({reason}) {desc}")
365 skipped = skipped + 1
369 test.setup(test_env['artifact_root'], config['test_id'])
372 except KeyboardInterrupt:
374 except Exception as e:
376 test.failure_reason += str(e)
377 logging.debug("Test %d exception:\n%s\n", config['test_id'], traceback.format_exc())
382 result = f"FAILED: {test.failure_reason}"
384 contents, _ = FioJobFileTest.get_file(test.stderr_file)
385 logging.debug("Test %d: stderr:\n%s", config['test_id'], contents)
386 contents, _ = FioJobFileTest.get_file(test.stdout_file)
387 logging.debug("Test %d: stdout:\n%s", config['test_id'], contents)
388 print(f"Test {config['test_id']} {result} {desc}")
390 print(f"{passed} test(s) passed, {failed} failed, {skipped} skipped")
392 return passed, failed, skipped