Merge branch 'improvement/fix-warnings-if-NDEBUG-enabled' of https://github.com/dpron...
[fio.git] / t / run-fio-tests.py
1 #!/usr/bin/env python3
2 # SPDX-License-Identifier: GPL-2.0-only
3 #
4 # Copyright (c) 2019 Western Digital Corporation or its affiliates.
5 #
6 """
7 # run-fio-tests.py
8 #
9 # Automate running of fio tests
10 #
11 # USAGE
12 # python3 run-fio-tests.py [-r fio-root] [-f fio-path] [-a artifact-root]
13 #                           [--skip # # #...] [--run-only # # #...]
14 #
15 #
16 # EXAMPLE
17 # # git clone git://git.kernel.dk/fio.git
18 # # cd fio
19 # # make -j
20 # # python3 t/run-fio-tests.py
21 #
22 #
23 # REQUIREMENTS
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).
30 # - 4 CPUs (t0009)
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)
36 #
37 """
38
39 #
40 # TODO  run multiple tests simultaneously
41 # TODO  Add sgunmap tests (requires SAS SSD)
42 #
43
44 import os
45 import sys
46 import time
47 import shutil
48 import logging
49 import argparse
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 *
54
55
56 class FioJobFileTest_t0005(FioJobFileTest):
57     """Test consists of fio test job t0005
58     Confirm that read['io_kbytes'] == write['io_kbytes'] == 102400"""
59
60     def check_result(self):
61         super().check_result()
62
63         if not self.passed:
64             return
65
66         if self.json_data['jobs'][0]['read']['io_kbytes'] != 102400:
67             self.failure_reason = f"{self.failure_reason} bytes read mismatch,"
68             self.passed = False
69         if self.json_data['jobs'][0]['write']['io_kbytes'] != 102400:
70             self.failure_reason = f"{self.failure_reason} bytes written mismatch,"
71             self.passed = False
72
73
74 class FioJobFileTest_t0006(FioJobFileTest):
75     """Test consists of fio test job t0006
76     Confirm that read['io_kbytes'] ~ 2*write['io_kbytes']"""
77
78     def check_result(self):
79         super().check_result()
80
81         if not self.passed:
82             return
83
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,"
89             self.passed = False
90
91
92 class FioJobFileTest_t0007(FioJobFileTest):
93     """Test consists of fio test job t0007
94     Confirm that read['io_kbytes'] = 87040"""
95
96     def check_result(self):
97         super().check_result()
98
99         if not self.passed:
100             return
101
102         if self.json_data['jobs'][0]['read']['io_kbytes'] != 87040:
103             self.failure_reason = f"{self.failure_reason} bytes read mismatch,"
104             self.passed = False
105
106
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
111
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."""
117
118     def check_result(self):
119         super().check_result()
120
121         if not self.passed:
122             return
123
124         ratio = self.json_data['jobs'][0]['write']['io_kbytes'] / 16384
125         logging.debug("Test %d: ratio: %f", self.testnum, ratio)
126
127         if ratio < 0.97 or ratio > 1.03:
128             self.failure_reason = f"{self.failure_reason} bytes written mismatch,"
129             self.passed = False
130         if self.json_data['jobs'][0]['read']['io_kbytes'] != 32768:
131             self.failure_reason = f"{self.failure_reason} bytes read mismatch,"
132             self.passed = False
133
134
135 class FioJobFileTest_t0009(FioJobFileTest):
136     """Test consists of fio test job t0009
137     Confirm that runtime >= 60s"""
138
139     def check_result(self):
140         super().check_result()
141
142         if not self.passed:
143             return
144
145         logging.debug('Test %d: elapsed: %d', self.testnum, self.json_data['jobs'][0]['elapsed'])
146
147         if self.json_data['jobs'][0]['elapsed'] < 60:
148             self.failure_reason = f"{self.failure_reason} elapsed time mismatch,"
149             self.passed = False
150
151
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"""
156
157     def check_result(self):
158         super().check_result()
159
160         if not self.passed:
161             return
162
163         iops_files = []
164         for i in range(1, 4):
165             filename = os.path.join(self.paths['test_dir'], "{0}_iops.{1}.log".format(os.path.basename(
166                 self.fio_job), i))
167             file_data = self.get_file_fail(filename)
168             if not file_data:
169                 return
170
171             iops_files.append(file_data.splitlines())
172
173         # there are 9 samples for job1 and job2, 4 samples for job3
174         iops1 = 0.0
175         iops2 = 0.0
176         iops3 = 0.0
177         for i in range(9):
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])
181
182             ratio1 = iops3/iops2
183             ratio2 = iops3/iops1
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,
186                                                              ratio2))
187
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,
192                                                                         ratio1, ratio2)
193             self.passed = False
194             return
195
196
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.
201
202     The test is about making sure the flow feature can
203     re-calibrate the activity dynamically"""
204
205     def check_result(self):
206         super().check_result()
207
208         if not self.passed:
209             return
210
211         iops_files = []
212         for i in range(1, 4):
213             filename = os.path.join(self.paths['test_dir'], "{0}_iops.{1}.log".format(os.path.basename(
214                 self.fio_job), i))
215             file_data = self.get_file_fail(filename)
216             if not file_data:
217                 return
218
219             iops_files.append(file_data.splitlines())
220
221         # there are 9 samples for job1 and job2, 4 samples for job3
222         iops1 = 0.0
223         iops2 = 0.0
224         iops3 = 0.0
225         for i in range(9):
226             if i < 4:
227                 iops3 = iops3 + float(iops_files[2][i].split(',')[1])
228             elif i == 4:
229                 ratio1 = iops1 / iops2
230                 ratio2 = iops1 / iops3
231
232
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)
237                     self.passed = False
238
239             iops1 = iops1 + float(iops_files[0][i].split(',')[1])
240             iops2 = iops2 + float(iops_files[1][i].split(',')[1])
241
242             ratio1 = iops1/iops2
243             ratio2 = iops1/iops3
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,
246                                                                        ratio1, ratio2))
247
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)
252             self.passed = False
253             return
254
255
256 class FioJobFileTest_t0015(FioJobFileTest):
257     """Test consists of fio test jobs t0015 and t0016
258     Confirm that mean(slat) + mean(clat) = mean(tlat)"""
259
260     def check_result(self):
261         super().check_result()
262
263         if not self.passed:
264             return
265
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)
270
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)
274             self.passed = False
275
276
277 class FioJobFileTest_t0019(FioJobFileTest):
278     """Test consists of fio test job t0019
279     Confirm that all offsets were touched sequentially"""
280
281     def check_result(self):
282         super().check_result()
283
284         bw_log_filename = os.path.join(self.paths['test_dir'], "test_bw.log")
285         file_data = self.get_file_fail(bw_log_filename)
286         if not file_data:
287             return
288
289         log_lines = file_data.split('\n')
290
291         prev = -4096
292         for line in log_lines:
293             if len(line.strip()) == 0:
294                 continue
295             cur = int(line.split(',')[4])
296             if cur - prev != 4096:
297                 self.passed = False
298                 self.failure_reason = f"offsets {prev}, {cur} not sequential"
299                 return
300             prev = cur
301
302         if cur/4096 != 255:
303             self.passed = False
304             self.failure_reason = f"unexpected last offset {cur}"
305
306
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"""
310
311     def check_result(self):
312         super().check_result()
313
314         bw_log_filename = os.path.join(self.paths['test_dir'], "test_bw.log")
315         file_data = self.get_file_fail(bw_log_filename)
316         if not file_data:
317             return
318
319         log_lines = file_data.split('\n')
320
321         offsets = []
322
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:
327                 continue
328             cur = int(line.split(',')[4])
329             prev = cur
330
331         if len(offsets) != 256:
332             self.passed = False
333             self.failure_reason += f" number of offsets is {len(offsets)} instead of 256"
334
335         for i in range(256):
336             if not i in offsets:
337                 self.passed = False
338                 self.failure_reason += f" missing offset {i * 4096}"
339
340         (_, p) = runstest_1samp(list(offsets))
341         if p < 0.05:
342             self.passed = False
343             self.failure_reason += f" runs test failed with p = {p}"
344
345
346 class FioJobFileTest_t0022(FioJobFileTest):
347     """Test consists of fio test job t0022"""
348
349     def check_result(self):
350         super().check_result()
351
352         bw_log_filename = os.path.join(self.paths['test_dir'], "test_bw.log")
353         file_data = self.get_file_fail(bw_log_filename)
354         if not file_data:
355             return
356
357         log_lines = file_data.split('\n')
358
359         filesize = 1024*1024
360         bs = 4096
361         seq_count = 0
362         offsets = set()
363
364         prev = int(log_lines[0].split(',')[4])
365         for line in log_lines[1:]:
366             offsets.add(prev/bs)
367             if len(line.strip()) == 0:
368                 continue
369             cur = int(line.split(',')[4])
370             if cur - prev == bs:
371                 seq_count += 1
372             prev = cur
373
374         # 10 is an arbitrary threshold
375         if seq_count > 10:
376             self.passed = False
377             self.failure_reason = f"too many ({seq_count}) consecutive offsets"
378
379         if len(offsets) == filesize/bs:
380             self.passed = False
381             self.failure_reason += " no duplicate offsets found with norandommap=1"
382
383
384 class FioJobFileTest_t0023(FioJobFileTest):
385     """Test consists of fio test job t0023 randtrimwrite test."""
386
387     def check_trimwrite(self, filename):
388         """Make sure that trims are followed by writes of the same size at the same offset."""
389
390         bw_log_filename = os.path.join(self.paths['test_dir'], filename)
391         file_data = self.get_file_fail(bw_log_filename)
392         if not file_data:
393             return
394
395         log_lines = file_data.split('\n')
396
397         prev_ddir = 1
398         for line in log_lines:
399             if len(line.strip()) == 0:
400                 continue
401             vals = line.split(',')
402             ddir = int(vals[2])
403             bs = int(vals[3])
404             offset = int(vals[4])
405             if prev_ddir == 1:
406                 if ddir != 2:
407                     self.passed = False
408                     self.failure_reason += " {0}: write not preceeded by trim: {1}".format(
409                         bw_log_filename, line)
410                     break
411             else:
412                 if ddir != 1:   # pylint: disable=no-else-break
413                     self.passed = False
414                     self.failure_reason += " {0}: trim not preceeded by write: {1}".format(
415                         bw_log_filename, line)
416                     break
417                 else:
418                     if prev_bs != bs:
419                         self.passed = False
420                         self.failure_reason += " {0}: block size does not match: {1}".format(
421                             bw_log_filename, line)
422                         break
423
424                     if prev_offset != offset:
425                         self.passed = False
426                         self.failure_reason += " {0}: offset does not match: {1}".format(
427                             bw_log_filename, line)
428                         break
429
430             prev_ddir = ddir
431             prev_bs = bs
432             prev_offset = offset
433
434
435     def check_all_offsets(self, filename, sectorsize, filesize):
436         """Make sure all offsets were touched."""
437
438         file_data = self.get_file_fail(os.path.join(self.paths['test_dir'], filename))
439         if not file_data:
440             return
441
442         log_lines = file_data.split('\n')
443
444         offsets = set()
445
446         for line in log_lines:
447             if len(line.strip()) == 0:
448                 continue
449             vals = line.split(',')
450             bs = int(vals[3])
451             offset = int(vals[4])
452             if offset % sectorsize != 0:
453                 self.passed = False
454                 self.failure_reason += " {0}: offset {1} not a multiple of sector size {2}".format(
455                     filename, offset, sectorsize)
456                 break
457             if bs % sectorsize != 0:
458                 self.passed = False
459                 self.failure_reason += " {0}: block size {1} not a multiple of sector size " \
460                     "{2}".format(filename, bs, sectorsize)
461                 break
462             for i in range(int(bs/sectorsize)):
463                 offsets.add(offset/sectorsize + i)
464
465         if len(offsets) != filesize/sectorsize:
466             self.passed = False
467             self.failure_reason += " {0}: only {1} offsets touched; expected {2}".format(
468                 filename, len(offsets), filesize/sectorsize)
469         else:
470             logging.debug("%s: %d sectors touched", filename, len(offsets))
471
472
473     def check_result(self):
474         super().check_result()
475
476         filesize = 1024*1024
477
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")
486
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)
491
492
493 class FioJobFileTest_t0024(FioJobFileTest_t0023):
494     """Test consists of fio test job t0024 trimwrite test."""
495
496     def check_result(self):
497         # call FioJobFileTest_t0023's parent to skip checks done by t0023
498         super(FioJobFileTest_t0023, self).check_result()
499
500         filesize = 1024*1024
501
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")
506
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)
511
512
513 class FioJobFileTest_t0025(FioJobFileTest):
514     """Test experimental verify read backs written data pattern."""
515     def check_result(self):
516         super().check_result()
517
518         if not self.passed:
519             return
520
521         if self.json_data['jobs'][0]['read']['io_kbytes'] != 128:
522             self.passed = False
523
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)
532
533     def check_result(self):
534         super().check_result()
535
536         if not self.passed:
537             return
538
539         with open(self.output_file, "rb") as f:
540             data = f.read()
541
542         if data != self.pattern:
543             self.passed = False
544
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"""
550
551     def check_result(self):
552         super().check_result()
553
554         if not self.passed:
555             return
556
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)
563
564         if iops1 < 950 or iops1 > 1050:
565             self.failure_reason = f"{self.failure_reason} iops value mismatch,"
566             self.passed = False
567
568         if ratio < 6 or ratio > 10:
569             self.failure_reason = f"{self.failure_reason} iops ratio mismatch,"
570             self.passed = False
571
572
573 TEST_LIST = [
574     {
575         'test_id':          1,
576         'test_class':       FioJobFileTest,
577         'job':              't0001-52c58027.fio',
578         'success':          SUCCESS_DEFAULT,
579         'pre_job':          None,
580         'pre_success':      None,
581         'requirements':     [],
582     },
583     {
584         'test_id':          2,
585         'test_class':       FioJobFileTest,
586         'job':              't0002-13af05ae-post.fio',
587         'success':          SUCCESS_DEFAULT,
588         'pre_job':          't0002-13af05ae-pre.fio',
589         'pre_success':      None,
590         'requirements':     [Requirements.linux, Requirements.libaio],
591     },
592     {
593         'test_id':          3,
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],
600     },
601     {
602         'test_id':          4,
603         'test_class':       FioJobFileTest,
604         'job':              't0004-8a99fdf6.fio',
605         'success':          SUCCESS_DEFAULT,
606         'pre_job':          None,
607         'pre_success':      None,
608         'requirements':     [Requirements.linux, Requirements.libaio],
609     },
610     {
611         'test_id':          5,
612         'test_class':       FioJobFileTest_t0005,
613         'job':              't0005-f7078f7b.fio',
614         'success':          SUCCESS_DEFAULT,
615         'pre_job':          None,
616         'pre_success':      None,
617         'output_format':    'json',
618         'requirements':     [Requirements.not_windows],
619     },
620     {
621         'test_id':          6,
622         'test_class':       FioJobFileTest_t0006,
623         'job':              't0006-82af2a7c.fio',
624         'success':          SUCCESS_DEFAULT,
625         'pre_job':          None,
626         'pre_success':      None,
627         'output_format':    'json',
628         'requirements':     [Requirements.linux, Requirements.libaio],
629     },
630     {
631         'test_id':          7,
632         'test_class':       FioJobFileTest_t0007,
633         'job':              't0007-37cf9e3c.fio',
634         'success':          SUCCESS_DEFAULT,
635         'pre_job':          None,
636         'pre_success':      None,
637         'output_format':    'json',
638         'requirements':     [],
639     },
640     {
641         'test_id':          8,
642         'test_class':       FioJobFileTest_t0008,
643         'job':              't0008-ae2fafc8.fio',
644         'success':          SUCCESS_DEFAULT,
645         'pre_job':          None,
646         'pre_success':      None,
647         'output_format':    'json',
648         'requirements':     [],
649     },
650     {
651         'test_id':          9,
652         'test_class':       FioJobFileTest_t0009,
653         'job':              't0009-f8b0bd10.fio',
654         'success':          SUCCESS_DEFAULT,
655         'pre_job':          None,
656         'pre_success':      None,
657         'output_format':    'json',
658         'requirements':     [Requirements.not_macos,
659                              Requirements.cpucount4],
660         # mac os does not support CPU affinity
661     },
662     {
663         'test_id':          10,
664         'test_class':       FioJobFileTest,
665         'job':              't0010-b7aae4ba.fio',
666         'success':          SUCCESS_DEFAULT,
667         'pre_job':          None,
668         'pre_success':      None,
669         'requirements':     [],
670     },
671     {
672         'test_id':          11,
673         'test_class':       FioJobFileTest_iops_rate,
674         'job':              't0011-5d2788d5.fio',
675         'success':          SUCCESS_DEFAULT,
676         'pre_job':          None,
677         'pre_success':      None,
678         'output_format':    'json',
679         'requirements':     [],
680     },
681     {
682         'test_id':          12,
683         'test_class':       FioJobFileTest_t0012,
684         'job':              't0012.fio',
685         'success':          SUCCESS_DEFAULT,
686         'pre_job':          None,
687         'pre_success':      None,
688         'output_format':    'json',
689         'requirements':     [],
690     },
691     {
692         'test_id':          13,
693         'test_class':       FioJobFileTest,
694         'job':              't0013.fio',
695         'success':          SUCCESS_DEFAULT,
696         'pre_job':          None,
697         'pre_success':      None,
698         'output_format':    'json',
699         'requirements':     [],
700     },
701     {
702         'test_id':          14,
703         'test_class':       FioJobFileTest_t0014,
704         'job':              't0014.fio',
705         'success':          SUCCESS_DEFAULT,
706         'pre_job':          None,
707         'pre_success':      None,
708         'output_format':    'json',
709         'requirements':     [],
710     },
711     {
712         'test_id':          15,
713         'test_class':       FioJobFileTest_t0015,
714         'job':              't0015-e78980ff.fio',
715         'success':          SUCCESS_DEFAULT,
716         'pre_job':          None,
717         'pre_success':      None,
718         'output_format':    'json',
719         'requirements':     [Requirements.linux, Requirements.libaio],
720     },
721     {
722         'test_id':          16,
723         'test_class':       FioJobFileTest_t0015,
724         'job':              't0016-d54ae22.fio',
725         'success':          SUCCESS_DEFAULT,
726         'pre_job':          None,
727         'pre_success':      None,
728         'output_format':    'json',
729         'requirements':     [],
730     },
731     {
732         'test_id':          17,
733         'test_class':       FioJobFileTest_t0015,
734         'job':              't0017.fio',
735         'success':          SUCCESS_DEFAULT,
736         'pre_job':          None,
737         'pre_success':      None,
738         'output_format':    'json',
739         'requirements':     [Requirements.not_windows],
740     },
741     {
742         'test_id':          18,
743         'test_class':       FioJobFileTest,
744         'job':              't0018.fio',
745         'success':          SUCCESS_DEFAULT,
746         'pre_job':          None,
747         'pre_success':      None,
748         'requirements':     [Requirements.linux, Requirements.io_uring],
749     },
750     {
751         'test_id':          19,
752         'test_class':       FioJobFileTest_t0019,
753         'job':              't0019.fio',
754         'success':          SUCCESS_DEFAULT,
755         'pre_job':          None,
756         'pre_success':      None,
757         'requirements':     [],
758     },
759     {
760         'test_id':          20,
761         'test_class':       FioJobFileTest_t0020,
762         'job':              't0020.fio',
763         'success':          SUCCESS_DEFAULT,
764         'pre_job':          None,
765         'pre_success':      None,
766         'requirements':     [],
767     },
768     {
769         'test_id':          21,
770         'test_class':       FioJobFileTest_t0020,
771         'job':              't0021.fio',
772         'success':          SUCCESS_DEFAULT,
773         'pre_job':          None,
774         'pre_success':      None,
775         'requirements':     [],
776     },
777     {
778         'test_id':          22,
779         'test_class':       FioJobFileTest_t0022,
780         'job':              't0022.fio',
781         'success':          SUCCESS_DEFAULT,
782         'pre_job':          None,
783         'pre_success':      None,
784         'requirements':     [],
785     },
786     {
787         'test_id':          23,
788         'test_class':       FioJobFileTest_t0023,
789         'job':              't0023.fio',
790         'success':          SUCCESS_DEFAULT,
791         'pre_job':          None,
792         'pre_success':      None,
793         'requirements':     [],
794     },
795     {
796         'test_id':          24,
797         'test_class':       FioJobFileTest_t0024,
798         'job':              't0024.fio',
799         'success':          SUCCESS_DEFAULT,
800         'pre_job':          None,
801         'pre_success':      None,
802         'requirements':     [],
803     },
804     {
805         'test_id':          25,
806         'test_class':       FioJobFileTest_t0025,
807         'job':              't0025.fio',
808         'success':          SUCCESS_DEFAULT,
809         'pre_job':          None,
810         'pre_success':      None,
811         'output_format':    'json',
812         'requirements':     [],
813     },
814     {
815         'test_id':          26,
816         'test_class':       FioJobFileTest,
817         'job':              't0026.fio',
818         'success':          SUCCESS_DEFAULT,
819         'pre_job':          None,
820         'pre_success':      None,
821         'requirements':     [Requirements.not_windows],
822     },
823     {
824         'test_id':          27,
825         'test_class':       FioJobFileTest_t0027,
826         'job':              't0027.fio',
827         'success':          SUCCESS_DEFAULT,
828         'pre_job':          None,
829         'pre_success':      None,
830         'requirements':     [],
831     },
832     {
833         'test_id':          28,
834         'test_class':       FioJobFileTest,
835         'job':              't0028-c6cade16.fio',
836         'success':          SUCCESS_DEFAULT,
837         'pre_job':          None,
838         'pre_success':      None,
839         'requirements':     [],
840     },
841     {
842         'test_id':          1000,
843         'test_class':       FioExeTest,
844         'exe':              't/axmap',
845         'parameters':       None,
846         'success':          SUCCESS_DEFAULT,
847         'requirements':     [],
848     },
849     {
850         'test_id':          1001,
851         'test_class':       FioExeTest,
852         'exe':              't/ieee754',
853         'parameters':       None,
854         'success':          SUCCESS_DEFAULT,
855         'requirements':     [],
856     },
857     {
858         'test_id':          1002,
859         'test_class':       FioExeTest,
860         'exe':              't/lfsr-test',
861         'parameters':       ['0xFFFFFF', '0', '0', 'verify'],
862         'success':          SUCCESS_STDERR,
863         'requirements':     [],
864     },
865     {
866         'test_id':          1003,
867         'test_class':       FioExeTest,
868         'exe':              't/readonly.py',
869         'parameters':       ['-f', '{fio_path}'],
870         'success':          SUCCESS_DEFAULT,
871         'requirements':     [],
872     },
873     {
874         'test_id':          1004,
875         'test_class':       FioExeTest,
876         'exe':              't/steadystate_tests.py',
877         'parameters':       ['{fio_path}'],
878         'success':          SUCCESS_DEFAULT,
879         'requirements':     [],
880     },
881     {
882         'test_id':          1005,
883         'test_class':       FioExeTest,
884         'exe':              't/stest',
885         'parameters':       None,
886         'success':          SUCCESS_STDERR,
887         'requirements':     [],
888     },
889     {
890         'test_id':          1006,
891         'test_class':       FioExeTest,
892         'exe':              't/strided.py',
893         'parameters':       ['--fio', '{fio_path}'],
894         'success':          SUCCESS_DEFAULT,
895         'requirements':     [],
896     },
897     {
898         'test_id':          1007,
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,
904                              Requirements.root],
905     },
906     {
907         'test_id':          1008,
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],
914     },
915     {
916         'test_id':          1009,
917         'test_class':       FioExeTest,
918         'exe':              'unittests/unittest',
919         'parameters':       None,
920         'success':          SUCCESS_DEFAULT,
921         'requirements':     [Requirements.unittests],
922     },
923     {
924         'test_id':          1010,
925         'test_class':       FioExeTest,
926         'exe':              't/latency_percentiles.py',
927         'parameters':       ['-f', '{fio_path}'],
928         'success':          SUCCESS_DEFAULT,
929         'requirements':     [],
930     },
931     {
932         'test_id':          1011,
933         'test_class':       FioExeTest,
934         'exe':              't/jsonplus2csv_test.py',
935         'parameters':       ['-f', '{fio_path}'],
936         'success':          SUCCESS_DEFAULT,
937         'requirements':     [],
938     },
939     {
940         'test_id':          1012,
941         'test_class':       FioExeTest,
942         'exe':              't/log_compression.py',
943         'parameters':       ['-f', '{fio_path}'],
944         'success':          SUCCESS_DEFAULT,
945         'requirements':     [],
946     },
947     {
948         'test_id':          1013,
949         'test_class':       FioExeTest,
950         'exe':              't/random_seed.py',
951         'parameters':       ['-f', '{fio_path}'],
952         'success':          SUCCESS_DEFAULT,
953         'requirements':     [],
954     },
955     {
956         'test_id':          1014,
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],
962     },
963 ]
964
965
966 def parse_args():
967     """Parse command-line arguments."""
968
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()
989
990     return args
991
992
993 def main():
994     """Entry point."""
995
996     args = parse_args()
997     if args.debug:
998         logging.basicConfig(level=logging.DEBUG)
999     else:
1000         logging.basicConfig(level=logging.INFO)
1001
1002     pass_through = {}
1003     if args.pass_through:
1004         for arg in args.pass_through:
1005             if not ':' in arg:
1006                 print(f"Invalid --pass-through argument '{arg}'")
1007                 print("Syntax for --pass-through is TESTNUMBER:ARGUMENT")
1008                 return
1009             split = arg.split(":", 1)
1010             pass_through[int(split[0])] = split[1]
1011         logging.debug("Pass-through arguments: %s", pass_through)
1012
1013     if args.fio_root:
1014         fio_root = args.fio_root
1015     else:
1016         fio_root = str(Path(__file__).absolute().parent.parent)
1017     print(f"fio root is {fio_root}")
1018
1019     if args.fio:
1020         fio_path = args.fio
1021     else:
1022         if platform.system() == "Windows":
1023             fio_exe = "fio.exe"
1024         else:
1025             fio_exe = "fio"
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")
1030
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}")
1035
1036     if not args.skip_req:
1037         Requirements(fio_root, args)
1038
1039     test_env = {
1040               'fio_path': fio_path,
1041               'fio_root': fio_root,
1042               'artifact_root': artifact_root,
1043               'pass_through': pass_through,
1044               }
1045     _, failed, _ = run_fio_tests(TEST_LIST, test_env, args)
1046     sys.exit(failed)
1047
1048
1049 if __name__ == '__main__':
1050     main()