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)
54 import multiprocessing
55 from pathlib import Path
59 """Base for all fio tests."""
61 def __init__(self, exe_path, parameters, success):
62 self.exe_path = exe_path
63 self.parameters = parameters
64 self.success = success
66 self.artifact_root = None
70 self.failure_reason = ''
71 self.command_file = None
72 self.stdout_file = None
73 self.stderr_file = None
74 self.exitcode_file = None
76 def setup(self, artifact_root, testnum):
77 """Setup instance variables for test."""
79 self.artifact_root = artifact_root
80 self.testnum = testnum
81 self.test_dir = os.path.join(artifact_root, "{:04d}".format(testnum))
82 if not os.path.exists(self.test_dir):
83 os.mkdir(self.test_dir)
85 self.command_file = os.path.join(
87 "{0}.command".format(os.path.basename(self.exe_path)))
88 self.stdout_file = os.path.join(
90 "{0}.stdout".format(os.path.basename(self.exe_path)))
91 self.stderr_file = os.path.join(
93 "{0}.stderr".format(os.path.basename(self.exe_path)))
94 self.exitcode_file = os.path.join(
96 "{0}.exitcode".format(os.path.basename(self.exe_path)))
101 raise NotImplementedError()
103 def check_result(self):
104 """Check test results."""
106 raise NotImplementedError()
109 class FioExeTest(FioTest):
110 """Test consists of an executable binary or script"""
112 def __init__(self, exe_path, parameters, success):
113 """Construct a FioExeTest which is a FioTest consisting of an
114 executable binary or script.
116 exe_path: location of executable binary or script
117 parameters: list of parameters for executable
118 success: Definition of test success
121 FioTest.__init__(self, exe_path, parameters, success)
124 """Execute the binary or script described by this instance."""
126 command = [self.exe_path] + self.parameters
127 command_file = open(self.command_file, "w+")
128 command_file.write("%s\n" % command)
131 stdout_file = open(self.stdout_file, "w+")
132 stderr_file = open(self.stderr_file, "w+")
133 exitcode_file = open(self.exitcode_file, "w+")
136 # Avoid using subprocess.run() here because when a timeout occurs,
137 # fio will be stopped with SIGKILL. This does not give fio a
138 # chance to clean up and means that child processes may continue
139 # running and submitting IO.
140 proc = subprocess.Popen(command,
144 universal_newlines=True)
145 proc.communicate(timeout=self.success['timeout'])
146 exitcode_file.write('{0}\n'.format(proc.returncode))
147 logging.debug("Test %d: return code: %d", self.testnum, proc.returncode)
148 self.output['proc'] = proc
149 except subprocess.TimeoutExpired:
153 self.output['failure'] = 'timeout'
159 self.output['failure'] = 'exception'
160 self.output['exc_info'] = sys.exc_info()
164 exitcode_file.close()
166 def check_result(self):
167 """Check results of test run."""
169 if 'proc' not in self.output:
170 if self.output['failure'] == 'timeout':
171 self.failure_reason = "{0} timeout,".format(self.failure_reason)
173 assert self.output['failure'] == 'exception'
174 self.failure_reason = '{0} exception: {1}, {2}'.format(
175 self.failure_reason, self.output['exc_info'][0],
176 self.output['exc_info'][1])
181 if 'zero_return' in self.success:
182 if self.success['zero_return']:
183 if self.output['proc'].returncode != 0:
185 self.failure_reason = "{0} non-zero return code,".format(self.failure_reason)
187 if self.output['proc'].returncode == 0:
188 self.failure_reason = "{0} zero return code,".format(self.failure_reason)
191 stderr_size = os.path.getsize(self.stderr_file)
192 if 'stderr_empty' in self.success:
193 if self.success['stderr_empty']:
195 self.failure_reason = "{0} stderr not empty,".format(self.failure_reason)
199 self.failure_reason = "{0} stderr empty,".format(self.failure_reason)
203 class FioJobTest(FioExeTest):
204 """Test consists of a fio job"""
206 def __init__(self, fio_path, fio_job, success, fio_pre_job=None,
207 fio_pre_success=None, output_format="normal"):
208 """Construct a FioJobTest which is a FioExeTest consisting of a
209 single fio job file with an optional setup step.
211 fio_path: location of fio executable
212 fio_job: location of fio job file
213 success: Definition of test success
214 fio_pre_job: fio job for preconditioning
215 fio_pre_success: Definition of test success for fio precon job
216 output_format: normal (default), json, jsonplus, or terse
219 self.fio_job = fio_job
220 self.fio_pre_job = fio_pre_job
221 self.fio_pre_success = fio_pre_success if fio_pre_success else success
222 self.output_format = output_format
223 self.precon_failed = False
224 self.json_data = None
225 self.fio_output = "{0}.output".format(os.path.basename(self.fio_job))
228 "--output-format={0}".format(self.output_format),
229 "--output={0}".format(self.fio_output),
232 FioExeTest.__init__(self, fio_path, self.fio_args, success)
234 def setup(self, artifact_root, testnum):
235 """Setup instance variables for fio job test."""
237 super(FioJobTest, self).setup(artifact_root, testnum)
239 self.command_file = os.path.join(
241 "{0}.command".format(os.path.basename(self.fio_job)))
242 self.stdout_file = os.path.join(
244 "{0}.stdout".format(os.path.basename(self.fio_job)))
245 self.stderr_file = os.path.join(
247 "{0}.stderr".format(os.path.basename(self.fio_job)))
248 self.exitcode_file = os.path.join(
250 "{0}.exitcode".format(os.path.basename(self.fio_job)))
252 def run_pre_job(self):
253 """Run fio job precondition step."""
255 precon = FioJobTest(self.exe_path, self.fio_pre_job,
256 self.fio_pre_success,
257 output_format=self.output_format)
258 precon.setup(self.artifact_root, self.testnum)
260 precon.check_result()
261 self.precon_failed = not precon.passed
262 self.failure_reason = precon.failure_reason
265 """Run fio job test."""
270 if not self.precon_failed:
271 super(FioJobTest, self).run()
273 logging.debug("Test %d: precondition step failed", self.testnum)
276 def get_file(cls, filename):
277 """Safely read a file."""
282 with open(filename, "r") as output_file:
283 file_data = output_file.read()
287 return file_data, success
289 def get_file_fail(self, filename):
290 """Safely read a file and fail the test upon error."""
294 with open(filename, "r") as output_file:
295 file_data = output_file.read()
297 self.failure_reason += " unable to read file {0}".format(filename)
302 def check_result(self):
303 """Check fio job results."""
305 if self.precon_failed:
307 self.failure_reason = "{0} precondition step failed,".format(self.failure_reason)
310 super(FioJobTest, self).check_result()
315 if 'json' not in self.output_format:
318 file_data = self.get_file_fail(os.path.join(self.test_dir, self.fio_output))
323 # Sometimes fio informational messages are included at the top of the
324 # JSON output, especially under Windows. Try to decode output as JSON
325 # data, skipping everything until the first {
327 lines = file_data.splitlines()
328 file_data = '\n'.join(lines[lines.index("{"):])
330 self.json_data = json.loads(file_data)
331 except json.JSONDecodeError:
332 self.failure_reason = "{0} unable to decode JSON data,".format(self.failure_reason)
336 class FioJobTest_t0005(FioJobTest):
337 """Test consists of fio test job t0005
338 Confirm that read['io_kbytes'] == write['io_kbytes'] == 102400"""
340 def check_result(self):
341 super(FioJobTest_t0005, self).check_result()
346 if self.json_data['jobs'][0]['read']['io_kbytes'] != 102400:
347 self.failure_reason = "{0} bytes read mismatch,".format(self.failure_reason)
349 if self.json_data['jobs'][0]['write']['io_kbytes'] != 102400:
350 self.failure_reason = "{0} bytes written mismatch,".format(self.failure_reason)
354 class FioJobTest_t0006(FioJobTest):
355 """Test consists of fio test job t0006
356 Confirm that read['io_kbytes'] ~ 2*write['io_kbytes']"""
358 def check_result(self):
359 super(FioJobTest_t0006, self).check_result()
364 ratio = self.json_data['jobs'][0]['read']['io_kbytes'] \
365 / self.json_data['jobs'][0]['write']['io_kbytes']
366 logging.debug("Test %d: ratio: %f", self.testnum, ratio)
367 if ratio < 1.99 or ratio > 2.01:
368 self.failure_reason = "{0} read/write ratio mismatch,".format(self.failure_reason)
372 class FioJobTest_t0007(FioJobTest):
373 """Test consists of fio test job t0007
374 Confirm that read['io_kbytes'] = 87040"""
376 def check_result(self):
377 super(FioJobTest_t0007, self).check_result()
382 if self.json_data['jobs'][0]['read']['io_kbytes'] != 87040:
383 self.failure_reason = "{0} bytes read mismatch,".format(self.failure_reason)
387 class FioJobTest_t0008(FioJobTest):
388 """Test consists of fio test job t0008
389 Confirm that read['io_kbytes'] = 32768 and that
390 write['io_kbytes'] ~ 16568
392 I did runs with fio-ae2fafc8 and saw write['io_kbytes'] values of
393 16585, 16588. With two runs of fio-3.16 I obtained 16568"""
395 def check_result(self):
396 super(FioJobTest_t0008, self).check_result()
401 ratio = self.json_data['jobs'][0]['write']['io_kbytes'] / 16568
402 logging.debug("Test %d: ratio: %f", self.testnum, ratio)
404 if ratio < 0.99 or ratio > 1.01:
405 self.failure_reason = "{0} bytes written mismatch,".format(self.failure_reason)
407 if self.json_data['jobs'][0]['read']['io_kbytes'] != 32768:
408 self.failure_reason = "{0} bytes read mismatch,".format(self.failure_reason)
412 class FioJobTest_t0009(FioJobTest):
413 """Test consists of fio test job t0009
414 Confirm that runtime >= 60s"""
416 def check_result(self):
417 super(FioJobTest_t0009, self).check_result()
422 logging.debug('Test %d: elapsed: %d', self.testnum, self.json_data['jobs'][0]['elapsed'])
424 if self.json_data['jobs'][0]['elapsed'] < 60:
425 self.failure_reason = "{0} elapsed time mismatch,".format(self.failure_reason)
429 class FioJobTest_t0012(FioJobTest):
430 """Test consists of fio test job t0012
431 Confirm ratios of job iops are 1:5:10
432 job1,job2,job3 respectively"""
434 def check_result(self):
435 super(FioJobTest_t0012, self).check_result()
441 for i in range(1, 4):
442 filename = os.path.join(self.test_dir, "{0}_iops.{1}.log".format(os.path.basename(
444 file_data = self.get_file_fail(filename)
448 iops_files.append(file_data.splitlines())
450 # there are 9 samples for job1 and job2, 4 samples for job3
455 iops1 = iops1 + float(iops_files[0][i].split(',')[1])
456 iops2 = iops2 + float(iops_files[1][i].split(',')[1])
457 iops3 = iops3 + float(iops_files[2][i].split(',')[1])
461 logging.debug("sample {0}: job1 iops={1} job2 iops={2} job3 iops={3} " \
462 "job3/job2={4:.3f} job3/job1={5:.3f}".format(i, iops1, iops2, iops3, ratio1,
465 # test job1 and job2 succeeded to recalibrate
466 if ratio1 < 1 or ratio1 > 3 or ratio2 < 7 or ratio2 > 13:
467 self.failure_reason += " iops ratio mismatch iops1={0} iops2={1} iops3={2} " \
468 "expected r1~2 r2~10 got r1={3:.3f} r2={4:.3f},".format(iops1, iops2, iops3,
474 class FioJobTest_t0014(FioJobTest):
475 """Test consists of fio test job t0014
476 Confirm that job1_iops / job2_iops ~ 1:2 for entire duration
477 and that job1_iops / job3_iops ~ 1:3 for first half of duration.
479 The test is about making sure the flow feature can
480 re-calibrate the activity dynamically"""
482 def check_result(self):
483 super(FioJobTest_t0014, self).check_result()
489 for i in range(1, 4):
490 filename = os.path.join(self.test_dir, "{0}_iops.{1}.log".format(os.path.basename(
492 file_data = self.get_file_fail(filename)
496 iops_files.append(file_data.splitlines())
498 # there are 9 samples for job1 and job2, 4 samples for job3
504 iops3 = iops3 + float(iops_files[2][i].split(',')[1])
506 ratio1 = iops1 / iops2
507 ratio2 = iops1 / iops3
510 if ratio1 < 0.43 or ratio1 > 0.57 or ratio2 < 0.21 or ratio2 > 0.45:
511 self.failure_reason += " iops ratio mismatch iops1={0} iops2={1} iops3={2} " \
512 "expected r1~0.5 r2~0.33 got r1={3:.3f} r2={4:.3f},".format(
513 iops1, iops2, iops3, ratio1, ratio2)
516 iops1 = iops1 + float(iops_files[0][i].split(',')[1])
517 iops2 = iops2 + float(iops_files[1][i].split(',')[1])
521 logging.debug("sample {0}: job1 iops={1} job2 iops={2} job3 iops={3} " \
522 "job1/job2={4:.3f} job1/job3={5:.3f}".format(i, iops1, iops2, iops3,
525 # test job1 and job2 succeeded to recalibrate
526 if ratio1 < 0.43 or ratio1 > 0.57:
527 self.failure_reason += " iops ratio mismatch iops1={0} iops2={1} expected ratio~0.5 " \
528 "got ratio={2:.3f},".format(iops1, iops2, ratio1)
533 class FioJobTest_t0015(FioJobTest):
534 """Test consists of fio test jobs t0015 and t0016
535 Confirm that mean(slat) + mean(clat) = mean(tlat)"""
537 def check_result(self):
538 super(FioJobTest_t0015, self).check_result()
543 slat = self.json_data['jobs'][0]['read']['slat_ns']['mean']
544 clat = self.json_data['jobs'][0]['read']['clat_ns']['mean']
545 tlat = self.json_data['jobs'][0]['read']['lat_ns']['mean']
546 logging.debug('Test %d: slat %f, clat %f, tlat %f', self.testnum, slat, clat, tlat)
548 if abs(slat + clat - tlat) > 1:
549 self.failure_reason = "{0} slat {1} + clat {2} = {3} != tlat {4},".format(
550 self.failure_reason, slat, clat, slat+clat, tlat)
554 class FioJobTest_t0019(FioJobTest):
555 """Test consists of fio test job t0019
556 Confirm that all offsets were touched sequentially"""
558 def check_result(self):
559 super(FioJobTest_t0019, self).check_result()
561 bw_log_filename = os.path.join(self.test_dir, "test_bw.log")
562 file_data = self.get_file_fail(bw_log_filename)
566 log_lines = file_data.split('\n')
569 for line in log_lines:
570 if len(line.strip()) == 0:
572 cur = int(line.split(',')[4])
573 if cur - prev != 4096:
575 self.failure_reason = "offsets {0}, {1} not sequential".format(prev, cur)
581 self.failure_reason = "unexpected last offset {0}".format(cur)
584 class FioJobTest_t0020(FioJobTest):
585 """Test consists of fio test jobs t0020 and t0021
586 Confirm that almost all offsets were touched non-sequentially"""
588 def check_result(self):
589 super(FioJobTest_t0020, self).check_result()
591 bw_log_filename = os.path.join(self.test_dir, "test_bw.log")
592 file_data = self.get_file_fail(bw_log_filename)
596 log_lines = file_data.split('\n')
601 prev = int(log_lines[0].split(',')[4])
602 for line in log_lines[1:]:
603 offsets.add(prev/4096)
604 if len(line.strip()) == 0:
606 cur = int(line.split(',')[4])
607 if cur - prev == 4096:
611 # 10 is an arbitrary threshold
614 self.failure_reason = "too many ({0}) consecutive offsets".format(seq_count)
616 if len(offsets) != 256:
618 self.failure_reason += " number of offsets is {0} instead of 256".format(len(offsets))
623 self.failure_reason += " missing offset {0}".format(i*4096)
626 class FioJobTest_t0022(FioJobTest):
627 """Test consists of fio test job t0022"""
629 def check_result(self):
630 super(FioJobTest_t0022, self).check_result()
632 bw_log_filename = os.path.join(self.test_dir, "test_bw.log")
633 file_data = self.get_file_fail(bw_log_filename)
637 log_lines = file_data.split('\n')
644 prev = int(log_lines[0].split(',')[4])
645 for line in log_lines[1:]:
647 if len(line.strip()) == 0:
649 cur = int(line.split(',')[4])
654 # 10 is an arbitrary threshold
657 self.failure_reason = "too many ({0}) consecutive offsets".format(seq_count)
659 if len(offsets) == filesize/bs:
661 self.failure_reason += " no duplicate offsets found with norandommap=1"
664 class FioJobTest_t0023(FioJobTest):
665 """Test consists of fio test job t0023 randtrimwrite test."""
667 def check_trimwrite(self, filename):
668 """Make sure that trims are followed by writes of the same size at the same offset."""
670 bw_log_filename = os.path.join(self.test_dir, filename)
671 file_data = self.get_file_fail(bw_log_filename)
675 log_lines = file_data.split('\n')
678 for line in log_lines:
679 if len(line.strip()) == 0:
681 vals = line.split(',')
684 offset = int(vals[4])
688 self.failure_reason += " {0}: write not preceeded by trim: {1}".format(
689 bw_log_filename, line)
694 self.failure_reason += " {0}: trim not preceeded by write: {1}".format(
695 bw_log_filename, line)
700 self.failure_reason += " {0}: block size does not match: {1}".format(
701 bw_log_filename, line)
703 if prev_offset != offset:
705 self.failure_reason += " {0}: offset does not match: {1}".format(
706 bw_log_filename, line)
713 def check_all_offsets(self, filename, sectorsize, filesize):
714 """Make sure all offsets were touched."""
716 file_data = self.get_file_fail(os.path.join(self.test_dir, filename))
720 log_lines = file_data.split('\n')
724 for line in log_lines:
725 if len(line.strip()) == 0:
727 vals = line.split(',')
729 offset = int(vals[4])
730 if offset % sectorsize != 0:
732 self.failure_reason += " {0}: offset {1} not a multiple of sector size {2}".format(
733 filename, offset, sectorsize)
735 if bs % sectorsize != 0:
737 self.failure_reason += " {0}: block size {1} not a multiple of sector size " \
738 "{2}".format(filename, bs, sectorsize)
740 for i in range(int(bs/sectorsize)):
741 offsets.add(offset/sectorsize + i)
743 if len(offsets) != filesize/sectorsize:
745 self.failure_reason += " {0}: only {1} offsets touched; expected {2}".format(
746 filename, len(offsets), filesize/sectorsize)
748 logging.debug("%s: %d sectors touched", filename, len(offsets))
751 def check_result(self):
752 super(FioJobTest_t0023, self).check_result()
756 self.check_trimwrite("basic_bw.log")
757 self.check_trimwrite("bs_bw.log")
758 self.check_trimwrite("bsrange_bw.log")
759 self.check_trimwrite("bssplit_bw.log")
760 self.check_trimwrite("basic_no_rm_bw.log")
761 self.check_trimwrite("bs_no_rm_bw.log")
762 self.check_trimwrite("bsrange_no_rm_bw.log")
763 self.check_trimwrite("bssplit_no_rm_bw.log")
765 self.check_all_offsets("basic_bw.log", 4096, filesize)
766 self.check_all_offsets("bs_bw.log", 8192, filesize)
767 self.check_all_offsets("bsrange_bw.log", 512, filesize)
768 self.check_all_offsets("bssplit_bw.log", 512, filesize)
771 class FioJobTest_t0024(FioJobTest_t0023):
772 """Test consists of fio test job t0024 trimwrite test."""
774 def check_result(self):
775 # call FioJobTest_t0023's parent to skip checks done by t0023
776 super(FioJobTest_t0023, self).check_result()
780 self.check_trimwrite("basic_bw.log")
781 self.check_trimwrite("bs_bw.log")
782 self.check_trimwrite("bsrange_bw.log")
783 self.check_trimwrite("bssplit_bw.log")
785 self.check_all_offsets("basic_bw.log", 4096, filesize)
786 self.check_all_offsets("bs_bw.log", 8192, filesize)
787 self.check_all_offsets("bsrange_bw.log", 512, filesize)
788 self.check_all_offsets("bssplit_bw.log", 512, filesize)
791 class FioJobTest_t0025(FioJobTest):
792 """Test experimental verify read backs written data pattern."""
793 def check_result(self):
794 super(FioJobTest_t0025, self).check_result()
799 if self.json_data['jobs'][0]['read']['io_kbytes'] != 128:
802 class FioJobTest_t0027(FioJobTest):
803 def setup(self, *args, **kws):
804 super(FioJobTest_t0027, self).setup(*args, **kws)
805 self.pattern_file = os.path.join(self.test_dir, "t0027.pattern")
806 self.output_file = os.path.join(self.test_dir, "t0027file")
807 self.pattern = os.urandom(16 << 10)
808 with open(self.pattern_file, "wb") as f:
809 f.write(self.pattern)
811 def check_result(self):
812 super(FioJobTest_t0027, self).check_result()
817 with open(self.output_file, "rb") as f:
820 if data != self.pattern:
823 class FioJobTest_iops_rate(FioJobTest):
824 """Test consists of fio test job t0009
825 Confirm that job0 iops == 1000
826 and that job1_iops / job0_iops ~ 8
827 With two runs of fio-3.16 I observed a ratio of 8.3"""
829 def check_result(self):
830 super(FioJobTest_iops_rate, self).check_result()
835 iops1 = self.json_data['jobs'][0]['read']['iops']
836 logging.debug("Test %d: iops1: %f", self.testnum, iops1)
837 iops2 = self.json_data['jobs'][1]['read']['iops']
838 logging.debug("Test %d: iops2: %f", self.testnum, iops2)
839 ratio = iops2 / iops1
840 logging.debug("Test %d: ratio: %f", self.testnum, ratio)
842 if iops1 < 950 or iops1 > 1050:
843 self.failure_reason = "{0} iops value mismatch,".format(self.failure_reason)
846 if ratio < 6 or ratio > 10:
847 self.failure_reason = "{0} iops ratio mismatch,".format(self.failure_reason)
851 class Requirements():
852 """Requirements consists of multiple run environment characteristics.
853 These are to determine if a particular test can be run"""
866 def __init__(self, fio_root):
867 Requirements._not_macos = platform.system() != "Darwin"
868 Requirements._not_windows = platform.system() != "Windows"
869 Requirements._linux = platform.system() == "Linux"
871 if Requirements._linux:
872 config_file = os.path.join(fio_root, "config-host.h")
873 contents, success = FioJobTest.get_file(config_file)
875 print("Unable to open {0} to check requirements".format(config_file))
876 Requirements._zbd = True
878 Requirements._zbd = "CONFIG_HAS_BLKZONED" in contents
879 Requirements._libaio = "CONFIG_LIBAIO" in contents
881 contents, success = FioJobTest.get_file("/proc/kallsyms")
883 print("Unable to open '/proc/kallsyms' to probe for io_uring support")
885 Requirements._io_uring = "io_uring_setup" in contents
887 Requirements._root = (os.geteuid() == 0)
888 if Requirements._zbd and Requirements._root:
890 subprocess.run(["modprobe", "null_blk"],
891 stdout=subprocess.PIPE,
892 stderr=subprocess.PIPE)
893 if os.path.exists("/sys/module/null_blk/parameters/zoned"):
894 Requirements._zoned_nullb = True
898 if platform.system() == "Windows":
899 utest_exe = "unittest.exe"
901 utest_exe = "unittest"
902 unittest_path = os.path.join(fio_root, "unittests", utest_exe)
903 Requirements._unittests = os.path.exists(unittest_path)
905 Requirements._cpucount4 = multiprocessing.cpu_count() >= 4
907 req_list = [Requirements.linux,
909 Requirements.io_uring,
912 Requirements.zoned_nullb,
913 Requirements.not_macos,
914 Requirements.not_windows,
915 Requirements.unittests,
916 Requirements.cpucount4]
919 logging.debug("Requirements: Requirement '%s' met? %s", desc, value)
923 """Are we running on Linux?"""
924 return Requirements._linux, "Linux required"
928 """Is libaio available?"""
929 return Requirements._libaio, "libaio required"
933 """Is io_uring available?"""
934 return Requirements._io_uring, "io_uring required"
938 """Is ZBD support available?"""
939 return Requirements._zbd, "Zoned block device support required"
943 """Are we running as root?"""
944 return Requirements._root, "root required"
947 def zoned_nullb(cls):
948 """Are zoned null block devices available?"""
949 return Requirements._zoned_nullb, "Zoned null block device support required"
953 """Are we running on a platform other than macOS?"""
954 return Requirements._not_macos, "platform other than macOS required"
957 def not_windows(cls):
958 """Are we running on a platform other than Windws?"""
959 return Requirements._not_windows, "platform other than Windows required"
963 """Were unittests built?"""
964 return Requirements._unittests, "Unittests support required"
968 """Do we have at least 4 CPUs?"""
969 return Requirements._cpucount4, "4+ CPUs required"
974 'stderr_empty': True,
978 'zero_return': False,
979 'stderr_empty': False,
984 'stderr_empty': False,
990 'test_class': FioJobTest,
991 'job': 't0001-52c58027.fio',
992 'success': SUCCESS_DEFAULT,
999 'test_class': FioJobTest,
1000 'job': 't0002-13af05ae-post.fio',
1001 'success': SUCCESS_DEFAULT,
1002 'pre_job': 't0002-13af05ae-pre.fio',
1003 'pre_success': None,
1004 'requirements': [Requirements.linux, Requirements.libaio],
1008 'test_class': FioJobTest,
1009 'job': 't0003-0ae2c6e1-post.fio',
1010 'success': SUCCESS_NONZERO,
1011 'pre_job': 't0003-0ae2c6e1-pre.fio',
1012 'pre_success': SUCCESS_DEFAULT,
1013 'requirements': [Requirements.linux, Requirements.libaio],
1017 'test_class': FioJobTest,
1018 'job': 't0004-8a99fdf6.fio',
1019 'success': SUCCESS_DEFAULT,
1021 'pre_success': None,
1022 'requirements': [Requirements.linux, Requirements.libaio],
1026 'test_class': FioJobTest_t0005,
1027 'job': 't0005-f7078f7b.fio',
1028 'success': SUCCESS_DEFAULT,
1030 'pre_success': None,
1031 'output_format': 'json',
1032 'requirements': [Requirements.not_windows],
1036 'test_class': FioJobTest_t0006,
1037 'job': 't0006-82af2a7c.fio',
1038 'success': SUCCESS_DEFAULT,
1040 'pre_success': None,
1041 'output_format': 'json',
1042 'requirements': [Requirements.linux, Requirements.libaio],
1046 'test_class': FioJobTest_t0007,
1047 'job': 't0007-37cf9e3c.fio',
1048 'success': SUCCESS_DEFAULT,
1050 'pre_success': None,
1051 'output_format': 'json',
1056 'test_class': FioJobTest_t0008,
1057 'job': 't0008-ae2fafc8.fio',
1058 'success': SUCCESS_DEFAULT,
1060 'pre_success': None,
1061 'output_format': 'json',
1066 'test_class': FioJobTest_t0009,
1067 'job': 't0009-f8b0bd10.fio',
1068 'success': SUCCESS_DEFAULT,
1070 'pre_success': None,
1071 'output_format': 'json',
1072 'requirements': [Requirements.not_macos,
1073 Requirements.cpucount4],
1074 # mac os does not support CPU affinity
1078 'test_class': FioJobTest,
1079 'job': 't0010-b7aae4ba.fio',
1080 'success': SUCCESS_DEFAULT,
1082 'pre_success': None,
1087 'test_class': FioJobTest_iops_rate,
1088 'job': 't0011-5d2788d5.fio',
1089 'success': SUCCESS_DEFAULT,
1091 'pre_success': None,
1092 'output_format': 'json',
1097 'test_class': FioJobTest_t0012,
1099 'success': SUCCESS_DEFAULT,
1101 'pre_success': None,
1102 'output_format': 'json',
1107 'test_class': FioJobTest,
1109 'success': SUCCESS_DEFAULT,
1111 'pre_success': None,
1112 'output_format': 'json',
1117 'test_class': FioJobTest_t0014,
1119 'success': SUCCESS_DEFAULT,
1121 'pre_success': None,
1122 'output_format': 'json',
1127 'test_class': FioJobTest_t0015,
1128 'job': 't0015-e78980ff.fio',
1129 'success': SUCCESS_DEFAULT,
1131 'pre_success': None,
1132 'output_format': 'json',
1133 'requirements': [Requirements.linux, Requirements.libaio],
1137 'test_class': FioJobTest_t0015,
1138 'job': 't0016-d54ae22.fio',
1139 'success': SUCCESS_DEFAULT,
1141 'pre_success': None,
1142 'output_format': 'json',
1147 'test_class': FioJobTest_t0015,
1149 'success': SUCCESS_DEFAULT,
1151 'pre_success': None,
1152 'output_format': 'json',
1153 'requirements': [Requirements.not_windows],
1157 'test_class': FioJobTest,
1159 'success': SUCCESS_DEFAULT,
1161 'pre_success': None,
1162 'requirements': [Requirements.linux, Requirements.io_uring],
1166 'test_class': FioJobTest_t0019,
1168 'success': SUCCESS_DEFAULT,
1170 'pre_success': None,
1175 'test_class': FioJobTest_t0020,
1177 'success': SUCCESS_DEFAULT,
1179 'pre_success': None,
1184 'test_class': FioJobTest_t0020,
1186 'success': SUCCESS_DEFAULT,
1188 'pre_success': None,
1193 'test_class': FioJobTest_t0022,
1195 'success': SUCCESS_DEFAULT,
1197 'pre_success': None,
1202 'test_class': FioJobTest_t0023,
1204 'success': SUCCESS_DEFAULT,
1206 'pre_success': None,
1211 'test_class': FioJobTest_t0024,
1213 'success': SUCCESS_DEFAULT,
1215 'pre_success': None,
1220 'test_class': FioJobTest_t0025,
1222 'success': SUCCESS_DEFAULT,
1224 'pre_success': None,
1225 'output_format': 'json',
1230 'test_class': FioJobTest,
1232 'success': SUCCESS_DEFAULT,
1234 'pre_success': None,
1235 'requirements': [Requirements.not_windows],
1239 'test_class': FioJobTest_t0027,
1241 'success': SUCCESS_DEFAULT,
1243 'pre_success': None,
1248 'test_class': FioExeTest,
1251 'success': SUCCESS_DEFAULT,
1256 'test_class': FioExeTest,
1259 'success': SUCCESS_DEFAULT,
1264 'test_class': FioExeTest,
1265 'exe': 't/lfsr-test',
1266 'parameters': ['0xFFFFFF', '0', '0', 'verify'],
1267 'success': SUCCESS_STDERR,
1272 'test_class': FioExeTest,
1273 'exe': 't/readonly.py',
1274 'parameters': ['-f', '{fio_path}'],
1275 'success': SUCCESS_DEFAULT,
1280 'test_class': FioExeTest,
1281 'exe': 't/steadystate_tests.py',
1282 'parameters': ['{fio_path}'],
1283 'success': SUCCESS_DEFAULT,
1288 'test_class': FioExeTest,
1291 'success': SUCCESS_STDERR,
1296 'test_class': FioExeTest,
1297 'exe': 't/strided.py',
1298 'parameters': ['{fio_path}'],
1299 'success': SUCCESS_DEFAULT,
1304 'test_class': FioExeTest,
1305 'exe': 't/zbd/run-tests-against-nullb',
1306 'parameters': ['-s', '1'],
1307 'success': SUCCESS_DEFAULT,
1308 'requirements': [Requirements.linux, Requirements.zbd,
1313 'test_class': FioExeTest,
1314 'exe': 't/zbd/run-tests-against-nullb',
1315 'parameters': ['-s', '2'],
1316 'success': SUCCESS_DEFAULT,
1317 'requirements': [Requirements.linux, Requirements.zbd,
1318 Requirements.root, Requirements.zoned_nullb],
1322 'test_class': FioExeTest,
1323 'exe': 'unittests/unittest',
1325 'success': SUCCESS_DEFAULT,
1326 'requirements': [Requirements.unittests],
1330 'test_class': FioExeTest,
1331 'exe': 't/latency_percentiles.py',
1332 'parameters': ['-f', '{fio_path}'],
1333 'success': SUCCESS_DEFAULT,
1338 'test_class': FioExeTest,
1339 'exe': 't/jsonplus2csv_test.py',
1340 'parameters': ['-f', '{fio_path}'],
1341 'success': SUCCESS_DEFAULT,
1346 'test_class': FioExeTest,
1347 'exe': 't/log_compression.py',
1348 'parameters': ['-f', '{fio_path}'],
1349 'success': SUCCESS_DEFAULT,
1356 """Parse command-line arguments."""
1358 parser = argparse.ArgumentParser()
1359 parser.add_argument('-r', '--fio-root',
1360 help='fio root path')
1361 parser.add_argument('-f', '--fio',
1362 help='path to fio executable (e.g., ./fio)')
1363 parser.add_argument('-a', '--artifact-root',
1364 help='artifact root directory')
1365 parser.add_argument('-s', '--skip', nargs='+', type=int,
1366 help='list of test(s) to skip')
1367 parser.add_argument('-o', '--run-only', nargs='+', type=int,
1368 help='list of test(s) to run, skipping all others')
1369 parser.add_argument('-d', '--debug', action='store_true',
1370 help='provide debug output')
1371 parser.add_argument('-k', '--skip-req', action='store_true',
1372 help='skip requirements checking')
1373 parser.add_argument('-p', '--pass-through', action='append',
1374 help='pass-through an argument to an executable test')
1375 args = parser.parse_args()
1385 logging.basicConfig(level=logging.DEBUG)
1387 logging.basicConfig(level=logging.INFO)
1390 if args.pass_through:
1391 for arg in args.pass_through:
1393 print("Invalid --pass-through argument '%s'" % arg)
1394 print("Syntax for --pass-through is TESTNUMBER:ARGUMENT")
1396 split = arg.split(":", 1)
1397 pass_through[int(split[0])] = split[1]
1398 logging.debug("Pass-through arguments: %s", pass_through)
1401 fio_root = args.fio_root
1403 fio_root = str(Path(__file__).absolute().parent.parent)
1404 print("fio root is %s" % fio_root)
1409 if platform.system() == "Windows":
1413 fio_path = os.path.join(fio_root, fio_exe)
1414 print("fio path is %s" % fio_path)
1415 if not shutil.which(fio_path):
1416 print("Warning: fio executable not found")
1418 artifact_root = args.artifact_root if args.artifact_root else \
1419 "fio-test-{0}".format(time.strftime("%Y%m%d-%H%M%S"))
1420 os.mkdir(artifact_root)
1421 print("Artifact directory is %s" % artifact_root)
1423 if not args.skip_req:
1424 req = Requirements(fio_root)
1430 for config in TEST_LIST:
1431 if (args.skip and config['test_id'] in args.skip) or \
1432 (args.run_only and config['test_id'] not in args.run_only):
1433 skipped = skipped + 1
1434 print("Test {0} SKIPPED (User request)".format(config['test_id']))
1437 if issubclass(config['test_class'], FioJobTest):
1438 if config['pre_job']:
1439 fio_pre_job = os.path.join(fio_root, 't', 'jobs',
1443 if config['pre_success']:
1444 fio_pre_success = config['pre_success']
1446 fio_pre_success = None
1447 if 'output_format' in config:
1448 output_format = config['output_format']
1450 output_format = 'normal'
1451 test = config['test_class'](
1453 os.path.join(fio_root, 't', 'jobs', config['job']),
1455 fio_pre_job=fio_pre_job,
1456 fio_pre_success=fio_pre_success,
1457 output_format=output_format)
1458 desc = config['job']
1459 elif issubclass(config['test_class'], FioExeTest):
1460 exe_path = os.path.join(fio_root, config['exe'])
1461 if config['parameters']:
1462 parameters = [p.format(fio_path=fio_path) for p in config['parameters']]
1465 if Path(exe_path).suffix == '.py' and platform.system() == "Windows":
1466 parameters.insert(0, exe_path)
1467 exe_path = "python.exe"
1468 if config['test_id'] in pass_through:
1469 parameters += pass_through[config['test_id']].split()
1470 test = config['test_class'](exe_path, parameters,
1472 desc = config['exe']
1474 print("Test {0} FAILED: unable to process test config".format(config['test_id']))
1478 if not args.skip_req:
1480 for req in config['requirements']:
1481 reqs_met, reason = req()
1482 logging.debug("Test %d: Requirement '%s' met? %s", config['test_id'], reason,
1487 print("Test {0} SKIPPED ({1}) {2}".format(config['test_id'], reason, desc))
1488 skipped = skipped + 1
1492 test.setup(artifact_root, config['test_id'])
1495 except KeyboardInterrupt:
1497 except Exception as e:
1499 test.failure_reason += str(e)
1500 logging.debug("Test %d exception:\n%s\n", config['test_id'], traceback.format_exc())
1505 result = "FAILED: {0}".format(test.failure_reason)
1507 contents, _ = FioJobTest.get_file(test.stderr_file)
1508 logging.debug("Test %d: stderr:\n%s", config['test_id'], contents)
1509 contents, _ = FioJobTest.get_file(test.stdout_file)
1510 logging.debug("Test %d: stdout:\n%s", config['test_id'], contents)
1511 print("Test {0} {1} {2}".format(config['test_id'], result, desc))
1513 print("{0} test(s) passed, {1} failed, {2} skipped".format(passed, failed, skipped))
1518 if __name__ == '__main__':