b4863297666941f5250526aa1052db68c3aa620e
[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 import re
51 from pathlib import Path
52 from statsmodels.sandbox.stats.runs import runstest_1samp
53 from fiotestlib import FioExeTest, FioJobFileTest, run_fio_tests
54 from fiotestcommon import *
55
56
57 class FioJobFileTest_t0005(FioJobFileTest):
58     """Test consists of fio test job t0005
59     Confirm that read['io_kbytes'] == write['io_kbytes'] == 102400"""
60
61     def check_result(self):
62         super().check_result()
63
64         if not self.passed:
65             return
66
67         if self.json_data['jobs'][0]['read']['io_kbytes'] != 102400:
68             self.failure_reason = f"{self.failure_reason} bytes read mismatch,"
69             self.passed = False
70         if self.json_data['jobs'][0]['write']['io_kbytes'] != 102400:
71             self.failure_reason = f"{self.failure_reason} bytes written mismatch,"
72             self.passed = False
73
74
75 class FioJobFileTest_t0006(FioJobFileTest):
76     """Test consists of fio test job t0006
77     Confirm that read['io_kbytes'] ~ 2*write['io_kbytes']"""
78
79     def check_result(self):
80         super().check_result()
81
82         if not self.passed:
83             return
84
85         ratio = self.json_data['jobs'][0]['read']['io_kbytes'] \
86             / self.json_data['jobs'][0]['write']['io_kbytes']
87         logging.debug("Test %d: ratio: %f", self.testnum, ratio)
88         if ratio < 1.99 or ratio > 2.01:
89             self.failure_reason = f"{self.failure_reason} read/write ratio mismatch,"
90             self.passed = False
91
92
93 class FioJobFileTest_t0007(FioJobFileTest):
94     """Test consists of fio test job t0007
95     Confirm that read['io_kbytes'] = 87040"""
96
97     def check_result(self):
98         super().check_result()
99
100         if not self.passed:
101             return
102
103         if self.json_data['jobs'][0]['read']['io_kbytes'] != 87040:
104             self.failure_reason = f"{self.failure_reason} bytes read mismatch,"
105             self.passed = False
106
107
108 class FioJobFileTest_t0008(FioJobFileTest):
109     """Test consists of fio test job t0008
110     Confirm that read['io_kbytes'] = 32768 and that
111                 write['io_kbytes'] ~ 16384
112
113     This is a 50/50 seq read/write workload. Since fio flips a coin to
114     determine whether to issue a read or a write, total bytes written will not
115     be exactly 16384K. But total bytes read will be exactly 32768K because
116     reads will include the initial phase as well as the verify phase where all
117     the blocks originally written will be read."""
118
119     def check_result(self):
120         super().check_result()
121
122         if not self.passed:
123             return
124
125         ratio = self.json_data['jobs'][0]['write']['io_kbytes'] / 16384
126         logging.debug("Test %d: ratio: %f", self.testnum, ratio)
127
128         if ratio < 0.97 or ratio > 1.03:
129             self.failure_reason = f"{self.failure_reason} bytes written mismatch,"
130             self.passed = False
131         if self.json_data['jobs'][0]['read']['io_kbytes'] != 32768:
132             self.failure_reason = f"{self.failure_reason} bytes read mismatch,"
133             self.passed = False
134
135
136 class FioJobFileTest_t0009(FioJobFileTest):
137     """Test consists of fio test job t0009
138     Confirm that runtime >= 60s"""
139
140     def check_result(self):
141         super().check_result()
142
143         if not self.passed:
144             return
145
146         logging.debug('Test %d: elapsed: %d', self.testnum, self.json_data['jobs'][0]['elapsed'])
147
148         if self.json_data['jobs'][0]['elapsed'] < 60:
149             self.failure_reason = f"{self.failure_reason} elapsed time mismatch,"
150             self.passed = False
151
152
153 class FioJobFileTest_t0012(FioJobFileTest):
154     """Test consists of fio test job t0012
155     Confirm ratios of job iops are 1:5:10
156     job1,job2,job3 respectively"""
157
158     def check_result(self):
159         super().check_result()
160
161         if not self.passed:
162             return
163
164         iops_files = []
165         for i in range(1, 4):
166             filename = os.path.join(self.paths['test_dir'], "{0}_iops.{1}.log".format(os.path.basename(
167                 self.fio_job), i))
168             file_data = self.get_file_fail(filename)
169             if not file_data:
170                 return
171
172             iops_files.append(file_data.splitlines())
173
174         # there are 9 samples for job1 and job2, 4 samples for job3
175         iops1 = 0.0
176         iops2 = 0.0
177         iops3 = 0.0
178         for i in range(9):
179             iops1 = iops1 + float(iops_files[0][i].split(',')[1])
180             iops2 = iops2 + float(iops_files[1][i].split(',')[1])
181             iops3 = iops3 + float(iops_files[2][i].split(',')[1])
182
183             ratio1 = iops3/iops2
184             ratio2 = iops3/iops1
185             logging.debug("sample {0}: job1 iops={1} job2 iops={2} job3 iops={3} " \
186                 "job3/job2={4:.3f} job3/job1={5:.3f}".format(i, iops1, iops2, iops3, ratio1,
187                                                              ratio2))
188
189         # test job1 and job2 succeeded to recalibrate
190         if ratio1 < 1 or ratio1 > 3 or ratio2 < 7 or ratio2 > 13:
191             self.failure_reason += " iops ratio mismatch iops1={0} iops2={1} iops3={2} " \
192                 "expected r1~2 r2~10 got r1={3:.3f} r2={4:.3f},".format(iops1, iops2, iops3,
193                                                                         ratio1, ratio2)
194             self.passed = False
195             return
196
197
198 class FioJobFileTest_t0014(FioJobFileTest):
199     """Test consists of fio test job t0014
200         Confirm that job1_iops / job2_iops ~ 1:2 for entire duration
201         and that job1_iops / job3_iops ~ 1:3 for first half of duration.
202
203     The test is about making sure the flow feature can
204     re-calibrate the activity dynamically"""
205
206     def check_result(self):
207         super().check_result()
208
209         if not self.passed:
210             return
211
212         iops_files = []
213         for i in range(1, 4):
214             filename = os.path.join(self.paths['test_dir'], "{0}_iops.{1}.log".format(os.path.basename(
215                 self.fio_job), i))
216             file_data = self.get_file_fail(filename)
217             if not file_data:
218                 return
219
220             iops_files.append(file_data.splitlines())
221
222         # there are 9 samples for job1 and job2, 4 samples for job3
223         iops1 = 0.0
224         iops2 = 0.0
225         iops3 = 0.0
226         for i in range(9):
227             if i < 4:
228                 iops3 = iops3 + float(iops_files[2][i].split(',')[1])
229             elif i == 4:
230                 ratio1 = iops1 / iops2
231                 ratio2 = iops1 / iops3
232
233
234                 if ratio1 < 0.43 or ratio1 > 0.57 or ratio2 < 0.21 or ratio2 > 0.45:
235                     self.failure_reason += " iops ratio mismatch iops1={0} iops2={1} iops3={2} " \
236                                            "expected r1~0.5 r2~0.33 got r1={3:.3f} r2={4:.3f},".format(
237                                                iops1, iops2, iops3, ratio1, ratio2)
238                     self.passed = False
239
240             iops1 = iops1 + float(iops_files[0][i].split(',')[1])
241             iops2 = iops2 + float(iops_files[1][i].split(',')[1])
242
243             ratio1 = iops1/iops2
244             ratio2 = iops1/iops3
245             logging.debug("sample {0}: job1 iops={1} job2 iops={2} job3 iops={3} " \
246                           "job1/job2={4:.3f} job1/job3={5:.3f}".format(i, iops1, iops2, iops3,
247                                                                        ratio1, ratio2))
248
249         # test job1 and job2 succeeded to recalibrate
250         if ratio1 < 0.43 or ratio1 > 0.57:
251             self.failure_reason += " iops ratio mismatch iops1={0} iops2={1} expected ratio~0.5 " \
252                                    "got ratio={2:.3f},".format(iops1, iops2, ratio1)
253             self.passed = False
254             return
255
256
257 class FioJobFileTest_t0015(FioJobFileTest):
258     """Test consists of fio test jobs t0015 and t0016
259     Confirm that mean(slat) + mean(clat) = mean(tlat)"""
260
261     def check_result(self):
262         super().check_result()
263
264         if not self.passed:
265             return
266
267         slat = self.json_data['jobs'][0]['read']['slat_ns']['mean']
268         clat = self.json_data['jobs'][0]['read']['clat_ns']['mean']
269         tlat = self.json_data['jobs'][0]['read']['lat_ns']['mean']
270         logging.debug('Test %d: slat %f, clat %f, tlat %f', self.testnum, slat, clat, tlat)
271
272         if abs(slat + clat - tlat) > 1:
273             self.failure_reason = "{0} slat {1} + clat {2} = {3} != tlat {4},".format(
274                 self.failure_reason, slat, clat, slat+clat, tlat)
275             self.passed = False
276
277
278 class FioJobFileTest_t0019(FioJobFileTest):
279     """Test consists of fio test job t0019
280     Confirm that all offsets were touched sequentially"""
281
282     def check_result(self):
283         super().check_result()
284
285         bw_log_filename = os.path.join(self.paths['test_dir'], "test_bw.log")
286         file_data = self.get_file_fail(bw_log_filename)
287         if not file_data:
288             return
289
290         log_lines = file_data.split('\n')
291
292         prev = -4096
293         for line in log_lines:
294             if len(line.strip()) == 0:
295                 continue
296             cur = int(line.split(',')[4])
297             if cur - prev != 4096:
298                 self.passed = False
299                 self.failure_reason = f"offsets {prev}, {cur} not sequential"
300                 return
301             prev = cur
302
303         if cur/4096 != 255:
304             self.passed = False
305             self.failure_reason = f"unexpected last offset {cur}"
306
307
308 class FioJobFileTest_t0020(FioJobFileTest):
309     """Test consists of fio test jobs t0020 and t0021
310     Confirm that almost all offsets were touched non-sequentially"""
311
312     def check_result(self):
313         super().check_result()
314
315         bw_log_filename = os.path.join(self.paths['test_dir'], "test_bw.log")
316         file_data = self.get_file_fail(bw_log_filename)
317         if not file_data:
318             return
319
320         log_lines = file_data.split('\n')
321
322         offsets = []
323
324         prev = int(log_lines[0].split(',')[4])
325         for line in log_lines[1:]:
326             offsets.append(prev/4096)
327             if len(line.strip()) == 0:
328                 continue
329             cur = int(line.split(',')[4])
330             prev = cur
331
332         if len(offsets) != 256:
333             self.passed = False
334             self.failure_reason += f" number of offsets is {len(offsets)} instead of 256"
335
336         for i in range(256):
337             if not i in offsets:
338                 self.passed = False
339                 self.failure_reason += f" missing offset {i * 4096}"
340
341         (_, p) = runstest_1samp(list(offsets))
342         if p < 0.05:
343             self.passed = False
344             self.failure_reason += f" runs test failed with p = {p}"
345
346
347 class FioJobFileTest_t0022(FioJobFileTest):
348     """Test consists of fio test job t0022"""
349
350     def check_result(self):
351         super().check_result()
352
353         bw_log_filename = os.path.join(self.paths['test_dir'], "test_bw.log")
354         file_data = self.get_file_fail(bw_log_filename)
355         if not file_data:
356             return
357
358         log_lines = file_data.split('\n')
359
360         filesize = 1024*1024
361         bs = 4096
362         seq_count = 0
363         offsets = set()
364
365         prev = int(log_lines[0].split(',')[4])
366         for line in log_lines[1:]:
367             offsets.add(prev/bs)
368             if len(line.strip()) == 0:
369                 continue
370             cur = int(line.split(',')[4])
371             if cur - prev == bs:
372                 seq_count += 1
373             prev = cur
374
375         # 10 is an arbitrary threshold
376         if seq_count > 10:
377             self.passed = False
378             self.failure_reason = f"too many ({seq_count}) consecutive offsets"
379
380         if len(offsets) == filesize/bs:
381             self.passed = False
382             self.failure_reason += " no duplicate offsets found with norandommap=1"
383
384
385 class FioJobFileTest_t0023(FioJobFileTest):
386     """Test consists of fio test job t0023 randtrimwrite test."""
387
388     def check_trimwrite(self, filename):
389         """Make sure that trims are followed by writes of the same size at the same offset."""
390
391         bw_log_filename = os.path.join(self.paths['test_dir'], filename)
392         file_data = self.get_file_fail(bw_log_filename)
393         if not file_data:
394             return
395
396         log_lines = file_data.split('\n')
397
398         prev_ddir = 1
399         for line in log_lines:
400             if len(line.strip()) == 0:
401                 continue
402             vals = line.split(',')
403             ddir = int(vals[2])
404             bs = int(vals[3])
405             offset = int(vals[4])
406             if prev_ddir == 1:
407                 if ddir != 2:
408                     self.passed = False
409                     self.failure_reason += " {0}: write not preceeded by trim: {1}".format(
410                         bw_log_filename, line)
411                     break
412             else:
413                 if ddir != 1:   # pylint: disable=no-else-break
414                     self.passed = False
415                     self.failure_reason += " {0}: trim not preceeded by write: {1}".format(
416                         bw_log_filename, line)
417                     break
418                 else:
419                     if prev_bs != bs:
420                         self.passed = False
421                         self.failure_reason += " {0}: block size does not match: {1}".format(
422                             bw_log_filename, line)
423                         break
424
425                     if prev_offset != offset:
426                         self.passed = False
427                         self.failure_reason += " {0}: offset does not match: {1}".format(
428                             bw_log_filename, line)
429                         break
430
431             prev_ddir = ddir
432             prev_bs = bs
433             prev_offset = offset
434
435
436     def check_all_offsets(self, filename, sectorsize, filesize):
437         """Make sure all offsets were touched."""
438
439         file_data = self.get_file_fail(os.path.join(self.paths['test_dir'], filename))
440         if not file_data:
441             return
442
443         log_lines = file_data.split('\n')
444
445         offsets = set()
446
447         for line in log_lines:
448             if len(line.strip()) == 0:
449                 continue
450             vals = line.split(',')
451             bs = int(vals[3])
452             offset = int(vals[4])
453             if offset % sectorsize != 0:
454                 self.passed = False
455                 self.failure_reason += " {0}: offset {1} not a multiple of sector size {2}".format(
456                     filename, offset, sectorsize)
457                 break
458             if bs % sectorsize != 0:
459                 self.passed = False
460                 self.failure_reason += " {0}: block size {1} not a multiple of sector size " \
461                     "{2}".format(filename, bs, sectorsize)
462                 break
463             for i in range(int(bs/sectorsize)):
464                 offsets.add(offset/sectorsize + i)
465
466         if len(offsets) != filesize/sectorsize:
467             self.passed = False
468             self.failure_reason += " {0}: only {1} offsets touched; expected {2}".format(
469                 filename, len(offsets), filesize/sectorsize)
470         else:
471             logging.debug("%s: %d sectors touched", filename, len(offsets))
472
473
474     def check_result(self):
475         super().check_result()
476
477         filesize = 1024*1024
478
479         self.check_trimwrite("basic_bw.log")
480         self.check_trimwrite("bs_bw.log")
481         self.check_trimwrite("bsrange_bw.log")
482         self.check_trimwrite("bssplit_bw.log")
483         self.check_trimwrite("basic_no_rm_bw.log")
484         self.check_trimwrite("bs_no_rm_bw.log")
485         self.check_trimwrite("bsrange_no_rm_bw.log")
486         self.check_trimwrite("bssplit_no_rm_bw.log")
487
488         self.check_all_offsets("basic_bw.log", 4096, filesize)
489         self.check_all_offsets("bs_bw.log", 8192, filesize)
490         self.check_all_offsets("bsrange_bw.log", 512, filesize)
491         self.check_all_offsets("bssplit_bw.log", 512, filesize)
492
493
494 class FioJobFileTest_t0024(FioJobFileTest_t0023):
495     """Test consists of fio test job t0024 trimwrite test."""
496
497     def check_result(self):
498         # call FioJobFileTest_t0023's parent to skip checks done by t0023
499         super(FioJobFileTest_t0023, self).check_result()
500
501         filesize = 1024*1024
502
503         self.check_trimwrite("basic_bw.log")
504         self.check_trimwrite("bs_bw.log")
505         self.check_trimwrite("bsrange_bw.log")
506         self.check_trimwrite("bssplit_bw.log")
507
508         self.check_all_offsets("basic_bw.log", 4096, filesize)
509         self.check_all_offsets("bs_bw.log", 8192, filesize)
510         self.check_all_offsets("bsrange_bw.log", 512, filesize)
511         self.check_all_offsets("bssplit_bw.log", 512, filesize)
512
513
514 class FioJobFileTest_t0025(FioJobFileTest):
515     """Test experimental verify read backs written data pattern."""
516     def check_result(self):
517         super().check_result()
518
519         if not self.passed:
520             return
521
522         if self.json_data['jobs'][0]['read']['io_kbytes'] != 128:
523             self.passed = False
524
525 class FioJobFileTest_t0027(FioJobFileTest):
526     def setup(self, *args, **kws):
527         super().setup(*args, **kws)
528         self.pattern_file = os.path.join(self.paths['test_dir'], "t0027.pattern")
529         self.output_file = os.path.join(self.paths['test_dir'], "t0027file")
530         self.pattern = os.urandom(16 << 10)
531         with open(self.pattern_file, "wb") as f:
532             f.write(self.pattern)
533
534     def check_result(self):
535         super().check_result()
536
537         if not self.passed:
538             return
539
540         with open(self.output_file, "rb") as f:
541             data = f.read()
542
543         if data != self.pattern:
544             self.passed = False
545
546 class FioJobFileTest_t0029(FioJobFileTest):
547     """Test loops option works with read-verify workload."""
548     def check_result(self):
549         super().check_result()
550
551         if not self.passed:
552             return
553
554         if self.json_data['jobs'][1]['read']['io_kbytes'] != 8:
555             self.passed = False
556
557 class FioJobFileTest_LogFileFormat(FioJobFileTest):
558     """Test log file format"""
559     def setup(self, *args, **kws):
560         super().setup(*args, **kws)
561         self.patterns = {}
562
563     def check_result(self):
564         super().check_result()
565
566         if not self.passed:
567             return
568
569         for logfile in self.patterns.keys():
570             file_path = os.path.join(self.paths['test_dir'], logfile)
571             with open(file_path, "r") as f:
572                 line = f.readline()
573                 if not re.match(self.patterns[logfile], line):
574                     self.passed = False
575                     self.failure_reason = "wrong log file format: " + logfile
576                     return
577
578 class FioJobFileTest_t0033(FioJobFileTest_LogFileFormat):
579     """Test log file format"""
580     def setup(self, *args, **kws):
581         super().setup(*args, **kws)
582         self.patterns = {
583             'log_bw.1.log': '\\d+, \\d+, \\d+, \\d+, 0x[\\da-f]+\\n',
584             'log_clat.2.log': '\\d+, \\d+, \\d+, \\d+, 0, \\d+\\n',
585             'log_iops.3.log': '\\d+, \\d+, \\d+, \\d+, \\d+, 0x[\\da-f]+\\n',
586             'log_iops.4.log': '\\d+, \\d+, \\d+, \\d+, 0, 0, \\d+\\n',
587         }
588
589 class FioJobFileTest_t0034(FioJobFileTest_LogFileFormat):
590     """Test log file format"""
591     def setup(self, *args, **kws):
592         super().setup(*args, **kws)
593         self.patterns = {
594             'log_clat.1.log': '\\d+, \\d+, \\d+, \\d+, \\d+, \\d+, \\d+\\n',
595             'log_slat.1.log': '\\d+, \\d+, \\d+, \\d+, \\d+, \\d+, \\d+\\n',
596             'log_lat.1.log': '\\d+, \\d+, \\d+, \\d+, \\d+, \\d+, 0\\n',
597             'log_clat.2.log': '\\d+, \\d+, \\d+, \\d+, 0, 0, \\d+, 0\\n',
598             'log_bw.3.log': '\\d+, \\d+, \\d+, \\d+, \\d+, \\d+, 0\\n',
599             'log_iops.3.log': '\\d+, \\d+, \\d+, \\d+, \\d+, \\d+, 0\\n',
600         }
601
602 class FioJobFileTest_iops_rate(FioJobFileTest):
603     """Test consists of fio test job t0011
604     Confirm that job0 iops == 1000
605     and that job1_iops / job0_iops ~ 8
606     With two runs of fio-3.16 I observed a ratio of 8.3"""
607
608     def check_result(self):
609         super().check_result()
610
611         if not self.passed:
612             return
613
614         iops1 = self.json_data['jobs'][0]['read']['iops']
615         logging.debug("Test %d: iops1: %f", self.testnum, iops1)
616         iops2 = self.json_data['jobs'][1]['read']['iops']
617         logging.debug("Test %d: iops2: %f", self.testnum, iops2)
618         ratio = iops2 / iops1
619         logging.debug("Test %d: ratio: %f", self.testnum, ratio)
620
621         if iops1 < 950 or iops1 > 1050:
622             self.failure_reason = f"{self.failure_reason} iops value mismatch,"
623             self.passed = False
624
625         if ratio < 6 or ratio > 10:
626             self.failure_reason = f"{self.failure_reason} iops ratio mismatch,"
627             self.passed = False
628
629
630 TEST_LIST = [
631     {
632         'test_id':          1,
633         'test_class':       FioJobFileTest,
634         'job':              't0001-52c58027.fio',
635         'success':          SUCCESS_DEFAULT,
636         'pre_job':          None,
637         'pre_success':      None,
638         'requirements':     [],
639     },
640     {
641         'test_id':          2,
642         'test_class':       FioJobFileTest,
643         'job':              't0002-13af05ae-post.fio',
644         'success':          SUCCESS_DEFAULT,
645         'pre_job':          't0002-13af05ae-pre.fio',
646         'pre_success':      None,
647         'requirements':     [Requirements.linux, Requirements.libaio],
648     },
649     {
650         'test_id':          3,
651         'test_class':       FioJobFileTest,
652         'job':              't0003-0ae2c6e1-post.fio',
653         'success':          SUCCESS_NONZERO,
654         'pre_job':          't0003-0ae2c6e1-pre.fio',
655         'pre_success':      SUCCESS_DEFAULT,
656         'requirements':     [Requirements.linux, Requirements.libaio],
657     },
658     {
659         'test_id':          4,
660         'test_class':       FioJobFileTest,
661         'job':              't0004-8a99fdf6.fio',
662         'success':          SUCCESS_DEFAULT,
663         'pre_job':          None,
664         'pre_success':      None,
665         'requirements':     [Requirements.linux, Requirements.libaio],
666     },
667     {
668         'test_id':          5,
669         'test_class':       FioJobFileTest_t0005,
670         'job':              't0005-f7078f7b.fio',
671         'success':          SUCCESS_DEFAULT,
672         'pre_job':          None,
673         'pre_success':      None,
674         'output_format':    'json',
675         'requirements':     [Requirements.not_windows],
676     },
677     {
678         'test_id':          6,
679         'test_class':       FioJobFileTest_t0006,
680         'job':              't0006-82af2a7c.fio',
681         'success':          SUCCESS_DEFAULT,
682         'pre_job':          None,
683         'pre_success':      None,
684         'output_format':    'json',
685         'requirements':     [Requirements.linux, Requirements.libaio],
686     },
687     {
688         'test_id':          7,
689         'test_class':       FioJobFileTest_t0007,
690         'job':              't0007-37cf9e3c.fio',
691         'success':          SUCCESS_DEFAULT,
692         'pre_job':          None,
693         'pre_success':      None,
694         'output_format':    'json',
695         'requirements':     [],
696     },
697     {
698         'test_id':          8,
699         'test_class':       FioJobFileTest_t0008,
700         'job':              't0008-ae2fafc8.fio',
701         'success':          SUCCESS_DEFAULT,
702         'pre_job':          None,
703         'pre_success':      None,
704         'output_format':    'json',
705         'requirements':     [],
706     },
707     {
708         'test_id':          9,
709         'test_class':       FioJobFileTest_t0009,
710         'job':              't0009-f8b0bd10.fio',
711         'success':          SUCCESS_DEFAULT,
712         'pre_job':          None,
713         'pre_success':      None,
714         'output_format':    'json',
715         'requirements':     [Requirements.not_macos,
716                              Requirements.cpucount4],
717         # mac os does not support CPU affinity
718     },
719     {
720         'test_id':          10,
721         'test_class':       FioJobFileTest,
722         'job':              't0010-b7aae4ba.fio',
723         'success':          SUCCESS_DEFAULT,
724         'pre_job':          None,
725         'pre_success':      None,
726         'requirements':     [],
727     },
728     {
729         'test_id':          11,
730         'test_class':       FioJobFileTest_iops_rate,
731         'job':              't0011-5d2788d5.fio',
732         'success':          SUCCESS_DEFAULT,
733         'pre_job':          None,
734         'pre_success':      None,
735         'output_format':    'json',
736         'requirements':     [],
737     },
738     {
739         'test_id':          12,
740         'test_class':       FioJobFileTest_t0012,
741         'job':              't0012.fio',
742         'success':          SUCCESS_DEFAULT,
743         'pre_job':          None,
744         'pre_success':      None,
745         'output_format':    'json',
746         'requirements':     [],
747     },
748     {
749         'test_id':          13,
750         'test_class':       FioJobFileTest,
751         'job':              't0013.fio',
752         'success':          SUCCESS_DEFAULT,
753         'pre_job':          None,
754         'pre_success':      None,
755         'output_format':    'json',
756         'requirements':     [],
757     },
758     {
759         'test_id':          14,
760         'test_class':       FioJobFileTest_t0014,
761         'job':              't0014.fio',
762         'success':          SUCCESS_DEFAULT,
763         'pre_job':          None,
764         'pre_success':      None,
765         'output_format':    'json',
766         'requirements':     [],
767     },
768     {
769         'test_id':          15,
770         'test_class':       FioJobFileTest_t0015,
771         'job':              't0015-4e7e7898.fio',
772         'success':          SUCCESS_DEFAULT,
773         'pre_job':          None,
774         'pre_success':      None,
775         'output_format':    'json',
776         'requirements':     [Requirements.linux, Requirements.libaio],
777     },
778     {
779         'test_id':          16,
780         'test_class':       FioJobFileTest_t0015,
781         'job':              't0016-d54ae22.fio',
782         'success':          SUCCESS_DEFAULT,
783         'pre_job':          None,
784         'pre_success':      None,
785         'output_format':    'json',
786         'requirements':     [],
787     },
788     {
789         'test_id':          17,
790         'test_class':       FioJobFileTest_t0015,
791         'job':              't0017.fio',
792         'success':          SUCCESS_DEFAULT,
793         'pre_job':          None,
794         'pre_success':      None,
795         'output_format':    'json',
796         'requirements':     [Requirements.not_windows],
797     },
798     {
799         'test_id':          18,
800         'test_class':       FioJobFileTest,
801         'job':              't0018.fio',
802         'success':          SUCCESS_DEFAULT,
803         'pre_job':          None,
804         'pre_success':      None,
805         'requirements':     [Requirements.linux, Requirements.io_uring],
806     },
807     {
808         'test_id':          19,
809         'test_class':       FioJobFileTest_t0019,
810         'job':              't0019.fio',
811         'success':          SUCCESS_DEFAULT,
812         'pre_job':          None,
813         'pre_success':      None,
814         'requirements':     [],
815     },
816     {
817         'test_id':          20,
818         'test_class':       FioJobFileTest_t0020,
819         'job':              't0020.fio',
820         'success':          SUCCESS_DEFAULT,
821         'pre_job':          None,
822         'pre_success':      None,
823         'requirements':     [],
824     },
825     {
826         'test_id':          21,
827         'test_class':       FioJobFileTest_t0020,
828         'job':              't0021.fio',
829         'success':          SUCCESS_DEFAULT,
830         'pre_job':          None,
831         'pre_success':      None,
832         'requirements':     [],
833     },
834     {
835         'test_id':          22,
836         'test_class':       FioJobFileTest_t0022,
837         'job':              't0022.fio',
838         'success':          SUCCESS_DEFAULT,
839         'pre_job':          None,
840         'pre_success':      None,
841         'requirements':     [],
842     },
843     {
844         'test_id':          23,
845         'test_class':       FioJobFileTest_t0023,
846         'job':              't0023.fio',
847         'success':          SUCCESS_DEFAULT,
848         'pre_job':          None,
849         'pre_success':      None,
850         'requirements':     [],
851     },
852     {
853         'test_id':          24,
854         'test_class':       FioJobFileTest_t0024,
855         'job':              't0024.fio',
856         'success':          SUCCESS_DEFAULT,
857         'pre_job':          None,
858         'pre_success':      None,
859         'requirements':     [],
860     },
861     {
862         'test_id':          25,
863         'test_class':       FioJobFileTest_t0025,
864         'job':              't0025.fio',
865         'success':          SUCCESS_DEFAULT,
866         'pre_job':          None,
867         'pre_success':      None,
868         'output_format':    'json',
869         'requirements':     [],
870     },
871     {
872         'test_id':          26,
873         'test_class':       FioJobFileTest,
874         'job':              't0026.fio',
875         'success':          SUCCESS_DEFAULT,
876         'pre_job':          None,
877         'pre_success':      None,
878         'requirements':     [Requirements.not_windows],
879     },
880     {
881         'test_id':          27,
882         'test_class':       FioJobFileTest_t0027,
883         'job':              't0027.fio',
884         'success':          SUCCESS_DEFAULT,
885         'pre_job':          None,
886         'pre_success':      None,
887         'requirements':     [],
888     },
889     {
890         'test_id':          28,
891         'test_class':       FioJobFileTest,
892         'job':              't0028-c6cade16.fio',
893         'success':          SUCCESS_DEFAULT,
894         'pre_job':          None,
895         'pre_success':      None,
896         'requirements':     [],
897     },
898     {
899         'test_id':          29,
900         'test_class':       FioJobFileTest_t0029,
901         'job':              't0029.fio',
902         'success':          SUCCESS_DEFAULT,
903         'pre_job':          None,
904         'pre_success':      None,
905         'output_format':    'json',
906         'requirements':     [],
907     },
908     {
909         'test_id':          30,
910         'test_class':       FioJobFileTest,
911         'job':              't0030.fio',
912         'success':          SUCCESS_DEFAULT,
913         'pre_job':          None,
914         'pre_success':      None,
915         'parameters':       ['--bandwidth-log'],
916         'requirements':     [],
917     },
918     {
919         'test_id':          31,
920         'test_class':       FioJobFileTest,
921         'job':              't0031.fio',
922         'success':          SUCCESS_DEFAULT,
923         'pre_job':          't0031-pre.fio',
924         'pre_success':      SUCCESS_DEFAULT,
925         'requirements':     [Requirements.linux, Requirements.libaio],
926     },
927     {
928         'test_id':          33,
929         'test_class':       FioJobFileTest_t0033,
930         'job':              't0033.fio',
931         'success':          SUCCESS_DEFAULT,
932         'pre_job':          None,
933         'pre_success':      None,
934         'requirements':     [Requirements.linux, Requirements.libaio],
935     },
936     {
937         'test_id':          34,
938         'test_class':       FioJobFileTest_t0034,
939         'job':              't0034.fio',
940         'success':          SUCCESS_DEFAULT,
941         'pre_job':          None,
942         'pre_success':      None,
943         'requirements':     [Requirements.linux, Requirements.libaio],
944     },
945     {
946         'test_id':          35,
947         'test_class':       FioJobFileTest,
948         'job':              't0035.fio',
949         'success':          SUCCESS_DEFAULT,
950         'pre_job':          None,
951         'pre_success':      None,
952         'requirements':     [],
953     },
954     {
955         'test_id':          36,
956         'test_class':       FioJobFileTest,
957         'job':              't0036-post.fio',
958         'success':          SUCCESS_DEFAULT,
959         'pre_job':          't0036-pre.fio',
960         'pre_success':      SUCCESS_DEFAULT,
961         'requirements':     [],
962     },
963     {
964         'test_id':          37,
965         'test_class':       FioJobFileTest,
966         'job':              't0037-post.fio',
967         'success':          SUCCESS_DEFAULT,
968         'pre_job':          't0037-pre.fio',
969         'pre_success':      SUCCESS_DEFAULT,
970         'requirements':     [Requirements.linux, Requirements.libaio],
971     },
972     {
973         'test_id':          1000,
974         'test_class':       FioExeTest,
975         'exe':              't/axmap',
976         'parameters':       None,
977         'success':          SUCCESS_DEFAULT,
978         'requirements':     [],
979     },
980     {
981         'test_id':          1001,
982         'test_class':       FioExeTest,
983         'exe':              't/ieee754',
984         'parameters':       None,
985         'success':          SUCCESS_DEFAULT,
986         'requirements':     [],
987     },
988     {
989         'test_id':          1002,
990         'test_class':       FioExeTest,
991         'exe':              't/lfsr-test',
992         'parameters':       ['0xFFFFFF', '0', '0', 'verify'],
993         'success':          SUCCESS_STDERR,
994         'requirements':     [],
995     },
996     {
997         'test_id':          1003,
998         'test_class':       FioExeTest,
999         'exe':              't/readonly.py',
1000         'parameters':       ['-f', '{fio_path}'],
1001         'success':          SUCCESS_DEFAULT,
1002         'requirements':     [],
1003     },
1004     {
1005         'test_id':          1004,
1006         'test_class':       FioExeTest,
1007         'exe':              't/steadystate_tests.py',
1008         'parameters':       ['{fio_path}'],
1009         'success':          SUCCESS_DEFAULT,
1010         'requirements':     [],
1011     },
1012     {
1013         'test_id':          1005,
1014         'test_class':       FioExeTest,
1015         'exe':              't/stest',
1016         'parameters':       None,
1017         'success':          SUCCESS_STDERR,
1018         'requirements':     [],
1019     },
1020     {
1021         'test_id':          1006,
1022         'test_class':       FioExeTest,
1023         'exe':              't/strided.py',
1024         'parameters':       ['--fio', '{fio_path}'],
1025         'success':          SUCCESS_DEFAULT,
1026         'requirements':     [],
1027     },
1028     {
1029         'test_id':          1007,
1030         'test_class':       FioExeTest,
1031         'exe':              't/zbd/run-tests-against-nullb',
1032         'parameters':       ['-s', '1'],
1033         'success':          SUCCESS_DEFAULT,
1034         'requirements':     [Requirements.linux, Requirements.zbd,
1035                              Requirements.root],
1036     },
1037     {
1038         'test_id':          1008,
1039         'test_class':       FioExeTest,
1040         'exe':              't/zbd/run-tests-against-nullb',
1041         'parameters':       ['-s', '2'],
1042         'success':          SUCCESS_DEFAULT,
1043         'requirements':     [Requirements.linux, Requirements.zbd,
1044                              Requirements.root, Requirements.zoned_nullb],
1045     },
1046     {
1047         'test_id':          1009,
1048         'test_class':       FioExeTest,
1049         'exe':              'unittests/unittest',
1050         'parameters':       None,
1051         'success':          SUCCESS_DEFAULT,
1052         'requirements':     [Requirements.unittests],
1053     },
1054     {
1055         'test_id':          1010,
1056         'test_class':       FioExeTest,
1057         'exe':              't/latency_percentiles.py',
1058         'parameters':       ['-f', '{fio_path}'],
1059         'success':          SUCCESS_DEFAULT,
1060         'requirements':     [],
1061     },
1062     {
1063         'test_id':          1011,
1064         'test_class':       FioExeTest,
1065         'exe':              't/jsonplus2csv_test.py',
1066         'parameters':       ['-f', '{fio_path}'],
1067         'success':          SUCCESS_DEFAULT,
1068         'requirements':     [],
1069     },
1070     {
1071         'test_id':          1012,
1072         'test_class':       FioExeTest,
1073         'exe':              't/log_compression.py',
1074         'parameters':       ['-f', '{fio_path}'],
1075         'success':          SUCCESS_DEFAULT,
1076         'requirements':     [],
1077     },
1078     {
1079         'test_id':          1013,
1080         'test_class':       FioExeTest,
1081         'exe':              't/random_seed.py',
1082         'parameters':       ['-f', '{fio_path}'],
1083         'success':          SUCCESS_DEFAULT,
1084         'requirements':     [],
1085     },
1086     {
1087         'test_id':          1014,
1088         'test_class':       FioExeTest,
1089         'exe':              't/nvmept.py',
1090         'parameters':       ['-f', '{fio_path}', '--dut', '{nvmecdev}'],
1091         'success':          SUCCESS_DEFAULT,
1092         'requirements':     [Requirements.linux, Requirements.nvmecdev],
1093     },
1094     {
1095         'test_id':          1015,
1096         'test_class':       FioExeTest,
1097         'exe':              't/nvmept_trim.py',
1098         'parameters':       ['-f', '{fio_path}', '--dut', '{nvmecdev}'],
1099         'success':          SUCCESS_DEFAULT,
1100         'requirements':     [Requirements.linux, Requirements.nvmecdev],
1101     },
1102     {
1103         'test_id':          1016,
1104         'test_class':       FioExeTest,
1105         'exe':              't/client_server.py',
1106         'parameters':       ['-f', '{fio_path}'],
1107         'success':          SUCCESS_DEFAULT,
1108         'requirements':     [Requirements.linux],
1109     },
1110     {
1111         'test_id':          1017,
1112         'test_class':       FioExeTest,
1113         'exe':              't/verify.py',
1114         'parameters':       ['-f', '{fio_path}'],
1115         'success':          SUCCESS_LONG,
1116         'requirements':     [],
1117     },
1118     {
1119         'test_id':          1018,
1120         'test_class':       FioExeTest,
1121         'exe':              't/verify-trim.py',
1122         'parameters':       ['-f', '{fio_path}'],
1123         'success':          SUCCESS_DEFAULT,
1124         'requirements':     [Requirements.linux],
1125     },
1126 ]
1127
1128
1129 def parse_args():
1130     """Parse command-line arguments."""
1131
1132     parser = argparse.ArgumentParser()
1133     parser.add_argument('-r', '--fio-root',
1134                         help='fio root path')
1135     parser.add_argument('-f', '--fio',
1136                         help='path to fio executable (e.g., ./fio)')
1137     parser.add_argument('-a', '--artifact-root',
1138                         help='artifact root directory')
1139     parser.add_argument('-s', '--skip', nargs='+', type=int,
1140                         help='list of test(s) to skip')
1141     parser.add_argument('-o', '--run-only', nargs='+', type=int,
1142                         help='list of test(s) to run, skipping all others')
1143     parser.add_argument('-d', '--debug', action='store_true',
1144                         help='provide debug output')
1145     parser.add_argument('-k', '--skip-req', action='store_true',
1146                         help='skip requirements checking')
1147     parser.add_argument('-p', '--pass-through', action='append',
1148                         help='pass-through an argument to an executable test')
1149     parser.add_argument('--nvmecdev', action='store', default=None,
1150                         help='NVMe character device for **DESTRUCTIVE** testing (e.g., /dev/ng0n1)')
1151     args = parser.parse_args()
1152
1153     return args
1154
1155
1156 def main():
1157     """Entry point."""
1158
1159     args = parse_args()
1160     if args.debug:
1161         logging.basicConfig(level=logging.DEBUG)
1162     else:
1163         logging.basicConfig(level=logging.INFO)
1164
1165     pass_through = {}
1166     if args.pass_through:
1167         for arg in args.pass_through:
1168             if not ':' in arg:
1169                 print(f"Invalid --pass-through argument '{arg}'")
1170                 print("Syntax for --pass-through is TESTNUMBER:ARGUMENT")
1171                 return
1172             split = arg.split(":", 1)
1173             pass_through[int(split[0])] = split[1]
1174         logging.debug("Pass-through arguments: %s", pass_through)
1175
1176     if args.fio_root:
1177         fio_root = args.fio_root
1178     else:
1179         fio_root = str(Path(__file__).absolute().parent.parent)
1180     print(f"fio root is {fio_root}")
1181
1182     if args.fio:
1183         fio_path = args.fio
1184     else:
1185         if platform.system() == "Windows":
1186             fio_exe = "fio.exe"
1187         else:
1188             fio_exe = "fio"
1189         fio_path = os.path.join(fio_root, fio_exe)
1190     print(f"fio path is {fio_path}")
1191     if not shutil.which(fio_path):
1192         print("Warning: fio executable not found")
1193
1194     artifact_root = args.artifact_root if args.artifact_root else \
1195         f"fio-test-{time.strftime('%Y%m%d-%H%M%S')}"
1196     os.mkdir(artifact_root)
1197     print(f"Artifact directory is {artifact_root}")
1198
1199     if not args.skip_req:
1200         Requirements(fio_root, args)
1201
1202     test_env = {
1203               'fio_path': fio_path,
1204               'fio_root': fio_root,
1205               'artifact_root': artifact_root,
1206               'pass_through': pass_through,
1207               }
1208     _, failed, _ = run_fio_tests(TEST_LIST, test_env, args)
1209     sys.exit(failed)
1210
1211
1212 if __name__ == '__main__':
1213     main()