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)
50 from pathlib import Path
51 from statsmodels.sandbox.stats.runs import runstest_1samp
52 from fiotestlib import FioExeTest, FioJobFileTest, run_fio_tests
53 from fiotestcommon import *
56 class FioJobFileTest_t0005(FioJobFileTest):
57 """Test consists of fio test job t0005
58 Confirm that read['io_kbytes'] == write['io_kbytes'] == 102400"""
60 def check_result(self):
61 super().check_result()
66 if self.json_data['jobs'][0]['read']['io_kbytes'] != 102400:
67 self.failure_reason = f"{self.failure_reason} bytes read mismatch,"
69 if self.json_data['jobs'][0]['write']['io_kbytes'] != 102400:
70 self.failure_reason = f"{self.failure_reason} bytes written mismatch,"
74 class FioJobFileTest_t0006(FioJobFileTest):
75 """Test consists of fio test job t0006
76 Confirm that read['io_kbytes'] ~ 2*write['io_kbytes']"""
78 def check_result(self):
79 super().check_result()
84 ratio = self.json_data['jobs'][0]['read']['io_kbytes'] \
85 / self.json_data['jobs'][0]['write']['io_kbytes']
86 logging.debug("Test %d: ratio: %f", self.testnum, ratio)
87 if ratio < 1.99 or ratio > 2.01:
88 self.failure_reason = f"{self.failure_reason} read/write ratio mismatch,"
92 class FioJobFileTest_t0007(FioJobFileTest):
93 """Test consists of fio test job t0007
94 Confirm that read['io_kbytes'] = 87040"""
96 def check_result(self):
97 super().check_result()
102 if self.json_data['jobs'][0]['read']['io_kbytes'] != 87040:
103 self.failure_reason = f"{self.failure_reason} bytes read mismatch,"
107 class FioJobFileTest_t0008(FioJobFileTest):
108 """Test consists of fio test job t0008
109 Confirm that read['io_kbytes'] = 32768 and that
110 write['io_kbytes'] ~ 16384
112 This is a 50/50 seq read/write workload. Since fio flips a coin to
113 determine whether to issue a read or a write, total bytes written will not
114 be exactly 16384K. But total bytes read will be exactly 32768K because
115 reads will include the initial phase as well as the verify phase where all
116 the blocks originally written will be read."""
118 def check_result(self):
119 super().check_result()
124 ratio = self.json_data['jobs'][0]['write']['io_kbytes'] / 16384
125 logging.debug("Test %d: ratio: %f", self.testnum, ratio)
127 if ratio < 0.97 or ratio > 1.03:
128 self.failure_reason = f"{self.failure_reason} bytes written mismatch,"
130 if self.json_data['jobs'][0]['read']['io_kbytes'] != 32768:
131 self.failure_reason = f"{self.failure_reason} bytes read mismatch,"
135 class FioJobFileTest_t0009(FioJobFileTest):
136 """Test consists of fio test job t0009
137 Confirm that runtime >= 60s"""
139 def check_result(self):
140 super().check_result()
145 logging.debug('Test %d: elapsed: %d', self.testnum, self.json_data['jobs'][0]['elapsed'])
147 if self.json_data['jobs'][0]['elapsed'] < 60:
148 self.failure_reason = f"{self.failure_reason} elapsed time mismatch,"
152 class FioJobFileTest_t0012(FioJobFileTest):
153 """Test consists of fio test job t0012
154 Confirm ratios of job iops are 1:5:10
155 job1,job2,job3 respectively"""
157 def check_result(self):
158 super().check_result()
164 for i in range(1, 4):
165 filename = os.path.join(self.paths['test_dir'], "{0}_iops.{1}.log".format(os.path.basename(
167 file_data = self.get_file_fail(filename)
171 iops_files.append(file_data.splitlines())
173 # there are 9 samples for job1 and job2, 4 samples for job3
178 iops1 = iops1 + float(iops_files[0][i].split(',')[1])
179 iops2 = iops2 + float(iops_files[1][i].split(',')[1])
180 iops3 = iops3 + float(iops_files[2][i].split(',')[1])
184 logging.debug("sample {0}: job1 iops={1} job2 iops={2} job3 iops={3} " \
185 "job3/job2={4:.3f} job3/job1={5:.3f}".format(i, iops1, iops2, iops3, ratio1,
188 # test job1 and job2 succeeded to recalibrate
189 if ratio1 < 1 or ratio1 > 3 or ratio2 < 7 or ratio2 > 13:
190 self.failure_reason += " iops ratio mismatch iops1={0} iops2={1} iops3={2} " \
191 "expected r1~2 r2~10 got r1={3:.3f} r2={4:.3f},".format(iops1, iops2, iops3,
197 class FioJobFileTest_t0014(FioJobFileTest):
198 """Test consists of fio test job t0014
199 Confirm that job1_iops / job2_iops ~ 1:2 for entire duration
200 and that job1_iops / job3_iops ~ 1:3 for first half of duration.
202 The test is about making sure the flow feature can
203 re-calibrate the activity dynamically"""
205 def check_result(self):
206 super().check_result()
212 for i in range(1, 4):
213 filename = os.path.join(self.paths['test_dir'], "{0}_iops.{1}.log".format(os.path.basename(
215 file_data = self.get_file_fail(filename)
219 iops_files.append(file_data.splitlines())
221 # there are 9 samples for job1 and job2, 4 samples for job3
227 iops3 = iops3 + float(iops_files[2][i].split(',')[1])
229 ratio1 = iops1 / iops2
230 ratio2 = iops1 / iops3
233 if ratio1 < 0.43 or ratio1 > 0.57 or ratio2 < 0.21 or ratio2 > 0.45:
234 self.failure_reason += " iops ratio mismatch iops1={0} iops2={1} iops3={2} " \
235 "expected r1~0.5 r2~0.33 got r1={3:.3f} r2={4:.3f},".format(
236 iops1, iops2, iops3, ratio1, ratio2)
239 iops1 = iops1 + float(iops_files[0][i].split(',')[1])
240 iops2 = iops2 + float(iops_files[1][i].split(',')[1])
244 logging.debug("sample {0}: job1 iops={1} job2 iops={2} job3 iops={3} " \
245 "job1/job2={4:.3f} job1/job3={5:.3f}".format(i, iops1, iops2, iops3,
248 # test job1 and job2 succeeded to recalibrate
249 if ratio1 < 0.43 or ratio1 > 0.57:
250 self.failure_reason += " iops ratio mismatch iops1={0} iops2={1} expected ratio~0.5 " \
251 "got ratio={2:.3f},".format(iops1, iops2, ratio1)
256 class FioJobFileTest_t0015(FioJobFileTest):
257 """Test consists of fio test jobs t0015 and t0016
258 Confirm that mean(slat) + mean(clat) = mean(tlat)"""
260 def check_result(self):
261 super().check_result()
266 slat = self.json_data['jobs'][0]['read']['slat_ns']['mean']
267 clat = self.json_data['jobs'][0]['read']['clat_ns']['mean']
268 tlat = self.json_data['jobs'][0]['read']['lat_ns']['mean']
269 logging.debug('Test %d: slat %f, clat %f, tlat %f', self.testnum, slat, clat, tlat)
271 if abs(slat + clat - tlat) > 1:
272 self.failure_reason = "{0} slat {1} + clat {2} = {3} != tlat {4},".format(
273 self.failure_reason, slat, clat, slat+clat, tlat)
277 class FioJobFileTest_t0019(FioJobFileTest):
278 """Test consists of fio test job t0019
279 Confirm that all offsets were touched sequentially"""
281 def check_result(self):
282 super().check_result()
284 bw_log_filename = os.path.join(self.paths['test_dir'], "test_bw.log")
285 file_data = self.get_file_fail(bw_log_filename)
289 log_lines = file_data.split('\n')
292 for line in log_lines:
293 if len(line.strip()) == 0:
295 cur = int(line.split(',')[4])
296 if cur - prev != 4096:
298 self.failure_reason = f"offsets {prev}, {cur} not sequential"
304 self.failure_reason = f"unexpected last offset {cur}"
307 class FioJobFileTest_t0020(FioJobFileTest):
308 """Test consists of fio test jobs t0020 and t0021
309 Confirm that almost all offsets were touched non-sequentially"""
311 def check_result(self):
312 super().check_result()
314 bw_log_filename = os.path.join(self.paths['test_dir'], "test_bw.log")
315 file_data = self.get_file_fail(bw_log_filename)
319 log_lines = file_data.split('\n')
323 prev = int(log_lines[0].split(',')[4])
324 for line in log_lines[1:]:
325 offsets.append(prev/4096)
326 if len(line.strip()) == 0:
328 cur = int(line.split(',')[4])
331 if len(offsets) != 256:
333 self.failure_reason += f" number of offsets is {len(offsets)} instead of 256"
338 self.failure_reason += f" missing offset {i * 4096}"
340 (_, p) = runstest_1samp(list(offsets))
343 self.failure_reason += f" runs test failed with p = {p}"
346 class FioJobFileTest_t0022(FioJobFileTest):
347 """Test consists of fio test job t0022"""
349 def check_result(self):
350 super().check_result()
352 bw_log_filename = os.path.join(self.paths['test_dir'], "test_bw.log")
353 file_data = self.get_file_fail(bw_log_filename)
357 log_lines = file_data.split('\n')
364 prev = int(log_lines[0].split(',')[4])
365 for line in log_lines[1:]:
367 if len(line.strip()) == 0:
369 cur = int(line.split(',')[4])
374 # 10 is an arbitrary threshold
377 self.failure_reason = f"too many ({seq_count}) consecutive offsets"
379 if len(offsets) == filesize/bs:
381 self.failure_reason += " no duplicate offsets found with norandommap=1"
384 class FioJobFileTest_t0023(FioJobFileTest):
385 """Test consists of fio test job t0023 randtrimwrite test."""
387 def check_trimwrite(self, filename):
388 """Make sure that trims are followed by writes of the same size at the same offset."""
390 bw_log_filename = os.path.join(self.paths['test_dir'], filename)
391 file_data = self.get_file_fail(bw_log_filename)
395 log_lines = file_data.split('\n')
398 for line in log_lines:
399 if len(line.strip()) == 0:
401 vals = line.split(',')
404 offset = int(vals[4])
408 self.failure_reason += " {0}: write not preceeded by trim: {1}".format(
409 bw_log_filename, line)
412 if ddir != 1: # pylint: disable=no-else-break
414 self.failure_reason += " {0}: trim not preceeded by write: {1}".format(
415 bw_log_filename, line)
420 self.failure_reason += " {0}: block size does not match: {1}".format(
421 bw_log_filename, line)
424 if prev_offset != offset:
426 self.failure_reason += " {0}: offset does not match: {1}".format(
427 bw_log_filename, line)
435 def check_all_offsets(self, filename, sectorsize, filesize):
436 """Make sure all offsets were touched."""
438 file_data = self.get_file_fail(os.path.join(self.paths['test_dir'], filename))
442 log_lines = file_data.split('\n')
446 for line in log_lines:
447 if len(line.strip()) == 0:
449 vals = line.split(',')
451 offset = int(vals[4])
452 if offset % sectorsize != 0:
454 self.failure_reason += " {0}: offset {1} not a multiple of sector size {2}".format(
455 filename, offset, sectorsize)
457 if bs % sectorsize != 0:
459 self.failure_reason += " {0}: block size {1} not a multiple of sector size " \
460 "{2}".format(filename, bs, sectorsize)
462 for i in range(int(bs/sectorsize)):
463 offsets.add(offset/sectorsize + i)
465 if len(offsets) != filesize/sectorsize:
467 self.failure_reason += " {0}: only {1} offsets touched; expected {2}".format(
468 filename, len(offsets), filesize/sectorsize)
470 logging.debug("%s: %d sectors touched", filename, len(offsets))
473 def check_result(self):
474 super().check_result()
478 self.check_trimwrite("basic_bw.log")
479 self.check_trimwrite("bs_bw.log")
480 self.check_trimwrite("bsrange_bw.log")
481 self.check_trimwrite("bssplit_bw.log")
482 self.check_trimwrite("basic_no_rm_bw.log")
483 self.check_trimwrite("bs_no_rm_bw.log")
484 self.check_trimwrite("bsrange_no_rm_bw.log")
485 self.check_trimwrite("bssplit_no_rm_bw.log")
487 self.check_all_offsets("basic_bw.log", 4096, filesize)
488 self.check_all_offsets("bs_bw.log", 8192, filesize)
489 self.check_all_offsets("bsrange_bw.log", 512, filesize)
490 self.check_all_offsets("bssplit_bw.log", 512, filesize)
493 class FioJobFileTest_t0024(FioJobFileTest_t0023):
494 """Test consists of fio test job t0024 trimwrite test."""
496 def check_result(self):
497 # call FioJobFileTest_t0023's parent to skip checks done by t0023
498 super(FioJobFileTest_t0023, self).check_result()
502 self.check_trimwrite("basic_bw.log")
503 self.check_trimwrite("bs_bw.log")
504 self.check_trimwrite("bsrange_bw.log")
505 self.check_trimwrite("bssplit_bw.log")
507 self.check_all_offsets("basic_bw.log", 4096, filesize)
508 self.check_all_offsets("bs_bw.log", 8192, filesize)
509 self.check_all_offsets("bsrange_bw.log", 512, filesize)
510 self.check_all_offsets("bssplit_bw.log", 512, filesize)
513 class FioJobFileTest_t0025(FioJobFileTest):
514 """Test experimental verify read backs written data pattern."""
515 def check_result(self):
516 super().check_result()
521 if self.json_data['jobs'][0]['read']['io_kbytes'] != 128:
524 class FioJobFileTest_t0027(FioJobFileTest):
525 def setup(self, *args, **kws):
526 super().setup(*args, **kws)
527 self.pattern_file = os.path.join(self.paths['test_dir'], "t0027.pattern")
528 self.output_file = os.path.join(self.paths['test_dir'], "t0027file")
529 self.pattern = os.urandom(16 << 10)
530 with open(self.pattern_file, "wb") as f:
531 f.write(self.pattern)
533 def check_result(self):
534 super().check_result()
539 with open(self.output_file, "rb") as f:
542 if data != self.pattern:
545 class FioJobFileTest_iops_rate(FioJobFileTest):
546 """Test consists of fio test job t0011
547 Confirm that job0 iops == 1000
548 and that job1_iops / job0_iops ~ 8
549 With two runs of fio-3.16 I observed a ratio of 8.3"""
551 def check_result(self):
552 super().check_result()
557 iops1 = self.json_data['jobs'][0]['read']['iops']
558 logging.debug("Test %d: iops1: %f", self.testnum, iops1)
559 iops2 = self.json_data['jobs'][1]['read']['iops']
560 logging.debug("Test %d: iops2: %f", self.testnum, iops2)
561 ratio = iops2 / iops1
562 logging.debug("Test %d: ratio: %f", self.testnum, ratio)
564 if iops1 < 950 or iops1 > 1050:
565 self.failure_reason = f"{self.failure_reason} iops value mismatch,"
568 if ratio < 6 or ratio > 10:
569 self.failure_reason = f"{self.failure_reason} iops ratio mismatch,"
576 'test_class': FioJobFileTest,
577 'job': 't0001-52c58027.fio',
578 'success': SUCCESS_DEFAULT,
585 'test_class': FioJobFileTest,
586 'job': 't0002-13af05ae-post.fio',
587 'success': SUCCESS_DEFAULT,
588 'pre_job': 't0002-13af05ae-pre.fio',
590 'requirements': [Requirements.linux, Requirements.libaio],
594 'test_class': FioJobFileTest,
595 'job': 't0003-0ae2c6e1-post.fio',
596 'success': SUCCESS_NONZERO,
597 'pre_job': 't0003-0ae2c6e1-pre.fio',
598 'pre_success': SUCCESS_DEFAULT,
599 'requirements': [Requirements.linux, Requirements.libaio],
603 'test_class': FioJobFileTest,
604 'job': 't0004-8a99fdf6.fio',
605 'success': SUCCESS_DEFAULT,
608 'requirements': [Requirements.linux, Requirements.libaio],
612 'test_class': FioJobFileTest_t0005,
613 'job': 't0005-f7078f7b.fio',
614 'success': SUCCESS_DEFAULT,
617 'output_format': 'json',
618 'requirements': [Requirements.not_windows],
622 'test_class': FioJobFileTest_t0006,
623 'job': 't0006-82af2a7c.fio',
624 'success': SUCCESS_DEFAULT,
627 'output_format': 'json',
628 'requirements': [Requirements.linux, Requirements.libaio],
632 'test_class': FioJobFileTest_t0007,
633 'job': 't0007-37cf9e3c.fio',
634 'success': SUCCESS_DEFAULT,
637 'output_format': 'json',
642 'test_class': FioJobFileTest_t0008,
643 'job': 't0008-ae2fafc8.fio',
644 'success': SUCCESS_DEFAULT,
647 'output_format': 'json',
652 'test_class': FioJobFileTest_t0009,
653 'job': 't0009-f8b0bd10.fio',
654 'success': SUCCESS_DEFAULT,
657 'output_format': 'json',
658 'requirements': [Requirements.not_macos,
659 Requirements.cpucount4],
660 # mac os does not support CPU affinity
664 'test_class': FioJobFileTest,
665 'job': 't0010-b7aae4ba.fio',
666 'success': SUCCESS_DEFAULT,
673 'test_class': FioJobFileTest_iops_rate,
674 'job': 't0011-5d2788d5.fio',
675 'success': SUCCESS_DEFAULT,
678 'output_format': 'json',
683 'test_class': FioJobFileTest_t0012,
685 'success': SUCCESS_DEFAULT,
688 'output_format': 'json',
693 'test_class': FioJobFileTest,
695 'success': SUCCESS_DEFAULT,
698 'output_format': 'json',
703 'test_class': FioJobFileTest_t0014,
705 'success': SUCCESS_DEFAULT,
708 'output_format': 'json',
713 'test_class': FioJobFileTest_t0015,
714 'job': 't0015-e78980ff.fio',
715 'success': SUCCESS_DEFAULT,
718 'output_format': 'json',
719 'requirements': [Requirements.linux, Requirements.libaio],
723 'test_class': FioJobFileTest_t0015,
724 'job': 't0016-d54ae22.fio',
725 'success': SUCCESS_DEFAULT,
728 'output_format': 'json',
733 'test_class': FioJobFileTest_t0015,
735 'success': SUCCESS_DEFAULT,
738 'output_format': 'json',
739 'requirements': [Requirements.not_windows],
743 'test_class': FioJobFileTest,
745 'success': SUCCESS_DEFAULT,
748 'requirements': [Requirements.linux, Requirements.io_uring],
752 'test_class': FioJobFileTest_t0019,
754 'success': SUCCESS_DEFAULT,
761 'test_class': FioJobFileTest_t0020,
763 'success': SUCCESS_DEFAULT,
770 'test_class': FioJobFileTest_t0020,
772 'success': SUCCESS_DEFAULT,
779 'test_class': FioJobFileTest_t0022,
781 'success': SUCCESS_DEFAULT,
788 'test_class': FioJobFileTest_t0023,
790 'success': SUCCESS_DEFAULT,
797 'test_class': FioJobFileTest_t0024,
799 'success': SUCCESS_DEFAULT,
806 'test_class': FioJobFileTest_t0025,
808 'success': SUCCESS_DEFAULT,
811 'output_format': 'json',
816 'test_class': FioJobFileTest,
818 'success': SUCCESS_DEFAULT,
821 'requirements': [Requirements.not_windows],
825 'test_class': FioJobFileTest_t0027,
827 'success': SUCCESS_DEFAULT,
834 'test_class': FioJobFileTest,
835 'job': 't0028-c6cade16.fio',
836 'success': SUCCESS_DEFAULT,
843 'test_class': FioExeTest,
846 'success': SUCCESS_DEFAULT,
851 'test_class': FioExeTest,
854 'success': SUCCESS_DEFAULT,
859 'test_class': FioExeTest,
860 'exe': 't/lfsr-test',
861 'parameters': ['0xFFFFFF', '0', '0', 'verify'],
862 'success': SUCCESS_STDERR,
867 'test_class': FioExeTest,
868 'exe': 't/readonly.py',
869 'parameters': ['-f', '{fio_path}'],
870 'success': SUCCESS_DEFAULT,
875 'test_class': FioExeTest,
876 'exe': 't/steadystate_tests.py',
877 'parameters': ['{fio_path}'],
878 'success': SUCCESS_DEFAULT,
883 'test_class': FioExeTest,
886 'success': SUCCESS_STDERR,
891 'test_class': FioExeTest,
892 'exe': 't/strided.py',
893 'parameters': ['--fio', '{fio_path}'],
894 'success': SUCCESS_DEFAULT,
899 'test_class': FioExeTest,
900 'exe': 't/zbd/run-tests-against-nullb',
901 'parameters': ['-s', '1'],
902 'success': SUCCESS_DEFAULT,
903 'requirements': [Requirements.linux, Requirements.zbd,
908 'test_class': FioExeTest,
909 'exe': 't/zbd/run-tests-against-nullb',
910 'parameters': ['-s', '2'],
911 'success': SUCCESS_DEFAULT,
912 'requirements': [Requirements.linux, Requirements.zbd,
913 Requirements.root, Requirements.zoned_nullb],
917 'test_class': FioExeTest,
918 'exe': 'unittests/unittest',
920 'success': SUCCESS_DEFAULT,
921 'requirements': [Requirements.unittests],
925 'test_class': FioExeTest,
926 'exe': 't/latency_percentiles.py',
927 'parameters': ['-f', '{fio_path}'],
928 'success': SUCCESS_DEFAULT,
933 'test_class': FioExeTest,
934 'exe': 't/jsonplus2csv_test.py',
935 'parameters': ['-f', '{fio_path}'],
936 'success': SUCCESS_DEFAULT,
941 'test_class': FioExeTest,
942 'exe': 't/log_compression.py',
943 'parameters': ['-f', '{fio_path}'],
944 'success': SUCCESS_DEFAULT,
949 'test_class': FioExeTest,
950 'exe': 't/random_seed.py',
951 'parameters': ['-f', '{fio_path}'],
952 'success': SUCCESS_DEFAULT,
957 'test_class': FioExeTest,
958 'exe': 't/nvmept.py',
959 'parameters': ['-f', '{fio_path}', '--dut', '{nvmecdev}'],
960 'success': SUCCESS_DEFAULT,
961 'requirements': [Requirements.linux, Requirements.nvmecdev],
967 """Parse command-line arguments."""
969 parser = argparse.ArgumentParser()
970 parser.add_argument('-r', '--fio-root',
971 help='fio root path')
972 parser.add_argument('-f', '--fio',
973 help='path to fio executable (e.g., ./fio)')
974 parser.add_argument('-a', '--artifact-root',
975 help='artifact root directory')
976 parser.add_argument('-s', '--skip', nargs='+', type=int,
977 help='list of test(s) to skip')
978 parser.add_argument('-o', '--run-only', nargs='+', type=int,
979 help='list of test(s) to run, skipping all others')
980 parser.add_argument('-d', '--debug', action='store_true',
981 help='provide debug output')
982 parser.add_argument('-k', '--skip-req', action='store_true',
983 help='skip requirements checking')
984 parser.add_argument('-p', '--pass-through', action='append',
985 help='pass-through an argument to an executable test')
986 parser.add_argument('--nvmecdev', action='store', default=None,
987 help='NVMe character device for **DESTRUCTIVE** testing (e.g., /dev/ng0n1)')
988 args = parser.parse_args()
998 logging.basicConfig(level=logging.DEBUG)
1000 logging.basicConfig(level=logging.INFO)
1003 if args.pass_through:
1004 for arg in args.pass_through:
1006 print(f"Invalid --pass-through argument '{arg}'")
1007 print("Syntax for --pass-through is TESTNUMBER:ARGUMENT")
1009 split = arg.split(":", 1)
1010 pass_through[int(split[0])] = split[1]
1011 logging.debug("Pass-through arguments: %s", pass_through)
1014 fio_root = args.fio_root
1016 fio_root = str(Path(__file__).absolute().parent.parent)
1017 print(f"fio root is {fio_root}")
1022 if platform.system() == "Windows":
1026 fio_path = os.path.join(fio_root, fio_exe)
1027 print(f"fio path is {fio_path}")
1028 if not shutil.which(fio_path):
1029 print("Warning: fio executable not found")
1031 artifact_root = args.artifact_root if args.artifact_root else \
1032 f"fio-test-{time.strftime('%Y%m%d-%H%M%S')}"
1033 os.mkdir(artifact_root)
1034 print(f"Artifact directory is {artifact_root}")
1036 if not args.skip_req:
1037 Requirements(fio_root, args)
1040 'fio_path': fio_path,
1041 'fio_root': fio_root,
1042 'artifact_root': artifact_root,
1043 'pass_through': pass_through,
1045 _, failed, _ = run_fio_tests(TEST_LIST, test_env, args)
1049 if __name__ == '__main__':