test: add large pattern test
[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 json
47 import time
48 import shutil
49 import logging
50 import argparse
51 import platform
52 import traceback
53 import subprocess
54 import multiprocessing
55 from pathlib import Path
56
57
58 class FioTest():
59     """Base for all fio tests."""
60
61     def __init__(self, exe_path, parameters, success):
62         self.exe_path = exe_path
63         self.parameters = parameters
64         self.success = success
65         self.output = {}
66         self.artifact_root = None
67         self.testnum = None
68         self.test_dir = None
69         self.passed = True
70         self.failure_reason = ''
71         self.command_file = None
72         self.stdout_file = None
73         self.stderr_file = None
74         self.exitcode_file = None
75
76     def setup(self, artifact_root, testnum):
77         """Setup instance variables for test."""
78
79         self.artifact_root = artifact_root
80         self.testnum = testnum
81         self.test_dir = os.path.join(artifact_root, "{:04d}".format(testnum))
82         if not os.path.exists(self.test_dir):
83             os.mkdir(self.test_dir)
84
85         self.command_file = os.path.join(
86             self.test_dir,
87             "{0}.command".format(os.path.basename(self.exe_path)))
88         self.stdout_file = os.path.join(
89             self.test_dir,
90             "{0}.stdout".format(os.path.basename(self.exe_path)))
91         self.stderr_file = os.path.join(
92             self.test_dir,
93             "{0}.stderr".format(os.path.basename(self.exe_path)))
94         self.exitcode_file = os.path.join(
95             self.test_dir,
96             "{0}.exitcode".format(os.path.basename(self.exe_path)))
97
98     def run(self):
99         """Run the test."""
100
101         raise NotImplementedError()
102
103     def check_result(self):
104         """Check test results."""
105
106         raise NotImplementedError()
107
108
109 class FioExeTest(FioTest):
110     """Test consists of an executable binary or script"""
111
112     def __init__(self, exe_path, parameters, success):
113         """Construct a FioExeTest which is a FioTest consisting of an
114         executable binary or script.
115
116         exe_path:       location of executable binary or script
117         parameters:     list of parameters for executable
118         success:        Definition of test success
119         """
120
121         FioTest.__init__(self, exe_path, parameters, success)
122
123     def run(self):
124         """Execute the binary or script described by this instance."""
125
126         command = [self.exe_path] + self.parameters
127         command_file = open(self.command_file, "w+")
128         command_file.write("%s\n" % command)
129         command_file.close()
130
131         stdout_file = open(self.stdout_file, "w+")
132         stderr_file = open(self.stderr_file, "w+")
133         exitcode_file = open(self.exitcode_file, "w+")
134         try:
135             proc = None
136             # Avoid using subprocess.run() here because when a timeout occurs,
137             # fio will be stopped with SIGKILL. This does not give fio a
138             # chance to clean up and means that child processes may continue
139             # running and submitting IO.
140             proc = subprocess.Popen(command,
141                                     stdout=stdout_file,
142                                     stderr=stderr_file,
143                                     cwd=self.test_dir,
144                                     universal_newlines=True)
145             proc.communicate(timeout=self.success['timeout'])
146             exitcode_file.write('{0}\n'.format(proc.returncode))
147             logging.debug("Test %d: return code: %d", self.testnum, proc.returncode)
148             self.output['proc'] = proc
149         except subprocess.TimeoutExpired:
150             proc.terminate()
151             proc.communicate()
152             assert proc.poll()
153             self.output['failure'] = 'timeout'
154         except Exception:
155             if proc:
156                 if not proc.poll():
157                     proc.terminate()
158                     proc.communicate()
159             self.output['failure'] = 'exception'
160             self.output['exc_info'] = sys.exc_info()
161         finally:
162             stdout_file.close()
163             stderr_file.close()
164             exitcode_file.close()
165
166     def check_result(self):
167         """Check results of test run."""
168
169         if 'proc' not in self.output:
170             if self.output['failure'] == 'timeout':
171                 self.failure_reason = "{0} timeout,".format(self.failure_reason)
172             else:
173                 assert self.output['failure'] == 'exception'
174                 self.failure_reason = '{0} exception: {1}, {2}'.format(
175                     self.failure_reason, self.output['exc_info'][0],
176                     self.output['exc_info'][1])
177
178             self.passed = False
179             return
180
181         if 'zero_return' in self.success:
182             if self.success['zero_return']:
183                 if self.output['proc'].returncode != 0:
184                     self.passed = False
185                     self.failure_reason = "{0} non-zero return code,".format(self.failure_reason)
186             else:
187                 if self.output['proc'].returncode == 0:
188                     self.failure_reason = "{0} zero return code,".format(self.failure_reason)
189                     self.passed = False
190
191         stderr_size = os.path.getsize(self.stderr_file)
192         if 'stderr_empty' in self.success:
193             if self.success['stderr_empty']:
194                 if stderr_size != 0:
195                     self.failure_reason = "{0} stderr not empty,".format(self.failure_reason)
196                     self.passed = False
197             else:
198                 if stderr_size == 0:
199                     self.failure_reason = "{0} stderr empty,".format(self.failure_reason)
200                     self.passed = False
201
202
203 class FioJobTest(FioExeTest):
204     """Test consists of a fio job"""
205
206     def __init__(self, fio_path, fio_job, success, fio_pre_job=None,
207                  fio_pre_success=None, output_format="normal"):
208         """Construct a FioJobTest which is a FioExeTest consisting of a
209         single fio job file with an optional setup step.
210
211         fio_path:           location of fio executable
212         fio_job:            location of fio job file
213         success:            Definition of test success
214         fio_pre_job:        fio job for preconditioning
215         fio_pre_success:    Definition of test success for fio precon job
216         output_format:      normal (default), json, jsonplus, or terse
217         """
218
219         self.fio_job = fio_job
220         self.fio_pre_job = fio_pre_job
221         self.fio_pre_success = fio_pre_success if fio_pre_success else success
222         self.output_format = output_format
223         self.precon_failed = False
224         self.json_data = None
225         self.fio_output = "{0}.output".format(os.path.basename(self.fio_job))
226         self.fio_args = [
227             "--max-jobs=16",
228             "--output-format={0}".format(self.output_format),
229             "--output={0}".format(self.fio_output),
230             self.fio_job,
231             ]
232         FioExeTest.__init__(self, fio_path, self.fio_args, success)
233
234     def setup(self, artifact_root, testnum):
235         """Setup instance variables for fio job test."""
236
237         super(FioJobTest, self).setup(artifact_root, testnum)
238
239         self.command_file = os.path.join(
240             self.test_dir,
241             "{0}.command".format(os.path.basename(self.fio_job)))
242         self.stdout_file = os.path.join(
243             self.test_dir,
244             "{0}.stdout".format(os.path.basename(self.fio_job)))
245         self.stderr_file = os.path.join(
246             self.test_dir,
247             "{0}.stderr".format(os.path.basename(self.fio_job)))
248         self.exitcode_file = os.path.join(
249             self.test_dir,
250             "{0}.exitcode".format(os.path.basename(self.fio_job)))
251
252     def run_pre_job(self):
253         """Run fio job precondition step."""
254
255         precon = FioJobTest(self.exe_path, self.fio_pre_job,
256                             self.fio_pre_success,
257                             output_format=self.output_format)
258         precon.setup(self.artifact_root, self.testnum)
259         precon.run()
260         precon.check_result()
261         self.precon_failed = not precon.passed
262         self.failure_reason = precon.failure_reason
263
264     def run(self):
265         """Run fio job test."""
266
267         if self.fio_pre_job:
268             self.run_pre_job()
269
270         if not self.precon_failed:
271             super(FioJobTest, self).run()
272         else:
273             logging.debug("Test %d: precondition step failed", self.testnum)
274
275     @classmethod
276     def get_file(cls, filename):
277         """Safely read a file."""
278         file_data = ''
279         success = True
280
281         try:
282             with open(filename, "r") as output_file:
283                 file_data = output_file.read()
284         except OSError:
285             success = False
286
287         return file_data, success
288
289     def get_file_fail(self, filename):
290         """Safely read a file and fail the test upon error."""
291         file_data = None
292
293         try:
294             with open(filename, "r") as output_file:
295                 file_data = output_file.read()
296         except OSError:
297             self.failure_reason += " unable to read file {0}".format(filename)
298             self.passed = False
299
300         return file_data
301
302     def check_result(self):
303         """Check fio job results."""
304
305         if self.precon_failed:
306             self.passed = False
307             self.failure_reason = "{0} precondition step failed,".format(self.failure_reason)
308             return
309
310         super(FioJobTest, self).check_result()
311
312         if not self.passed:
313             return
314
315         if 'json' not in self.output_format:
316             return
317
318         file_data = self.get_file_fail(os.path.join(self.test_dir, self.fio_output))
319         if not file_data:
320             return
321
322         #
323         # Sometimes fio informational messages are included at the top of the
324         # JSON output, especially under Windows. Try to decode output as JSON
325         # data, skipping everything until the first {
326         #
327         lines = file_data.splitlines()
328         file_data = '\n'.join(lines[lines.index("{"):])
329         try:
330             self.json_data = json.loads(file_data)
331         except json.JSONDecodeError:
332             self.failure_reason = "{0} unable to decode JSON data,".format(self.failure_reason)
333             self.passed = False
334
335
336 class FioJobTest_t0005(FioJobTest):
337     """Test consists of fio test job t0005
338     Confirm that read['io_kbytes'] == write['io_kbytes'] == 102400"""
339
340     def check_result(self):
341         super(FioJobTest_t0005, self).check_result()
342
343         if not self.passed:
344             return
345
346         if self.json_data['jobs'][0]['read']['io_kbytes'] != 102400:
347             self.failure_reason = "{0} bytes read mismatch,".format(self.failure_reason)
348             self.passed = False
349         if self.json_data['jobs'][0]['write']['io_kbytes'] != 102400:
350             self.failure_reason = "{0} bytes written mismatch,".format(self.failure_reason)
351             self.passed = False
352
353
354 class FioJobTest_t0006(FioJobTest):
355     """Test consists of fio test job t0006
356     Confirm that read['io_kbytes'] ~ 2*write['io_kbytes']"""
357
358     def check_result(self):
359         super(FioJobTest_t0006, self).check_result()
360
361         if not self.passed:
362             return
363
364         ratio = self.json_data['jobs'][0]['read']['io_kbytes'] \
365             / self.json_data['jobs'][0]['write']['io_kbytes']
366         logging.debug("Test %d: ratio: %f", self.testnum, ratio)
367         if ratio < 1.99 or ratio > 2.01:
368             self.failure_reason = "{0} read/write ratio mismatch,".format(self.failure_reason)
369             self.passed = False
370
371
372 class FioJobTest_t0007(FioJobTest):
373     """Test consists of fio test job t0007
374     Confirm that read['io_kbytes'] = 87040"""
375
376     def check_result(self):
377         super(FioJobTest_t0007, self).check_result()
378
379         if not self.passed:
380             return
381
382         if self.json_data['jobs'][0]['read']['io_kbytes'] != 87040:
383             self.failure_reason = "{0} bytes read mismatch,".format(self.failure_reason)
384             self.passed = False
385
386
387 class FioJobTest_t0008(FioJobTest):
388     """Test consists of fio test job t0008
389     Confirm that read['io_kbytes'] = 32768 and that
390                 write['io_kbytes'] ~ 16568
391
392     I did runs with fio-ae2fafc8 and saw write['io_kbytes'] values of
393     16585, 16588. With two runs of fio-3.16 I obtained 16568"""
394
395     def check_result(self):
396         super(FioJobTest_t0008, self).check_result()
397
398         if not self.passed:
399             return
400
401         ratio = self.json_data['jobs'][0]['write']['io_kbytes'] / 16568
402         logging.debug("Test %d: ratio: %f", self.testnum, ratio)
403
404         if ratio < 0.99 or ratio > 1.01:
405             self.failure_reason = "{0} bytes written mismatch,".format(self.failure_reason)
406             self.passed = False
407         if self.json_data['jobs'][0]['read']['io_kbytes'] != 32768:
408             self.failure_reason = "{0} bytes read mismatch,".format(self.failure_reason)
409             self.passed = False
410
411
412 class FioJobTest_t0009(FioJobTest):
413     """Test consists of fio test job t0009
414     Confirm that runtime >= 60s"""
415
416     def check_result(self):
417         super(FioJobTest_t0009, self).check_result()
418
419         if not self.passed:
420             return
421
422         logging.debug('Test %d: elapsed: %d', self.testnum, self.json_data['jobs'][0]['elapsed'])
423
424         if self.json_data['jobs'][0]['elapsed'] < 60:
425             self.failure_reason = "{0} elapsed time mismatch,".format(self.failure_reason)
426             self.passed = False
427
428
429 class FioJobTest_t0012(FioJobTest):
430     """Test consists of fio test job t0012
431     Confirm ratios of job iops are 1:5:10
432     job1,job2,job3 respectively"""
433
434     def check_result(self):
435         super(FioJobTest_t0012, self).check_result()
436
437         if not self.passed:
438             return
439
440         iops_files = []
441         for i in range(1, 4):
442             filename = os.path.join(self.test_dir, "{0}_iops.{1}.log".format(os.path.basename(
443                 self.fio_job), i))
444             file_data = self.get_file_fail(filename)
445             if not file_data:
446                 return
447
448             iops_files.append(file_data.splitlines())
449
450         # there are 9 samples for job1 and job2, 4 samples for job3
451         iops1 = 0.0
452         iops2 = 0.0
453         iops3 = 0.0
454         for i in range(9):
455             iops1 = iops1 + float(iops_files[0][i].split(',')[1])
456             iops2 = iops2 + float(iops_files[1][i].split(',')[1])
457             iops3 = iops3 + float(iops_files[2][i].split(',')[1])
458
459             ratio1 = iops3/iops2
460             ratio2 = iops3/iops1
461             logging.debug("sample {0}: job1 iops={1} job2 iops={2} job3 iops={3} " \
462                 "job3/job2={4:.3f} job3/job1={5:.3f}".format(i, iops1, iops2, iops3, ratio1,
463                                                              ratio2))
464
465         # test job1 and job2 succeeded to recalibrate
466         if ratio1 < 1 or ratio1 > 3 or ratio2 < 7 or ratio2 > 13:
467             self.failure_reason += " iops ratio mismatch iops1={0} iops2={1} iops3={2} " \
468                 "expected r1~2 r2~10 got r1={3:.3f} r2={4:.3f},".format(iops1, iops2, iops3,
469                                                                         ratio1, ratio2)
470             self.passed = False
471             return
472
473
474 class FioJobTest_t0014(FioJobTest):
475     """Test consists of fio test job t0014
476         Confirm that job1_iops / job2_iops ~ 1:2 for entire duration
477         and that job1_iops / job3_iops ~ 1:3 for first half of duration.
478
479     The test is about making sure the flow feature can
480     re-calibrate the activity dynamically"""
481
482     def check_result(self):
483         super(FioJobTest_t0014, self).check_result()
484
485         if not self.passed:
486             return
487
488         iops_files = []
489         for i in range(1, 4):
490             filename = os.path.join(self.test_dir, "{0}_iops.{1}.log".format(os.path.basename(
491                 self.fio_job), i))
492             file_data = self.get_file_fail(filename)
493             if not file_data:
494                 return
495
496             iops_files.append(file_data.splitlines())
497
498         # there are 9 samples for job1 and job2, 4 samples for job3
499         iops1 = 0.0
500         iops2 = 0.0
501         iops3 = 0.0
502         for i in range(9):
503             if i < 4:
504                 iops3 = iops3 + float(iops_files[2][i].split(',')[1])
505             elif i == 4:
506                 ratio1 = iops1 / iops2
507                 ratio2 = iops1 / iops3
508
509
510                 if ratio1 < 0.43 or ratio1 > 0.57 or ratio2 < 0.21 or ratio2 > 0.45:
511                     self.failure_reason += " iops ratio mismatch iops1={0} iops2={1} iops3={2} " \
512                                            "expected r1~0.5 r2~0.33 got r1={3:.3f} r2={4:.3f},".format(
513                                                iops1, iops2, iops3, ratio1, ratio2)
514                     self.passed = False
515
516             iops1 = iops1 + float(iops_files[0][i].split(',')[1])
517             iops2 = iops2 + float(iops_files[1][i].split(',')[1])
518
519             ratio1 = iops1/iops2
520             ratio2 = iops1/iops3
521             logging.debug("sample {0}: job1 iops={1} job2 iops={2} job3 iops={3} " \
522                           "job1/job2={4:.3f} job1/job3={5:.3f}".format(i, iops1, iops2, iops3,
523                                                                        ratio1, ratio2))
524
525         # test job1 and job2 succeeded to recalibrate
526         if ratio1 < 0.43 or ratio1 > 0.57:
527             self.failure_reason += " iops ratio mismatch iops1={0} iops2={1} expected ratio~0.5 " \
528                                    "got ratio={2:.3f},".format(iops1, iops2, ratio1)
529             self.passed = False
530             return
531
532
533 class FioJobTest_t0015(FioJobTest):
534     """Test consists of fio test jobs t0015 and t0016
535     Confirm that mean(slat) + mean(clat) = mean(tlat)"""
536
537     def check_result(self):
538         super(FioJobTest_t0015, self).check_result()
539
540         if not self.passed:
541             return
542
543         slat = self.json_data['jobs'][0]['read']['slat_ns']['mean']
544         clat = self.json_data['jobs'][0]['read']['clat_ns']['mean']
545         tlat = self.json_data['jobs'][0]['read']['lat_ns']['mean']
546         logging.debug('Test %d: slat %f, clat %f, tlat %f', self.testnum, slat, clat, tlat)
547
548         if abs(slat + clat - tlat) > 1:
549             self.failure_reason = "{0} slat {1} + clat {2} = {3} != tlat {4},".format(
550                 self.failure_reason, slat, clat, slat+clat, tlat)
551             self.passed = False
552
553
554 class FioJobTest_t0019(FioJobTest):
555     """Test consists of fio test job t0019
556     Confirm that all offsets were touched sequentially"""
557
558     def check_result(self):
559         super(FioJobTest_t0019, self).check_result()
560
561         bw_log_filename = os.path.join(self.test_dir, "test_bw.log")
562         file_data = self.get_file_fail(bw_log_filename)
563         if not file_data:
564             return
565
566         log_lines = file_data.split('\n')
567
568         prev = -4096
569         for line in log_lines:
570             if len(line.strip()) == 0:
571                 continue
572             cur = int(line.split(',')[4])
573             if cur - prev != 4096:
574                 self.passed = False
575                 self.failure_reason = "offsets {0}, {1} not sequential".format(prev, cur)
576                 return
577             prev = cur
578
579         if cur/4096 != 255:
580             self.passed = False
581             self.failure_reason = "unexpected last offset {0}".format(cur)
582
583
584 class FioJobTest_t0020(FioJobTest):
585     """Test consists of fio test jobs t0020 and t0021
586     Confirm that almost all offsets were touched non-sequentially"""
587
588     def check_result(self):
589         super(FioJobTest_t0020, self).check_result()
590
591         bw_log_filename = os.path.join(self.test_dir, "test_bw.log")
592         file_data = self.get_file_fail(bw_log_filename)
593         if not file_data:
594             return
595
596         log_lines = file_data.split('\n')
597
598         seq_count = 0
599         offsets = set()
600
601         prev = int(log_lines[0].split(',')[4])
602         for line in log_lines[1:]:
603             offsets.add(prev/4096)
604             if len(line.strip()) == 0:
605                 continue
606             cur = int(line.split(',')[4])
607             if cur - prev == 4096:
608                 seq_count += 1
609             prev = cur
610
611         # 10 is an arbitrary threshold
612         if seq_count > 10:
613             self.passed = False
614             self.failure_reason = "too many ({0}) consecutive offsets".format(seq_count)
615
616         if len(offsets) != 256:
617             self.passed = False
618             self.failure_reason += " number of offsets is {0} instead of 256".format(len(offsets))
619
620         for i in range(256):
621             if not i in offsets:
622                 self.passed = False
623                 self.failure_reason += " missing offset {0}".format(i*4096)
624
625
626 class FioJobTest_t0022(FioJobTest):
627     """Test consists of fio test job t0022"""
628
629     def check_result(self):
630         super(FioJobTest_t0022, self).check_result()
631
632         bw_log_filename = os.path.join(self.test_dir, "test_bw.log")
633         file_data = self.get_file_fail(bw_log_filename)
634         if not file_data:
635             return
636
637         log_lines = file_data.split('\n')
638
639         filesize = 1024*1024
640         bs = 4096
641         seq_count = 0
642         offsets = set()
643
644         prev = int(log_lines[0].split(',')[4])
645         for line in log_lines[1:]:
646             offsets.add(prev/bs)
647             if len(line.strip()) == 0:
648                 continue
649             cur = int(line.split(',')[4])
650             if cur - prev == bs:
651                 seq_count += 1
652             prev = cur
653
654         # 10 is an arbitrary threshold
655         if seq_count > 10:
656             self.passed = False
657             self.failure_reason = "too many ({0}) consecutive offsets".format(seq_count)
658
659         if len(offsets) == filesize/bs:
660             self.passed = False
661             self.failure_reason += " no duplicate offsets found with norandommap=1"
662
663
664 class FioJobTest_t0023(FioJobTest):
665     """Test consists of fio test job t0023 randtrimwrite test."""
666
667     def check_trimwrite(self, filename):
668         """Make sure that trims are followed by writes of the same size at the same offset."""
669
670         bw_log_filename = os.path.join(self.test_dir, filename)
671         file_data = self.get_file_fail(bw_log_filename)
672         if not file_data:
673             return
674
675         log_lines = file_data.split('\n')
676
677         prev_ddir = 1
678         for line in log_lines:
679             if len(line.strip()) == 0:
680                 continue
681             vals = line.split(',')
682             ddir = int(vals[2])
683             bs = int(vals[3])
684             offset = int(vals[4])
685             if prev_ddir == 1:
686                 if ddir != 2:
687                     self.passed = False
688                     self.failure_reason += " {0}: write not preceeded by trim: {1}".format(
689                         bw_log_filename, line)
690                     break
691             else:
692                 if ddir != 1:
693                     self.passed = False
694                     self.failure_reason += " {0}: trim not preceeded by write: {1}".format(
695                         bw_log_filename, line)
696                     break
697                 else:
698                     if prev_bs != bs:
699                         self.passed = False
700                         self.failure_reason += " {0}: block size does not match: {1}".format(
701                             bw_log_filename, line)
702                         break
703                     if prev_offset != offset:
704                         self.passed = False
705                         self.failure_reason += " {0}: offset does not match: {1}".format(
706                             bw_log_filename, line)
707                         break
708             prev_ddir = ddir
709             prev_bs = bs
710             prev_offset = offset
711
712
713     def check_all_offsets(self, filename, sectorsize, filesize):
714         """Make sure all offsets were touched."""
715
716         file_data = self.get_file_fail(os.path.join(self.test_dir, filename))
717         if not file_data:
718             return
719
720         log_lines = file_data.split('\n')
721
722         offsets = set()
723
724         for line in log_lines:
725             if len(line.strip()) == 0:
726                 continue
727             vals = line.split(',')
728             bs = int(vals[3])
729             offset = int(vals[4])
730             if offset % sectorsize != 0:
731                 self.passed = False
732                 self.failure_reason += " {0}: offset {1} not a multiple of sector size {2}".format(
733                     filename, offset, sectorsize)
734                 break
735             if bs % sectorsize != 0:
736                 self.passed = False
737                 self.failure_reason += " {0}: block size {1} not a multiple of sector size " \
738                     "{2}".format(filename, bs, sectorsize)
739                 break
740             for i in range(int(bs/sectorsize)):
741                 offsets.add(offset/sectorsize + i)
742
743         if len(offsets) != filesize/sectorsize:
744             self.passed = False
745             self.failure_reason += " {0}: only {1} offsets touched; expected {2}".format(
746                 filename, len(offsets), filesize/sectorsize)
747         else:
748             logging.debug("%s: %d sectors touched", filename, len(offsets))
749
750
751     def check_result(self):
752         super(FioJobTest_t0023, self).check_result()
753
754         filesize = 1024*1024
755
756         self.check_trimwrite("basic_bw.log")
757         self.check_trimwrite("bs_bw.log")
758         self.check_trimwrite("bsrange_bw.log")
759         self.check_trimwrite("bssplit_bw.log")
760         self.check_trimwrite("basic_no_rm_bw.log")
761         self.check_trimwrite("bs_no_rm_bw.log")
762         self.check_trimwrite("bsrange_no_rm_bw.log")
763         self.check_trimwrite("bssplit_no_rm_bw.log")
764
765         self.check_all_offsets("basic_bw.log", 4096, filesize)
766         self.check_all_offsets("bs_bw.log", 8192, filesize)
767         self.check_all_offsets("bsrange_bw.log", 512, filesize)
768         self.check_all_offsets("bssplit_bw.log", 512, filesize)
769
770
771 class FioJobTest_t0024(FioJobTest_t0023):
772     """Test consists of fio test job t0024 trimwrite test."""
773
774     def check_result(self):
775         # call FioJobTest_t0023's parent to skip checks done by t0023
776         super(FioJobTest_t0023, self).check_result()
777
778         filesize = 1024*1024
779
780         self.check_trimwrite("basic_bw.log")
781         self.check_trimwrite("bs_bw.log")
782         self.check_trimwrite("bsrange_bw.log")
783         self.check_trimwrite("bssplit_bw.log")
784
785         self.check_all_offsets("basic_bw.log", 4096, filesize)
786         self.check_all_offsets("bs_bw.log", 8192, filesize)
787         self.check_all_offsets("bsrange_bw.log", 512, filesize)
788         self.check_all_offsets("bssplit_bw.log", 512, filesize)
789
790
791 class FioJobTest_t0025(FioJobTest):
792     """Test experimental verify read backs written data pattern."""
793     def check_result(self):
794         super(FioJobTest_t0025, self).check_result()
795
796         if not self.passed:
797             return
798
799         if self.json_data['jobs'][0]['read']['io_kbytes'] != 128:
800             self.passed = False
801
802 class FioJobTest_t0027(FioJobTest):
803     def setup(self, *args, **kws):
804         super(FioJobTest_t0027, self).setup(*args, **kws)
805         self.pattern_file = os.path.join(self.test_dir, "t0027.pattern")
806         self.output_file = os.path.join(self.test_dir, "t0027file")
807         self.pattern = os.urandom(16 << 10)
808         with open(self.pattern_file, "wb") as f:
809             f.write(self.pattern)
810
811     def check_result(self):
812         super(FioJobTest_t0027, self).check_result()
813
814         if not self.passed:
815             return
816
817         with open(self.output_file, "rb") as f:
818             data = f.read()
819
820         if data != self.pattern:
821             self.passed = False
822
823 class FioJobTest_iops_rate(FioJobTest):
824     """Test consists of fio test job t0009
825     Confirm that job0 iops == 1000
826     and that job1_iops / job0_iops ~ 8
827     With two runs of fio-3.16 I observed a ratio of 8.3"""
828
829     def check_result(self):
830         super(FioJobTest_iops_rate, self).check_result()
831
832         if not self.passed:
833             return
834
835         iops1 = self.json_data['jobs'][0]['read']['iops']
836         logging.debug("Test %d: iops1: %f", self.testnum, iops1)
837         iops2 = self.json_data['jobs'][1]['read']['iops']
838         logging.debug("Test %d: iops2: %f", self.testnum, iops2)
839         ratio = iops2 / iops1
840         logging.debug("Test %d: ratio: %f", self.testnum, ratio)
841
842         if iops1 < 950 or iops1 > 1050:
843             self.failure_reason = "{0} iops value mismatch,".format(self.failure_reason)
844             self.passed = False
845
846         if ratio < 6 or ratio > 10:
847             self.failure_reason = "{0} iops ratio mismatch,".format(self.failure_reason)
848             self.passed = False
849
850
851 class Requirements():
852     """Requirements consists of multiple run environment characteristics.
853     These are to determine if a particular test can be run"""
854
855     _linux = False
856     _libaio = False
857     _io_uring = False
858     _zbd = False
859     _root = False
860     _zoned_nullb = False
861     _not_macos = False
862     _not_windows = False
863     _unittests = False
864     _cpucount4 = False
865
866     def __init__(self, fio_root):
867         Requirements._not_macos = platform.system() != "Darwin"
868         Requirements._not_windows = platform.system() != "Windows"
869         Requirements._linux = platform.system() == "Linux"
870
871         if Requirements._linux:
872             config_file = os.path.join(fio_root, "config-host.h")
873             contents, success = FioJobTest.get_file(config_file)
874             if not success:
875                 print("Unable to open {0} to check requirements".format(config_file))
876                 Requirements._zbd = True
877             else:
878                 Requirements._zbd = "CONFIG_HAS_BLKZONED" in contents
879                 Requirements._libaio = "CONFIG_LIBAIO" in contents
880
881             contents, success = FioJobTest.get_file("/proc/kallsyms")
882             if not success:
883                 print("Unable to open '/proc/kallsyms' to probe for io_uring support")
884             else:
885                 Requirements._io_uring = "io_uring_setup" in contents
886
887             Requirements._root = (os.geteuid() == 0)
888             if Requirements._zbd and Requirements._root:
889                 try:
890                     subprocess.run(["modprobe", "null_blk"],
891                                    stdout=subprocess.PIPE,
892                                    stderr=subprocess.PIPE)
893                     if os.path.exists("/sys/module/null_blk/parameters/zoned"):
894                         Requirements._zoned_nullb = True
895                 except Exception:
896                     pass
897
898         if platform.system() == "Windows":
899             utest_exe = "unittest.exe"
900         else:
901             utest_exe = "unittest"
902         unittest_path = os.path.join(fio_root, "unittests", utest_exe)
903         Requirements._unittests = os.path.exists(unittest_path)
904
905         Requirements._cpucount4 = multiprocessing.cpu_count() >= 4
906
907         req_list = [Requirements.linux,
908                     Requirements.libaio,
909                     Requirements.io_uring,
910                     Requirements.zbd,
911                     Requirements.root,
912                     Requirements.zoned_nullb,
913                     Requirements.not_macos,
914                     Requirements.not_windows,
915                     Requirements.unittests,
916                     Requirements.cpucount4]
917         for req in req_list:
918             value, desc = req()
919             logging.debug("Requirements: Requirement '%s' met? %s", desc, value)
920
921     @classmethod
922     def linux(cls):
923         """Are we running on Linux?"""
924         return Requirements._linux, "Linux required"
925
926     @classmethod
927     def libaio(cls):
928         """Is libaio available?"""
929         return Requirements._libaio, "libaio required"
930
931     @classmethod
932     def io_uring(cls):
933         """Is io_uring available?"""
934         return Requirements._io_uring, "io_uring required"
935
936     @classmethod
937     def zbd(cls):
938         """Is ZBD support available?"""
939         return Requirements._zbd, "Zoned block device support required"
940
941     @classmethod
942     def root(cls):
943         """Are we running as root?"""
944         return Requirements._root, "root required"
945
946     @classmethod
947     def zoned_nullb(cls):
948         """Are zoned null block devices available?"""
949         return Requirements._zoned_nullb, "Zoned null block device support required"
950
951     @classmethod
952     def not_macos(cls):
953         """Are we running on a platform other than macOS?"""
954         return Requirements._not_macos, "platform other than macOS required"
955
956     @classmethod
957     def not_windows(cls):
958         """Are we running on a platform other than Windws?"""
959         return Requirements._not_windows, "platform other than Windows required"
960
961     @classmethod
962     def unittests(cls):
963         """Were unittests built?"""
964         return Requirements._unittests, "Unittests support required"
965
966     @classmethod
967     def cpucount4(cls):
968         """Do we have at least 4 CPUs?"""
969         return Requirements._cpucount4, "4+ CPUs required"
970
971
972 SUCCESS_DEFAULT = {
973     'zero_return': True,
974     'stderr_empty': True,
975     'timeout': 600,
976     }
977 SUCCESS_NONZERO = {
978     'zero_return': False,
979     'stderr_empty': False,
980     'timeout': 600,
981     }
982 SUCCESS_STDERR = {
983     'zero_return': True,
984     'stderr_empty': False,
985     'timeout': 600,
986     }
987 TEST_LIST = [
988     {
989         'test_id':          1,
990         'test_class':       FioJobTest,
991         'job':              't0001-52c58027.fio',
992         'success':          SUCCESS_DEFAULT,
993         'pre_job':          None,
994         'pre_success':      None,
995         'requirements':     [],
996     },
997     {
998         'test_id':          2,
999         'test_class':       FioJobTest,
1000         'job':              't0002-13af05ae-post.fio',
1001         'success':          SUCCESS_DEFAULT,
1002         'pre_job':          't0002-13af05ae-pre.fio',
1003         'pre_success':      None,
1004         'requirements':     [Requirements.linux, Requirements.libaio],
1005     },
1006     {
1007         'test_id':          3,
1008         'test_class':       FioJobTest,
1009         'job':              't0003-0ae2c6e1-post.fio',
1010         'success':          SUCCESS_NONZERO,
1011         'pre_job':          't0003-0ae2c6e1-pre.fio',
1012         'pre_success':      SUCCESS_DEFAULT,
1013         'requirements':     [Requirements.linux, Requirements.libaio],
1014     },
1015     {
1016         'test_id':          4,
1017         'test_class':       FioJobTest,
1018         'job':              't0004-8a99fdf6.fio',
1019         'success':          SUCCESS_DEFAULT,
1020         'pre_job':          None,
1021         'pre_success':      None,
1022         'requirements':     [Requirements.linux, Requirements.libaio],
1023     },
1024     {
1025         'test_id':          5,
1026         'test_class':       FioJobTest_t0005,
1027         'job':              't0005-f7078f7b.fio',
1028         'success':          SUCCESS_DEFAULT,
1029         'pre_job':          None,
1030         'pre_success':      None,
1031         'output_format':    'json',
1032         'requirements':     [Requirements.not_windows],
1033     },
1034     {
1035         'test_id':          6,
1036         'test_class':       FioJobTest_t0006,
1037         'job':              't0006-82af2a7c.fio',
1038         'success':          SUCCESS_DEFAULT,
1039         'pre_job':          None,
1040         'pre_success':      None,
1041         'output_format':    'json',
1042         'requirements':     [Requirements.linux, Requirements.libaio],
1043     },
1044     {
1045         'test_id':          7,
1046         'test_class':       FioJobTest_t0007,
1047         'job':              't0007-37cf9e3c.fio',
1048         'success':          SUCCESS_DEFAULT,
1049         'pre_job':          None,
1050         'pre_success':      None,
1051         'output_format':    'json',
1052         'requirements':     [],
1053     },
1054     {
1055         'test_id':          8,
1056         'test_class':       FioJobTest_t0008,
1057         'job':              't0008-ae2fafc8.fio',
1058         'success':          SUCCESS_DEFAULT,
1059         'pre_job':          None,
1060         'pre_success':      None,
1061         'output_format':    'json',
1062         'requirements':     [],
1063     },
1064     {
1065         'test_id':          9,
1066         'test_class':       FioJobTest_t0009,
1067         'job':              't0009-f8b0bd10.fio',
1068         'success':          SUCCESS_DEFAULT,
1069         'pre_job':          None,
1070         'pre_success':      None,
1071         'output_format':    'json',
1072         'requirements':     [Requirements.not_macos,
1073                              Requirements.cpucount4],
1074         # mac os does not support CPU affinity
1075     },
1076     {
1077         'test_id':          10,
1078         'test_class':       FioJobTest,
1079         'job':              't0010-b7aae4ba.fio',
1080         'success':          SUCCESS_DEFAULT,
1081         'pre_job':          None,
1082         'pre_success':      None,
1083         'requirements':     [],
1084     },
1085     {
1086         'test_id':          11,
1087         'test_class':       FioJobTest_iops_rate,
1088         'job':              't0011-5d2788d5.fio',
1089         'success':          SUCCESS_DEFAULT,
1090         'pre_job':          None,
1091         'pre_success':      None,
1092         'output_format':    'json',
1093         'requirements':     [],
1094     },
1095     {
1096         'test_id':          12,
1097         'test_class':       FioJobTest_t0012,
1098         'job':              't0012.fio',
1099         'success':          SUCCESS_DEFAULT,
1100         'pre_job':          None,
1101         'pre_success':      None,
1102         'output_format':    'json',
1103         'requirements':     [],
1104     },
1105     {
1106         'test_id':          13,
1107         'test_class':       FioJobTest,
1108         'job':              't0013.fio',
1109         'success':          SUCCESS_DEFAULT,
1110         'pre_job':          None,
1111         'pre_success':      None,
1112         'output_format':    'json',
1113         'requirements':     [],
1114     },
1115     {
1116         'test_id':          14,
1117         'test_class':       FioJobTest_t0014,
1118         'job':              't0014.fio',
1119         'success':          SUCCESS_DEFAULT,
1120         'pre_job':          None,
1121         'pre_success':      None,
1122         'output_format':    'json',
1123         'requirements':     [],
1124     },
1125     {
1126         'test_id':          15,
1127         'test_class':       FioJobTest_t0015,
1128         'job':              't0015-e78980ff.fio',
1129         'success':          SUCCESS_DEFAULT,
1130         'pre_job':          None,
1131         'pre_success':      None,
1132         'output_format':    'json',
1133         'requirements':     [Requirements.linux, Requirements.libaio],
1134     },
1135     {
1136         'test_id':          16,
1137         'test_class':       FioJobTest_t0015,
1138         'job':              't0016-d54ae22.fio',
1139         'success':          SUCCESS_DEFAULT,
1140         'pre_job':          None,
1141         'pre_success':      None,
1142         'output_format':    'json',
1143         'requirements':     [],
1144     },
1145     {
1146         'test_id':          17,
1147         'test_class':       FioJobTest_t0015,
1148         'job':              't0017.fio',
1149         'success':          SUCCESS_DEFAULT,
1150         'pre_job':          None,
1151         'pre_success':      None,
1152         'output_format':    'json',
1153         'requirements':     [Requirements.not_windows],
1154     },
1155     {
1156         'test_id':          18,
1157         'test_class':       FioJobTest,
1158         'job':              't0018.fio',
1159         'success':          SUCCESS_DEFAULT,
1160         'pre_job':          None,
1161         'pre_success':      None,
1162         'requirements':     [Requirements.linux, Requirements.io_uring],
1163     },
1164     {
1165         'test_id':          19,
1166         'test_class':       FioJobTest_t0019,
1167         'job':              't0019.fio',
1168         'success':          SUCCESS_DEFAULT,
1169         'pre_job':          None,
1170         'pre_success':      None,
1171         'requirements':     [],
1172     },
1173     {
1174         'test_id':          20,
1175         'test_class':       FioJobTest_t0020,
1176         'job':              't0020.fio',
1177         'success':          SUCCESS_DEFAULT,
1178         'pre_job':          None,
1179         'pre_success':      None,
1180         'requirements':     [],
1181     },
1182     {
1183         'test_id':          21,
1184         'test_class':       FioJobTest_t0020,
1185         'job':              't0021.fio',
1186         'success':          SUCCESS_DEFAULT,
1187         'pre_job':          None,
1188         'pre_success':      None,
1189         'requirements':     [],
1190     },
1191     {
1192         'test_id':          22,
1193         'test_class':       FioJobTest_t0022,
1194         'job':              't0022.fio',
1195         'success':          SUCCESS_DEFAULT,
1196         'pre_job':          None,
1197         'pre_success':      None,
1198         'requirements':     [],
1199     },
1200     {
1201         'test_id':          23,
1202         'test_class':       FioJobTest_t0023,
1203         'job':              't0023.fio',
1204         'success':          SUCCESS_DEFAULT,
1205         'pre_job':          None,
1206         'pre_success':      None,
1207         'requirements':     [],
1208     },
1209     {
1210         'test_id':          24,
1211         'test_class':       FioJobTest_t0024,
1212         'job':              't0024.fio',
1213         'success':          SUCCESS_DEFAULT,
1214         'pre_job':          None,
1215         'pre_success':      None,
1216         'requirements':     [],
1217     },
1218     {
1219         'test_id':          25,
1220         'test_class':       FioJobTest_t0025,
1221         'job':              't0025.fio',
1222         'success':          SUCCESS_DEFAULT,
1223         'pre_job':          None,
1224         'pre_success':      None,
1225         'output_format':    'json',
1226         'requirements':     [],
1227     },
1228     {
1229         'test_id':          26,
1230         'test_class':       FioJobTest,
1231         'job':              't0026.fio',
1232         'success':          SUCCESS_DEFAULT,
1233         'pre_job':          None,
1234         'pre_success':      None,
1235         'requirements':     [Requirements.not_windows],
1236     },
1237     {
1238         'test_id':          27,
1239         'test_class':       FioJobTest_t0027,
1240         'job':              't0027.fio',
1241         'success':          SUCCESS_DEFAULT,
1242         'pre_job':          None,
1243         'pre_success':      None,
1244         'requirements':     [],
1245     },
1246     {
1247         'test_id':          1000,
1248         'test_class':       FioExeTest,
1249         'exe':              't/axmap',
1250         'parameters':       None,
1251         'success':          SUCCESS_DEFAULT,
1252         'requirements':     [],
1253     },
1254     {
1255         'test_id':          1001,
1256         'test_class':       FioExeTest,
1257         'exe':              't/ieee754',
1258         'parameters':       None,
1259         'success':          SUCCESS_DEFAULT,
1260         'requirements':     [],
1261     },
1262     {
1263         'test_id':          1002,
1264         'test_class':       FioExeTest,
1265         'exe':              't/lfsr-test',
1266         'parameters':       ['0xFFFFFF', '0', '0', 'verify'],
1267         'success':          SUCCESS_STDERR,
1268         'requirements':     [],
1269     },
1270     {
1271         'test_id':          1003,
1272         'test_class':       FioExeTest,
1273         'exe':              't/readonly.py',
1274         'parameters':       ['-f', '{fio_path}'],
1275         'success':          SUCCESS_DEFAULT,
1276         'requirements':     [],
1277     },
1278     {
1279         'test_id':          1004,
1280         'test_class':       FioExeTest,
1281         'exe':              't/steadystate_tests.py',
1282         'parameters':       ['{fio_path}'],
1283         'success':          SUCCESS_DEFAULT,
1284         'requirements':     [],
1285     },
1286     {
1287         'test_id':          1005,
1288         'test_class':       FioExeTest,
1289         'exe':              't/stest',
1290         'parameters':       None,
1291         'success':          SUCCESS_STDERR,
1292         'requirements':     [],
1293     },
1294     {
1295         'test_id':          1006,
1296         'test_class':       FioExeTest,
1297         'exe':              't/strided.py',
1298         'parameters':       ['{fio_path}'],
1299         'success':          SUCCESS_DEFAULT,
1300         'requirements':     [],
1301     },
1302     {
1303         'test_id':          1007,
1304         'test_class':       FioExeTest,
1305         'exe':              't/zbd/run-tests-against-nullb',
1306         'parameters':       ['-s', '1'],
1307         'success':          SUCCESS_DEFAULT,
1308         'requirements':     [Requirements.linux, Requirements.zbd,
1309                              Requirements.root],
1310     },
1311     {
1312         'test_id':          1008,
1313         'test_class':       FioExeTest,
1314         'exe':              't/zbd/run-tests-against-nullb',
1315         'parameters':       ['-s', '2'],
1316         'success':          SUCCESS_DEFAULT,
1317         'requirements':     [Requirements.linux, Requirements.zbd,
1318                              Requirements.root, Requirements.zoned_nullb],
1319     },
1320     {
1321         'test_id':          1009,
1322         'test_class':       FioExeTest,
1323         'exe':              'unittests/unittest',
1324         'parameters':       None,
1325         'success':          SUCCESS_DEFAULT,
1326         'requirements':     [Requirements.unittests],
1327     },
1328     {
1329         'test_id':          1010,
1330         'test_class':       FioExeTest,
1331         'exe':              't/latency_percentiles.py',
1332         'parameters':       ['-f', '{fio_path}'],
1333         'success':          SUCCESS_DEFAULT,
1334         'requirements':     [],
1335     },
1336     {
1337         'test_id':          1011,
1338         'test_class':       FioExeTest,
1339         'exe':              't/jsonplus2csv_test.py',
1340         'parameters':       ['-f', '{fio_path}'],
1341         'success':          SUCCESS_DEFAULT,
1342         'requirements':     [],
1343     },
1344     {
1345         'test_id':          1012,
1346         'test_class':       FioExeTest,
1347         'exe':              't/log_compression.py',
1348         'parameters':       ['-f', '{fio_path}'],
1349         'success':          SUCCESS_DEFAULT,
1350         'requirements':     [],
1351     },
1352 ]
1353
1354
1355 def parse_args():
1356     """Parse command-line arguments."""
1357
1358     parser = argparse.ArgumentParser()
1359     parser.add_argument('-r', '--fio-root',
1360                         help='fio root path')
1361     parser.add_argument('-f', '--fio',
1362                         help='path to fio executable (e.g., ./fio)')
1363     parser.add_argument('-a', '--artifact-root',
1364                         help='artifact root directory')
1365     parser.add_argument('-s', '--skip', nargs='+', type=int,
1366                         help='list of test(s) to skip')
1367     parser.add_argument('-o', '--run-only', nargs='+', type=int,
1368                         help='list of test(s) to run, skipping all others')
1369     parser.add_argument('-d', '--debug', action='store_true',
1370                         help='provide debug output')
1371     parser.add_argument('-k', '--skip-req', action='store_true',
1372                         help='skip requirements checking')
1373     parser.add_argument('-p', '--pass-through', action='append',
1374                         help='pass-through an argument to an executable test')
1375     args = parser.parse_args()
1376
1377     return args
1378
1379
1380 def main():
1381     """Entry point."""
1382
1383     args = parse_args()
1384     if args.debug:
1385         logging.basicConfig(level=logging.DEBUG)
1386     else:
1387         logging.basicConfig(level=logging.INFO)
1388
1389     pass_through = {}
1390     if args.pass_through:
1391         for arg in args.pass_through:
1392             if not ':' in arg:
1393                 print("Invalid --pass-through argument '%s'" % arg)
1394                 print("Syntax for --pass-through is TESTNUMBER:ARGUMENT")
1395                 return
1396             split = arg.split(":", 1)
1397             pass_through[int(split[0])] = split[1]
1398         logging.debug("Pass-through arguments: %s", pass_through)
1399
1400     if args.fio_root:
1401         fio_root = args.fio_root
1402     else:
1403         fio_root = str(Path(__file__).absolute().parent.parent)
1404     print("fio root is %s" % fio_root)
1405
1406     if args.fio:
1407         fio_path = args.fio
1408     else:
1409         if platform.system() == "Windows":
1410             fio_exe = "fio.exe"
1411         else:
1412             fio_exe = "fio"
1413         fio_path = os.path.join(fio_root, fio_exe)
1414     print("fio path is %s" % fio_path)
1415     if not shutil.which(fio_path):
1416         print("Warning: fio executable not found")
1417
1418     artifact_root = args.artifact_root if args.artifact_root else \
1419         "fio-test-{0}".format(time.strftime("%Y%m%d-%H%M%S"))
1420     os.mkdir(artifact_root)
1421     print("Artifact directory is %s" % artifact_root)
1422
1423     if not args.skip_req:
1424         req = Requirements(fio_root)
1425
1426     passed = 0
1427     failed = 0
1428     skipped = 0
1429
1430     for config in TEST_LIST:
1431         if (args.skip and config['test_id'] in args.skip) or \
1432            (args.run_only and config['test_id'] not in args.run_only):
1433             skipped = skipped + 1
1434             print("Test {0} SKIPPED (User request)".format(config['test_id']))
1435             continue
1436
1437         if issubclass(config['test_class'], FioJobTest):
1438             if config['pre_job']:
1439                 fio_pre_job = os.path.join(fio_root, 't', 'jobs',
1440                                            config['pre_job'])
1441             else:
1442                 fio_pre_job = None
1443             if config['pre_success']:
1444                 fio_pre_success = config['pre_success']
1445             else:
1446                 fio_pre_success = None
1447             if 'output_format' in config:
1448                 output_format = config['output_format']
1449             else:
1450                 output_format = 'normal'
1451             test = config['test_class'](
1452                 fio_path,
1453                 os.path.join(fio_root, 't', 'jobs', config['job']),
1454                 config['success'],
1455                 fio_pre_job=fio_pre_job,
1456                 fio_pre_success=fio_pre_success,
1457                 output_format=output_format)
1458             desc = config['job']
1459         elif issubclass(config['test_class'], FioExeTest):
1460             exe_path = os.path.join(fio_root, config['exe'])
1461             if config['parameters']:
1462                 parameters = [p.format(fio_path=fio_path) for p in config['parameters']]
1463             else:
1464                 parameters = []
1465             if Path(exe_path).suffix == '.py' and platform.system() == "Windows":
1466                 parameters.insert(0, exe_path)
1467                 exe_path = "python.exe"
1468             if config['test_id'] in pass_through:
1469                 parameters += pass_through[config['test_id']].split()
1470             test = config['test_class'](exe_path, parameters,
1471                                         config['success'])
1472             desc = config['exe']
1473         else:
1474             print("Test {0} FAILED: unable to process test config".format(config['test_id']))
1475             failed = failed + 1
1476             continue
1477
1478         if not args.skip_req:
1479             reqs_met = True
1480             for req in config['requirements']:
1481                 reqs_met, reason = req()
1482                 logging.debug("Test %d: Requirement '%s' met? %s", config['test_id'], reason,
1483                               reqs_met)
1484                 if not reqs_met:
1485                     break
1486             if not reqs_met:
1487                 print("Test {0} SKIPPED ({1}) {2}".format(config['test_id'], reason, desc))
1488                 skipped = skipped + 1
1489                 continue
1490
1491         try:
1492             test.setup(artifact_root, config['test_id'])
1493             test.run()
1494             test.check_result()
1495         except KeyboardInterrupt:
1496             break
1497         except Exception as e:
1498             test.passed = False
1499             test.failure_reason += str(e)
1500             logging.debug("Test %d exception:\n%s\n", config['test_id'], traceback.format_exc())
1501         if test.passed:
1502             result = "PASSED"
1503             passed = passed + 1
1504         else:
1505             result = "FAILED: {0}".format(test.failure_reason)
1506             failed = failed + 1
1507             contents, _ = FioJobTest.get_file(test.stderr_file)
1508             logging.debug("Test %d: stderr:\n%s", config['test_id'], contents)
1509             contents, _ = FioJobTest.get_file(test.stdout_file)
1510             logging.debug("Test %d: stdout:\n%s", config['test_id'], contents)
1511         print("Test {0} {1} {2}".format(config['test_id'], result, desc))
1512
1513     print("{0} test(s) passed, {1} failed, {2} skipped".format(passed, failed, skipped))
1514
1515     sys.exit(failed)
1516
1517
1518 if __name__ == '__main__':
1519     main()