2 # SPDX-License-Identifier: GPL-2.0-only
4 # Copyright (c) 2019 Western Digital Corporation or its affiliates.
9 # Automate running of fio tests
12 # python3 run-fio-tests.py [-r fio-root] [-f fio-path] [-a artifact-root]
13 # [--skip # # #...] [--run-only # # #...]
17 # # git clone git://git.kernel.dk/fio.git
20 # # python3 t/run-fio-tests.py
24 # - Python 3.5 (subprocess.run)
25 # - Linux (libaio ioengine, zbd tests, etc)
26 # - The artifact directory must be on a file system that accepts 512-byte IO
27 # (t0002, t0003, t0004).
28 # - The artifact directory needs to be on an SSD. Otherwise tests that carry
29 # out file-based IO will trigger a timeout (t0006).
31 # - SciPy (steadystate_tests.py)
32 # - libzbc (zbd tests)
33 # - root privileges (zbd test)
34 # - kernel 4.19 or later for zoned null block devices (zbd tests)
35 # - CUnit support (unittests)
40 # TODO run multiple tests simultaneously
41 # TODO Add sgunmap tests (requires SAS SSD)
53 import multiprocessing
54 from pathlib import Path
57 class FioTest(object):
58 """Base for all fio tests."""
60 def __init__(self, exe_path, parameters, success):
61 self.exe_path = exe_path
62 self.parameters = parameters
63 self.success = success
65 self.artifact_root = None
69 self.failure_reason = ''
71 def setup(self, artifact_root, testnum):
72 self.artifact_root = artifact_root
73 self.testnum = testnum
74 self.test_dir = os.path.join(artifact_root, "{:04d}".format(testnum))
75 if not os.path.exists(self.test_dir):
76 os.mkdir(self.test_dir)
78 self.command_file = os.path.join(
80 "{0}.command".format(os.path.basename(self.exe_path)))
81 self.stdout_file = os.path.join(
83 "{0}.stdout".format(os.path.basename(self.exe_path)))
84 self.stderr_file = os.path.join(
86 "{0}.stderr".format(os.path.basename(self.exe_path)))
87 self.exticode_file = os.path.join(
89 "{0}.exitcode".format(os.path.basename(self.exe_path)))
92 raise NotImplementedError()
94 def check_result(self):
95 raise NotImplementedError()
98 class FioExeTest(FioTest):
99 """Test consists of an executable binary or script"""
101 def __init__(self, exe_path, parameters, success):
102 """Construct a FioExeTest which is a FioTest consisting of an
103 executable binary or script.
105 exe_path: location of executable binary or script
106 parameters: list of parameters for executable
107 success: Definition of test success
110 FioTest.__init__(self, exe_path, parameters, success)
112 def setup(self, artifact_root, testnum):
113 super(FioExeTest, self).setup(artifact_root, testnum)
117 command = [self.exe_path] + self.parameters
119 command = [self.exe_path]
120 command_file = open(self.command_file, "w+")
121 command_file.write("%s\n" % command)
124 stdout_file = open(self.stdout_file, "w+")
125 stderr_file = open(self.stderr_file, "w+")
126 exticode_file = open(self.exticode_file, "w+")
129 # Avoid using subprocess.run() here because when a timeout occurs,
130 # fio will be stopped with SIGKILL. This does not give fio a
131 # chance to clean up and means that child processes may continue
132 # running and submitting IO.
133 proc = subprocess.Popen(command,
137 universal_newlines=True)
138 proc.communicate(timeout=self.success['timeout'])
139 exticode_file.write('{0}\n'.format(proc.returncode))
140 logging.debug("return code: %d" % proc.returncode)
141 self.output['proc'] = proc
142 except subprocess.TimeoutExpired:
146 self.output['failure'] = 'timeout'
152 self.output['failure'] = 'exception'
153 self.output['exc_info'] = sys.exc_info()
157 exticode_file.close()
159 def check_result(self):
160 if 'proc' not in self.output:
161 if self.output['failure'] == 'timeout':
162 self.failure_reason = "{0} timeout,".format(self.failure_reason)
164 assert self.output['failure'] == 'exception'
165 self.failure_reason = '{0} exception: {1}, {2}'.format(
166 self.failure_reason, self.output['exc_info'][0],
167 self.output['exc_info'][1])
172 if 'zero_return' in self.success:
173 if self.success['zero_return']:
174 if self.output['proc'].returncode != 0:
176 self.failure_reason = "{0} non-zero return code,".format(self.failure_reason)
178 if self.output['proc'].returncode == 0:
179 self.failure_reason = "{0} zero return code,".format(self.failure_reason)
182 stderr_size = os.path.getsize(self.stderr_file)
183 if 'stderr_empty' in self.success:
184 if self.success['stderr_empty']:
186 self.failure_reason = "{0} stderr not empty,".format(self.failure_reason)
190 self.failure_reason = "{0} stderr empty,".format(self.failure_reason)
194 class FioJobTest(FioExeTest):
195 """Test consists of a fio job"""
197 def __init__(self, fio_path, fio_job, success, fio_pre_job=None,
198 fio_pre_success=None, output_format="normal"):
199 """Construct a FioJobTest which is a FioExeTest consisting of a
200 single fio job file with an optional setup step.
202 fio_path: location of fio executable
203 fio_job: location of fio job file
204 success: Definition of test success
205 fio_pre_job: fio job for preconditioning
206 fio_pre_success: Definition of test success for fio precon job
207 output_format: normal (default), json, jsonplus, or terse
210 self.fio_job = fio_job
211 self.fio_pre_job = fio_pre_job
212 self.fio_pre_success = fio_pre_success if fio_pre_success else success
213 self.output_format = output_format
214 self.precon_failed = False
215 self.json_data = None
216 self.fio_output = "{0}.output".format(os.path.basename(self.fio_job))
218 "--output-format={0}".format(self.output_format),
219 "--output={0}".format(self.fio_output),
222 FioExeTest.__init__(self, fio_path, self.fio_args, success)
224 def setup(self, artifact_root, testnum):
225 super(FioJobTest, self).setup(artifact_root, testnum)
227 self.command_file = os.path.join(
229 "{0}.command".format(os.path.basename(self.fio_job)))
230 self.stdout_file = os.path.join(
232 "{0}.stdout".format(os.path.basename(self.fio_job)))
233 self.stderr_file = os.path.join(
235 "{0}.stderr".format(os.path.basename(self.fio_job)))
236 self.exticode_file = os.path.join(
238 "{0}.exitcode".format(os.path.basename(self.fio_job)))
240 def run_pre_job(self):
241 precon = FioJobTest(self.exe_path, self.fio_pre_job,
242 self.fio_pre_success,
243 output_format=self.output_format)
244 precon.setup(self.artifact_root, self.testnum)
246 precon.check_result()
247 self.precon_failed = not precon.passed
248 self.failure_reason = precon.failure_reason
254 if not self.precon_failed:
255 super(FioJobTest, self).run()
257 logging.debug("precondition step failed")
259 def check_result(self):
260 if self.precon_failed:
262 self.failure_reason = "{0} precondition step failed,".format(self.failure_reason)
265 super(FioJobTest, self).check_result()
270 if 'json' in self.output_format:
272 with open(os.path.join(self.test_dir, self.fio_output), "r") as output_file:
273 file_data = output_file.read()
274 except EnvironmentError:
275 self.failure_reason = "{0} unable to open output file,".format(self.failure_reason)
279 self.json_data = json.loads(file_data)
280 except json.JSONDecodeError:
281 self.failure_reason = "{0} unable to decode JSON data,".format(self.failure_reason)
285 class FioJobTest_t0005(FioJobTest):
286 """Test consists of fio test job t0005
287 Confirm that read['io_kbytes'] == write['io_kbytes'] == 102400"""
289 def check_result(self):
290 super(FioJobTest_t0005, self).check_result()
295 if self.json_data['jobs'][0]['read']['io_kbytes'] != 102400:
296 self.failure_reason = "{0} bytes read mismatch,".format(self.failure_reason)
298 if self.json_data['jobs'][0]['write']['io_kbytes'] != 102400:
299 self.failure_reason = "{0} bytes written mismatch,".format(self.failure_reason)
303 class FioJobTest_t0006(FioJobTest):
304 """Test consists of fio test job t0006
305 Confirm that read['io_kbytes'] ~ 2*write['io_kbytes']"""
307 def check_result(self):
308 super(FioJobTest_t0006, self).check_result()
313 ratio = self.json_data['jobs'][0]['read']['io_kbytes'] \
314 / self.json_data['jobs'][0]['write']['io_kbytes']
315 logging.debug("ratio: %f" % ratio)
316 if ratio < 1.99 or ratio > 2.01:
317 self.failure_reason = "{0} read/write ratio mismatch,".format(self.failure_reason)
321 class FioJobTest_t0007(FioJobTest):
322 """Test consists of fio test job t0007
323 Confirm that read['io_kbytes'] = 87040"""
325 def check_result(self):
326 super(FioJobTest_t0007, self).check_result()
331 if self.json_data['jobs'][0]['read']['io_kbytes'] != 87040:
332 self.failure_reason = "{0} bytes read mismatch,".format(self.failure_reason)
336 class FioJobTest_t0008(FioJobTest):
337 """Test consists of fio test job t0008
338 Confirm that read['io_kbytes'] = 32768 and that
339 write['io_kbytes'] ~ 16568
341 I did runs with fio-ae2fafc8 and saw write['io_kbytes'] values of
342 16585, 16588. With two runs of fio-3.16 I obtained 16568"""
344 def check_result(self):
345 super(FioJobTest_t0008, self).check_result()
350 ratio = self.json_data['jobs'][0]['write']['io_kbytes'] / 16568
351 logging.debug("ratio: %f" % ratio)
353 if ratio < 0.99 or ratio > 1.01:
354 self.failure_reason = "{0} bytes written mismatch,".format(self.failure_reason)
356 if self.json_data['jobs'][0]['read']['io_kbytes'] != 32768:
357 self.failure_reason = "{0} bytes read mismatch,".format(self.failure_reason)
361 class FioJobTest_t0009(FioJobTest):
362 """Test consists of fio test job t0009
363 Confirm that runtime >= 60s"""
365 def check_result(self):
366 super(FioJobTest_t0009, self).check_result()
371 logging.debug('elapsed: %d' % self.json_data['jobs'][0]['elapsed'])
373 if self.json_data['jobs'][0]['elapsed'] < 60:
374 self.failure_reason = "{0} elapsed time mismatch,".format(self.failure_reason)
378 class FioJobTest_t0011(FioJobTest):
379 """Test consists of fio test job t0009
380 Confirm that job0 iops == 1000
381 and that job1_iops / job0_iops ~ 8
382 With two runs of fio-3.16 I observed a ratio of 8.3"""
384 def check_result(self):
385 super(FioJobTest_t0011, self).check_result()
390 iops1 = self.json_data['jobs'][0]['read']['iops']
391 iops2 = self.json_data['jobs'][1]['read']['iops']
392 ratio = iops2 / iops1
393 logging.debug("ratio: %f" % ratio)
395 if iops1 < 999 or iops1 > 1001:
396 self.failure_reason = "{0} iops value mismatch,".format(self.failure_reason)
399 if ratio < 7 or ratio > 9:
400 self.failure_reason = "{0} iops ratio mismatch,".format(self.failure_reason)
404 class Requirements(object):
405 """Requirements consists of multiple run environment characteristics.
406 These are to determine if a particular test can be run"""
417 def __init__(self, fio_root):
418 Requirements._not_macos = platform.system() != "Darwin"
419 Requirements._linux = platform.system() == "Linux"
421 if Requirements._linux:
423 config_file = os.path.join(fio_root, "config-host.h")
424 with open(config_file, "r") as config:
425 contents = config.read()
427 print("Unable to open {0} to check requirements".format(config_file))
428 Requirements._zbd = True
430 Requirements._zbd = "CONFIG_LINUX_BLKZONED" in contents
431 Requirements._libaio = "CONFIG_LIBAIO" in contents
433 Requirements._root = (os.geteuid() == 0)
434 if Requirements._zbd and Requirements._root:
435 subprocess.run(["modprobe", "null_blk"],
436 stdout=subprocess.PIPE,
437 stderr=subprocess.PIPE)
438 if os.path.exists("/sys/module/null_blk/parameters/zoned"):
439 Requirements._zoned_nullb = True
441 unittest_path = os.path.join(fio_root, "unittests", "unittest")
442 Requirements._unittests = os.path.exists(unittest_path)
444 Requirements._cpucount4 = multiprocessing.cpu_count() >= 4
446 req_list = [Requirements.linux,
450 Requirements.zoned_nullb,
451 Requirements.not_macos,
452 Requirements.unittests,
453 Requirements.cpucount4]
456 logging.debug("Requirement '%s' met? %s" % (desc, value))
459 return Requirements._linux, "Linux required"
462 return Requirements._libaio, "libaio required"
465 return Requirements._zbd, "Zoned block device support required"
468 return Requirements._root, "root required"
471 return Requirements._zoned_nullb, "Zoned null block device support required"
474 return Requirements._not_macos, "platform other than macOS required"
477 return Requirements._unittests, "Unittests support required"
480 return Requirements._cpucount4, "4+ CPUs required"
485 'stderr_empty': True,
489 'zero_return': False,
490 'stderr_empty': False,
495 'stderr_empty': False,
501 'test_class': FioJobTest,
502 'job': 't0001-52c58027.fio',
503 'success': SUCCESS_DEFAULT,
510 'test_class': FioJobTest,
511 'job': 't0002-13af05ae-post.fio',
512 'success': SUCCESS_DEFAULT,
513 'pre_job': 't0002-13af05ae-pre.fio',
515 'requirements': [Requirements.linux, Requirements.libaio],
519 'test_class': FioJobTest,
520 'job': 't0003-0ae2c6e1-post.fio',
521 'success': SUCCESS_NONZERO,
522 'pre_job': 't0003-0ae2c6e1-pre.fio',
523 'pre_success': SUCCESS_DEFAULT,
524 'requirements': [Requirements.linux, Requirements.libaio],
528 'test_class': FioJobTest,
529 'job': 't0004-8a99fdf6.fio',
530 'success': SUCCESS_DEFAULT,
533 'requirements': [Requirements.linux, Requirements.libaio],
537 'test_class': FioJobTest_t0005,
538 'job': 't0005-f7078f7b.fio',
539 'success': SUCCESS_DEFAULT,
542 'output_format': 'json',
547 'test_class': FioJobTest_t0006,
548 'job': 't0006-82af2a7c.fio',
549 'success': SUCCESS_DEFAULT,
552 'output_format': 'json',
553 'requirements': [Requirements.linux, Requirements.libaio],
557 'test_class': FioJobTest_t0007,
558 'job': 't0007-37cf9e3c.fio',
559 'success': SUCCESS_DEFAULT,
562 'output_format': 'json',
567 'test_class': FioJobTest_t0008,
568 'job': 't0008-ae2fafc8.fio',
569 'success': SUCCESS_DEFAULT,
572 'output_format': 'json',
577 'test_class': FioJobTest_t0009,
578 'job': 't0009-f8b0bd10.fio',
579 'success': SUCCESS_DEFAULT,
582 'output_format': 'json',
583 'requirements': [Requirements.not_macos,
584 Requirements.cpucount4],
585 # mac os does not support CPU affinity
589 'test_class': FioJobTest,
590 'job': 't0010-b7aae4ba.fio',
591 'success': SUCCESS_DEFAULT,
598 'test_class': FioJobTest_t0011,
599 'job': 't0011-5d2788d5.fio',
600 'success': SUCCESS_DEFAULT,
603 'output_format': 'json',
608 'test_class': FioExeTest,
611 'success': SUCCESS_DEFAULT,
616 'test_class': FioExeTest,
619 'success': SUCCESS_DEFAULT,
624 'test_class': FioExeTest,
625 'exe': 't/lfsr-test',
626 'parameters': ['0xFFFFFF', '0', '0', 'verify'],
627 'success': SUCCESS_STDERR,
632 'test_class': FioExeTest,
633 'exe': 't/readonly.py',
634 'parameters': ['-f', '{fio_path}'],
635 'success': SUCCESS_DEFAULT,
640 'test_class': FioExeTest,
641 'exe': 't/steadystate_tests.py',
642 'parameters': ['{fio_path}'],
643 'success': SUCCESS_DEFAULT,
648 'test_class': FioExeTest,
651 'success': SUCCESS_STDERR,
656 'test_class': FioExeTest,
657 'exe': 't/strided.py',
658 'parameters': ['{fio_path}'],
659 'success': SUCCESS_DEFAULT,
664 'test_class': FioExeTest,
665 'exe': 't/zbd/run-tests-against-regular-nullb',
667 'success': SUCCESS_DEFAULT,
668 'requirements': [Requirements.linux, Requirements.zbd,
673 'test_class': FioExeTest,
674 'exe': 't/zbd/run-tests-against-zoned-nullb',
676 'success': SUCCESS_DEFAULT,
677 'requirements': [Requirements.linux, Requirements.zbd,
678 Requirements.root, Requirements.zoned_nullb],
682 'test_class': FioExeTest,
683 'exe': 'unittests/unittest',
685 'success': SUCCESS_DEFAULT,
686 'requirements': [Requirements.unittests],
692 parser = argparse.ArgumentParser()
693 parser.add_argument('-r', '--fio-root',
694 help='fio root path')
695 parser.add_argument('-f', '--fio',
696 help='path to fio executable (e.g., ./fio)')
697 parser.add_argument('-a', '--artifact-root',
698 help='artifact root directory')
699 parser.add_argument('-s', '--skip', nargs='+', type=int,
700 help='list of test(s) to skip')
701 parser.add_argument('-o', '--run-only', nargs='+', type=int,
702 help='list of test(s) to run, skipping all others')
703 parser.add_argument('-d', '--debug', action='store_true',
704 help='provide debug output')
705 parser.add_argument('-k', '--skip-req', action='store_true',
706 help='skip requirements checking')
707 args = parser.parse_args()
715 logging.basicConfig(level=logging.DEBUG)
717 logging.basicConfig(level=logging.INFO)
720 fio_root = args.fio_root
722 fio_root = str(Path(__file__).absolute().parent.parent)
723 print("fio root is %s" % fio_root)
728 fio_path = os.path.join(fio_root, "fio")
729 print("fio path is %s" % fio_path)
730 if not shutil.which(fio_path):
731 print("Warning: fio executable not found")
733 artifact_root = args.artifact_root if args.artifact_root else \
734 "fio-test-{0}".format(time.strftime("%Y%m%d-%H%M%S"))
735 os.mkdir(artifact_root)
736 print("Artifact directory is %s" % artifact_root)
738 if not args.skip_req:
739 req = Requirements(fio_root)
745 for config in TEST_LIST:
746 if (args.skip and config['test_id'] in args.skip) or \
747 (args.run_only and config['test_id'] not in args.run_only):
748 skipped = skipped + 1
749 print("Test {0} SKIPPED (User request)".format(config['test_id']))
752 if issubclass(config['test_class'], FioJobTest):
753 if config['pre_job']:
754 fio_pre_job = os.path.join(fio_root, 't', 'jobs',
758 if config['pre_success']:
759 fio_pre_success = config['pre_success']
761 fio_pre_success = None
762 if 'output_format' in config:
763 output_format = config['output_format']
765 output_format = 'normal'
766 test = config['test_class'](
768 os.path.join(fio_root, 't', 'jobs', config['job']),
770 fio_pre_job=fio_pre_job,
771 fio_pre_success=fio_pre_success,
772 output_format=output_format)
773 elif issubclass(config['test_class'], FioExeTest):
774 exe_path = os.path.join(fio_root, config['exe'])
775 if config['parameters']:
776 parameters = [p.format(fio_path=fio_path) for p in config['parameters']]
779 test = config['test_class'](exe_path, parameters,
782 print("Test {0} FAILED: unable to process test config".format(config['test_id']))
786 if not args.skip_req:
788 for req in config['requirements']:
791 logging.debug("Requirement '%s' met? %s" % (reason, ok))
795 print("Test {0} SKIPPED ({1})".format(config['test_id'], reason))
796 skipped = skipped + 1
799 test.setup(artifact_root, config['test_id'])
806 result = "FAILED: {0}".format(test.failure_reason)
808 with open(test.stderr_file, "r") as stderr_file:
809 logging.debug("stderr:\n%s" % stderr_file.read())
810 with open(test.stdout_file, "r") as stdout_file:
811 logging.debug("stdout:\n%s" % stdout_file.read())
812 print("Test {0} {1}".format(config['test_id'], result))
814 print("{0} test(s) passed, {1} failed, {2} skipped".format(passed, failed, skipped))
819 if __name__ == '__main__':