t/run-fio-tests: relax acceptance criteria for t0008
[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'] ~ 16384
391
392     This is a 50/50 seq read/write workload. Since fio flips a coin to
393     determine whether to issue a read or a write, total bytes written will not
394     be exactly 16384K. But total bytes read will be exactly 32768K because
395     reads will include the initial phase as well as the verify phase where all
396     the blocks originally written will be read."""
397
398     def check_result(self):
399         super(FioJobTest_t0008, self).check_result()
400
401         if not self.passed:
402             return
403
404         ratio = self.json_data['jobs'][0]['write']['io_kbytes'] / 16384
405         logging.debug("Test %d: ratio: %f", self.testnum, ratio)
406
407         if ratio < 0.97 or ratio > 1.03:
408             self.failure_reason = "{0} bytes written mismatch,".format(self.failure_reason)
409             self.passed = False
410         if self.json_data['jobs'][0]['read']['io_kbytes'] != 32768:
411             self.failure_reason = "{0} bytes read mismatch,".format(self.failure_reason)
412             self.passed = False
413
414
415 class FioJobTest_t0009(FioJobTest):
416     """Test consists of fio test job t0009
417     Confirm that runtime >= 60s"""
418
419     def check_result(self):
420         super(FioJobTest_t0009, self).check_result()
421
422         if not self.passed:
423             return
424
425         logging.debug('Test %d: elapsed: %d', self.testnum, self.json_data['jobs'][0]['elapsed'])
426
427         if self.json_data['jobs'][0]['elapsed'] < 60:
428             self.failure_reason = "{0} elapsed time mismatch,".format(self.failure_reason)
429             self.passed = False
430
431
432 class FioJobTest_t0012(FioJobTest):
433     """Test consists of fio test job t0012
434     Confirm ratios of job iops are 1:5:10
435     job1,job2,job3 respectively"""
436
437     def check_result(self):
438         super(FioJobTest_t0012, self).check_result()
439
440         if not self.passed:
441             return
442
443         iops_files = []
444         for i in range(1, 4):
445             filename = os.path.join(self.test_dir, "{0}_iops.{1}.log".format(os.path.basename(
446                 self.fio_job), i))
447             file_data = self.get_file_fail(filename)
448             if not file_data:
449                 return
450
451             iops_files.append(file_data.splitlines())
452
453         # there are 9 samples for job1 and job2, 4 samples for job3
454         iops1 = 0.0
455         iops2 = 0.0
456         iops3 = 0.0
457         for i in range(9):
458             iops1 = iops1 + float(iops_files[0][i].split(',')[1])
459             iops2 = iops2 + float(iops_files[1][i].split(',')[1])
460             iops3 = iops3 + float(iops_files[2][i].split(',')[1])
461
462             ratio1 = iops3/iops2
463             ratio2 = iops3/iops1
464             logging.debug("sample {0}: job1 iops={1} job2 iops={2} job3 iops={3} " \
465                 "job3/job2={4:.3f} job3/job1={5:.3f}".format(i, iops1, iops2, iops3, ratio1,
466                                                              ratio2))
467
468         # test job1 and job2 succeeded to recalibrate
469         if ratio1 < 1 or ratio1 > 3 or ratio2 < 7 or ratio2 > 13:
470             self.failure_reason += " iops ratio mismatch iops1={0} iops2={1} iops3={2} " \
471                 "expected r1~2 r2~10 got r1={3:.3f} r2={4:.3f},".format(iops1, iops2, iops3,
472                                                                         ratio1, ratio2)
473             self.passed = False
474             return
475
476
477 class FioJobTest_t0014(FioJobTest):
478     """Test consists of fio test job t0014
479         Confirm that job1_iops / job2_iops ~ 1:2 for entire duration
480         and that job1_iops / job3_iops ~ 1:3 for first half of duration.
481
482     The test is about making sure the flow feature can
483     re-calibrate the activity dynamically"""
484
485     def check_result(self):
486         super(FioJobTest_t0014, self).check_result()
487
488         if not self.passed:
489             return
490
491         iops_files = []
492         for i in range(1, 4):
493             filename = os.path.join(self.test_dir, "{0}_iops.{1}.log".format(os.path.basename(
494                 self.fio_job), i))
495             file_data = self.get_file_fail(filename)
496             if not file_data:
497                 return
498
499             iops_files.append(file_data.splitlines())
500
501         # there are 9 samples for job1 and job2, 4 samples for job3
502         iops1 = 0.0
503         iops2 = 0.0
504         iops3 = 0.0
505         for i in range(9):
506             if i < 4:
507                 iops3 = iops3 + float(iops_files[2][i].split(',')[1])
508             elif i == 4:
509                 ratio1 = iops1 / iops2
510                 ratio2 = iops1 / iops3
511
512
513                 if ratio1 < 0.43 or ratio1 > 0.57 or ratio2 < 0.21 or ratio2 > 0.45:
514                     self.failure_reason += " iops ratio mismatch iops1={0} iops2={1} iops3={2} " \
515                                            "expected r1~0.5 r2~0.33 got r1={3:.3f} r2={4:.3f},".format(
516                                                iops1, iops2, iops3, ratio1, ratio2)
517                     self.passed = False
518
519             iops1 = iops1 + float(iops_files[0][i].split(',')[1])
520             iops2 = iops2 + float(iops_files[1][i].split(',')[1])
521
522             ratio1 = iops1/iops2
523             ratio2 = iops1/iops3
524             logging.debug("sample {0}: job1 iops={1} job2 iops={2} job3 iops={3} " \
525                           "job1/job2={4:.3f} job1/job3={5:.3f}".format(i, iops1, iops2, iops3,
526                                                                        ratio1, ratio2))
527
528         # test job1 and job2 succeeded to recalibrate
529         if ratio1 < 0.43 or ratio1 > 0.57:
530             self.failure_reason += " iops ratio mismatch iops1={0} iops2={1} expected ratio~0.5 " \
531                                    "got ratio={2:.3f},".format(iops1, iops2, ratio1)
532             self.passed = False
533             return
534
535
536 class FioJobTest_t0015(FioJobTest):
537     """Test consists of fio test jobs t0015 and t0016
538     Confirm that mean(slat) + mean(clat) = mean(tlat)"""
539
540     def check_result(self):
541         super(FioJobTest_t0015, self).check_result()
542
543         if not self.passed:
544             return
545
546         slat = self.json_data['jobs'][0]['read']['slat_ns']['mean']
547         clat = self.json_data['jobs'][0]['read']['clat_ns']['mean']
548         tlat = self.json_data['jobs'][0]['read']['lat_ns']['mean']
549         logging.debug('Test %d: slat %f, clat %f, tlat %f', self.testnum, slat, clat, tlat)
550
551         if abs(slat + clat - tlat) > 1:
552             self.failure_reason = "{0} slat {1} + clat {2} = {3} != tlat {4},".format(
553                 self.failure_reason, slat, clat, slat+clat, tlat)
554             self.passed = False
555
556
557 class FioJobTest_t0019(FioJobTest):
558     """Test consists of fio test job t0019
559     Confirm that all offsets were touched sequentially"""
560
561     def check_result(self):
562         super(FioJobTest_t0019, self).check_result()
563
564         bw_log_filename = os.path.join(self.test_dir, "test_bw.log")
565         file_data = self.get_file_fail(bw_log_filename)
566         if not file_data:
567             return
568
569         log_lines = file_data.split('\n')
570
571         prev = -4096
572         for line in log_lines:
573             if len(line.strip()) == 0:
574                 continue
575             cur = int(line.split(',')[4])
576             if cur - prev != 4096:
577                 self.passed = False
578                 self.failure_reason = "offsets {0}, {1} not sequential".format(prev, cur)
579                 return
580             prev = cur
581
582         if cur/4096 != 255:
583             self.passed = False
584             self.failure_reason = "unexpected last offset {0}".format(cur)
585
586
587 class FioJobTest_t0020(FioJobTest):
588     """Test consists of fio test jobs t0020 and t0021
589     Confirm that almost all offsets were touched non-sequentially"""
590
591     def check_result(self):
592         super(FioJobTest_t0020, self).check_result()
593
594         bw_log_filename = os.path.join(self.test_dir, "test_bw.log")
595         file_data = self.get_file_fail(bw_log_filename)
596         if not file_data:
597             return
598
599         log_lines = file_data.split('\n')
600
601         seq_count = 0
602         offsets = set()
603
604         prev = int(log_lines[0].split(',')[4])
605         for line in log_lines[1:]:
606             offsets.add(prev/4096)
607             if len(line.strip()) == 0:
608                 continue
609             cur = int(line.split(',')[4])
610             if cur - prev == 4096:
611                 seq_count += 1
612             prev = cur
613
614         # 10 is an arbitrary threshold
615         if seq_count > 10:
616             self.passed = False
617             self.failure_reason = "too many ({0}) consecutive offsets".format(seq_count)
618
619         if len(offsets) != 256:
620             self.passed = False
621             self.failure_reason += " number of offsets is {0} instead of 256".format(len(offsets))
622
623         for i in range(256):
624             if not i in offsets:
625                 self.passed = False
626                 self.failure_reason += " missing offset {0}".format(i*4096)
627
628
629 class FioJobTest_t0022(FioJobTest):
630     """Test consists of fio test job t0022"""
631
632     def check_result(self):
633         super(FioJobTest_t0022, self).check_result()
634
635         bw_log_filename = os.path.join(self.test_dir, "test_bw.log")
636         file_data = self.get_file_fail(bw_log_filename)
637         if not file_data:
638             return
639
640         log_lines = file_data.split('\n')
641
642         filesize = 1024*1024
643         bs = 4096
644         seq_count = 0
645         offsets = set()
646
647         prev = int(log_lines[0].split(',')[4])
648         for line in log_lines[1:]:
649             offsets.add(prev/bs)
650             if len(line.strip()) == 0:
651                 continue
652             cur = int(line.split(',')[4])
653             if cur - prev == bs:
654                 seq_count += 1
655             prev = cur
656
657         # 10 is an arbitrary threshold
658         if seq_count > 10:
659             self.passed = False
660             self.failure_reason = "too many ({0}) consecutive offsets".format(seq_count)
661
662         if len(offsets) == filesize/bs:
663             self.passed = False
664             self.failure_reason += " no duplicate offsets found with norandommap=1"
665
666
667 class FioJobTest_t0023(FioJobTest):
668     """Test consists of fio test job t0023 randtrimwrite test."""
669
670     def check_trimwrite(self, filename):
671         """Make sure that trims are followed by writes of the same size at the same offset."""
672
673         bw_log_filename = os.path.join(self.test_dir, filename)
674         file_data = self.get_file_fail(bw_log_filename)
675         if not file_data:
676             return
677
678         log_lines = file_data.split('\n')
679
680         prev_ddir = 1
681         for line in log_lines:
682             if len(line.strip()) == 0:
683                 continue
684             vals = line.split(',')
685             ddir = int(vals[2])
686             bs = int(vals[3])
687             offset = int(vals[4])
688             if prev_ddir == 1:
689                 if ddir != 2:
690                     self.passed = False
691                     self.failure_reason += " {0}: write not preceeded by trim: {1}".format(
692                         bw_log_filename, line)
693                     break
694             else:
695                 if ddir != 1:
696                     self.passed = False
697                     self.failure_reason += " {0}: trim not preceeded by write: {1}".format(
698                         bw_log_filename, line)
699                     break
700                 else:
701                     if prev_bs != bs:
702                         self.passed = False
703                         self.failure_reason += " {0}: block size does not match: {1}".format(
704                             bw_log_filename, line)
705                         break
706                     if prev_offset != offset:
707                         self.passed = False
708                         self.failure_reason += " {0}: offset does not match: {1}".format(
709                             bw_log_filename, line)
710                         break
711             prev_ddir = ddir
712             prev_bs = bs
713             prev_offset = offset
714
715
716     def check_all_offsets(self, filename, sectorsize, filesize):
717         """Make sure all offsets were touched."""
718
719         file_data = self.get_file_fail(os.path.join(self.test_dir, filename))
720         if not file_data:
721             return
722
723         log_lines = file_data.split('\n')
724
725         offsets = set()
726
727         for line in log_lines:
728             if len(line.strip()) == 0:
729                 continue
730             vals = line.split(',')
731             bs = int(vals[3])
732             offset = int(vals[4])
733             if offset % sectorsize != 0:
734                 self.passed = False
735                 self.failure_reason += " {0}: offset {1} not a multiple of sector size {2}".format(
736                     filename, offset, sectorsize)
737                 break
738             if bs % sectorsize != 0:
739                 self.passed = False
740                 self.failure_reason += " {0}: block size {1} not a multiple of sector size " \
741                     "{2}".format(filename, bs, sectorsize)
742                 break
743             for i in range(int(bs/sectorsize)):
744                 offsets.add(offset/sectorsize + i)
745
746         if len(offsets) != filesize/sectorsize:
747             self.passed = False
748             self.failure_reason += " {0}: only {1} offsets touched; expected {2}".format(
749                 filename, len(offsets), filesize/sectorsize)
750         else:
751             logging.debug("%s: %d sectors touched", filename, len(offsets))
752
753
754     def check_result(self):
755         super(FioJobTest_t0023, self).check_result()
756
757         filesize = 1024*1024
758
759         self.check_trimwrite("basic_bw.log")
760         self.check_trimwrite("bs_bw.log")
761         self.check_trimwrite("bsrange_bw.log")
762         self.check_trimwrite("bssplit_bw.log")
763         self.check_trimwrite("basic_no_rm_bw.log")
764         self.check_trimwrite("bs_no_rm_bw.log")
765         self.check_trimwrite("bsrange_no_rm_bw.log")
766         self.check_trimwrite("bssplit_no_rm_bw.log")
767
768         self.check_all_offsets("basic_bw.log", 4096, filesize)
769         self.check_all_offsets("bs_bw.log", 8192, filesize)
770         self.check_all_offsets("bsrange_bw.log", 512, filesize)
771         self.check_all_offsets("bssplit_bw.log", 512, filesize)
772
773
774 class FioJobTest_t0024(FioJobTest_t0023):
775     """Test consists of fio test job t0024 trimwrite test."""
776
777     def check_result(self):
778         # call FioJobTest_t0023's parent to skip checks done by t0023
779         super(FioJobTest_t0023, self).check_result()
780
781         filesize = 1024*1024
782
783         self.check_trimwrite("basic_bw.log")
784         self.check_trimwrite("bs_bw.log")
785         self.check_trimwrite("bsrange_bw.log")
786         self.check_trimwrite("bssplit_bw.log")
787
788         self.check_all_offsets("basic_bw.log", 4096, filesize)
789         self.check_all_offsets("bs_bw.log", 8192, filesize)
790         self.check_all_offsets("bsrange_bw.log", 512, filesize)
791         self.check_all_offsets("bssplit_bw.log", 512, filesize)
792
793
794 class FioJobTest_t0025(FioJobTest):
795     """Test experimental verify read backs written data pattern."""
796     def check_result(self):
797         super(FioJobTest_t0025, self).check_result()
798
799         if not self.passed:
800             return
801
802         if self.json_data['jobs'][0]['read']['io_kbytes'] != 128:
803             self.passed = False
804
805 class FioJobTest_t0027(FioJobTest):
806     def setup(self, *args, **kws):
807         super(FioJobTest_t0027, self).setup(*args, **kws)
808         self.pattern_file = os.path.join(self.test_dir, "t0027.pattern")
809         self.output_file = os.path.join(self.test_dir, "t0027file")
810         self.pattern = os.urandom(16 << 10)
811         with open(self.pattern_file, "wb") as f:
812             f.write(self.pattern)
813
814     def check_result(self):
815         super(FioJobTest_t0027, self).check_result()
816
817         if not self.passed:
818             return
819
820         with open(self.output_file, "rb") as f:
821             data = f.read()
822
823         if data != self.pattern:
824             self.passed = False
825
826 class FioJobTest_iops_rate(FioJobTest):
827     """Test consists of fio test job t0009
828     Confirm that job0 iops == 1000
829     and that job1_iops / job0_iops ~ 8
830     With two runs of fio-3.16 I observed a ratio of 8.3"""
831
832     def check_result(self):
833         super(FioJobTest_iops_rate, self).check_result()
834
835         if not self.passed:
836             return
837
838         iops1 = self.json_data['jobs'][0]['read']['iops']
839         logging.debug("Test %d: iops1: %f", self.testnum, iops1)
840         iops2 = self.json_data['jobs'][1]['read']['iops']
841         logging.debug("Test %d: iops2: %f", self.testnum, iops2)
842         ratio = iops2 / iops1
843         logging.debug("Test %d: ratio: %f", self.testnum, ratio)
844
845         if iops1 < 950 or iops1 > 1050:
846             self.failure_reason = "{0} iops value mismatch,".format(self.failure_reason)
847             self.passed = False
848
849         if ratio < 6 or ratio > 10:
850             self.failure_reason = "{0} iops ratio mismatch,".format(self.failure_reason)
851             self.passed = False
852
853
854 class Requirements():
855     """Requirements consists of multiple run environment characteristics.
856     These are to determine if a particular test can be run"""
857
858     _linux = False
859     _libaio = False
860     _io_uring = False
861     _zbd = False
862     _root = False
863     _zoned_nullb = False
864     _not_macos = False
865     _not_windows = False
866     _unittests = False
867     _cpucount4 = False
868
869     def __init__(self, fio_root):
870         Requirements._not_macos = platform.system() != "Darwin"
871         Requirements._not_windows = platform.system() != "Windows"
872         Requirements._linux = platform.system() == "Linux"
873
874         if Requirements._linux:
875             config_file = os.path.join(fio_root, "config-host.h")
876             contents, success = FioJobTest.get_file(config_file)
877             if not success:
878                 print("Unable to open {0} to check requirements".format(config_file))
879                 Requirements._zbd = True
880             else:
881                 Requirements._zbd = "CONFIG_HAS_BLKZONED" in contents
882                 Requirements._libaio = "CONFIG_LIBAIO" in contents
883
884             contents, success = FioJobTest.get_file("/proc/kallsyms")
885             if not success:
886                 print("Unable to open '/proc/kallsyms' to probe for io_uring support")
887             else:
888                 Requirements._io_uring = "io_uring_setup" in contents
889
890             Requirements._root = (os.geteuid() == 0)
891             if Requirements._zbd and Requirements._root:
892                 try:
893                     subprocess.run(["modprobe", "null_blk"],
894                                    stdout=subprocess.PIPE,
895                                    stderr=subprocess.PIPE)
896                     if os.path.exists("/sys/module/null_blk/parameters/zoned"):
897                         Requirements._zoned_nullb = True
898                 except Exception:
899                     pass
900
901         if platform.system() == "Windows":
902             utest_exe = "unittest.exe"
903         else:
904             utest_exe = "unittest"
905         unittest_path = os.path.join(fio_root, "unittests", utest_exe)
906         Requirements._unittests = os.path.exists(unittest_path)
907
908         Requirements._cpucount4 = multiprocessing.cpu_count() >= 4
909
910         req_list = [Requirements.linux,
911                     Requirements.libaio,
912                     Requirements.io_uring,
913                     Requirements.zbd,
914                     Requirements.root,
915                     Requirements.zoned_nullb,
916                     Requirements.not_macos,
917                     Requirements.not_windows,
918                     Requirements.unittests,
919                     Requirements.cpucount4]
920         for req in req_list:
921             value, desc = req()
922             logging.debug("Requirements: Requirement '%s' met? %s", desc, value)
923
924     @classmethod
925     def linux(cls):
926         """Are we running on Linux?"""
927         return Requirements._linux, "Linux required"
928
929     @classmethod
930     def libaio(cls):
931         """Is libaio available?"""
932         return Requirements._libaio, "libaio required"
933
934     @classmethod
935     def io_uring(cls):
936         """Is io_uring available?"""
937         return Requirements._io_uring, "io_uring required"
938
939     @classmethod
940     def zbd(cls):
941         """Is ZBD support available?"""
942         return Requirements._zbd, "Zoned block device support required"
943
944     @classmethod
945     def root(cls):
946         """Are we running as root?"""
947         return Requirements._root, "root required"
948
949     @classmethod
950     def zoned_nullb(cls):
951         """Are zoned null block devices available?"""
952         return Requirements._zoned_nullb, "Zoned null block device support required"
953
954     @classmethod
955     def not_macos(cls):
956         """Are we running on a platform other than macOS?"""
957         return Requirements._not_macos, "platform other than macOS required"
958
959     @classmethod
960     def not_windows(cls):
961         """Are we running on a platform other than Windws?"""
962         return Requirements._not_windows, "platform other than Windows required"
963
964     @classmethod
965     def unittests(cls):
966         """Were unittests built?"""
967         return Requirements._unittests, "Unittests support required"
968
969     @classmethod
970     def cpucount4(cls):
971         """Do we have at least 4 CPUs?"""
972         return Requirements._cpucount4, "4+ CPUs required"
973
974
975 SUCCESS_DEFAULT = {
976     'zero_return': True,
977     'stderr_empty': True,
978     'timeout': 600,
979     }
980 SUCCESS_NONZERO = {
981     'zero_return': False,
982     'stderr_empty': False,
983     'timeout': 600,
984     }
985 SUCCESS_STDERR = {
986     'zero_return': True,
987     'stderr_empty': False,
988     'timeout': 600,
989     }
990 TEST_LIST = [
991     {
992         'test_id':          1,
993         'test_class':       FioJobTest,
994         'job':              't0001-52c58027.fio',
995         'success':          SUCCESS_DEFAULT,
996         'pre_job':          None,
997         'pre_success':      None,
998         'requirements':     [],
999     },
1000     {
1001         'test_id':          2,
1002         'test_class':       FioJobTest,
1003         'job':              't0002-13af05ae-post.fio',
1004         'success':          SUCCESS_DEFAULT,
1005         'pre_job':          't0002-13af05ae-pre.fio',
1006         'pre_success':      None,
1007         'requirements':     [Requirements.linux, Requirements.libaio],
1008     },
1009     {
1010         'test_id':          3,
1011         'test_class':       FioJobTest,
1012         'job':              't0003-0ae2c6e1-post.fio',
1013         'success':          SUCCESS_NONZERO,
1014         'pre_job':          't0003-0ae2c6e1-pre.fio',
1015         'pre_success':      SUCCESS_DEFAULT,
1016         'requirements':     [Requirements.linux, Requirements.libaio],
1017     },
1018     {
1019         'test_id':          4,
1020         'test_class':       FioJobTest,
1021         'job':              't0004-8a99fdf6.fio',
1022         'success':          SUCCESS_DEFAULT,
1023         'pre_job':          None,
1024         'pre_success':      None,
1025         'requirements':     [Requirements.linux, Requirements.libaio],
1026     },
1027     {
1028         'test_id':          5,
1029         'test_class':       FioJobTest_t0005,
1030         'job':              't0005-f7078f7b.fio',
1031         'success':          SUCCESS_DEFAULT,
1032         'pre_job':          None,
1033         'pre_success':      None,
1034         'output_format':    'json',
1035         'requirements':     [Requirements.not_windows],
1036     },
1037     {
1038         'test_id':          6,
1039         'test_class':       FioJobTest_t0006,
1040         'job':              't0006-82af2a7c.fio',
1041         'success':          SUCCESS_DEFAULT,
1042         'pre_job':          None,
1043         'pre_success':      None,
1044         'output_format':    'json',
1045         'requirements':     [Requirements.linux, Requirements.libaio],
1046     },
1047     {
1048         'test_id':          7,
1049         'test_class':       FioJobTest_t0007,
1050         'job':              't0007-37cf9e3c.fio',
1051         'success':          SUCCESS_DEFAULT,
1052         'pre_job':          None,
1053         'pre_success':      None,
1054         'output_format':    'json',
1055         'requirements':     [],
1056     },
1057     {
1058         'test_id':          8,
1059         'test_class':       FioJobTest_t0008,
1060         'job':              't0008-ae2fafc8.fio',
1061         'success':          SUCCESS_DEFAULT,
1062         'pre_job':          None,
1063         'pre_success':      None,
1064         'output_format':    'json',
1065         'requirements':     [],
1066     },
1067     {
1068         'test_id':          9,
1069         'test_class':       FioJobTest_t0009,
1070         'job':              't0009-f8b0bd10.fio',
1071         'success':          SUCCESS_DEFAULT,
1072         'pre_job':          None,
1073         'pre_success':      None,
1074         'output_format':    'json',
1075         'requirements':     [Requirements.not_macos,
1076                              Requirements.cpucount4],
1077         # mac os does not support CPU affinity
1078     },
1079     {
1080         'test_id':          10,
1081         'test_class':       FioJobTest,
1082         'job':              't0010-b7aae4ba.fio',
1083         'success':          SUCCESS_DEFAULT,
1084         'pre_job':          None,
1085         'pre_success':      None,
1086         'requirements':     [],
1087     },
1088     {
1089         'test_id':          11,
1090         'test_class':       FioJobTest_iops_rate,
1091         'job':              't0011-5d2788d5.fio',
1092         'success':          SUCCESS_DEFAULT,
1093         'pre_job':          None,
1094         'pre_success':      None,
1095         'output_format':    'json',
1096         'requirements':     [],
1097     },
1098     {
1099         'test_id':          12,
1100         'test_class':       FioJobTest_t0012,
1101         'job':              't0012.fio',
1102         'success':          SUCCESS_DEFAULT,
1103         'pre_job':          None,
1104         'pre_success':      None,
1105         'output_format':    'json',
1106         'requirements':     [],
1107     },
1108     {
1109         'test_id':          13,
1110         'test_class':       FioJobTest,
1111         'job':              't0013.fio',
1112         'success':          SUCCESS_DEFAULT,
1113         'pre_job':          None,
1114         'pre_success':      None,
1115         'output_format':    'json',
1116         'requirements':     [],
1117     },
1118     {
1119         'test_id':          14,
1120         'test_class':       FioJobTest_t0014,
1121         'job':              't0014.fio',
1122         'success':          SUCCESS_DEFAULT,
1123         'pre_job':          None,
1124         'pre_success':      None,
1125         'output_format':    'json',
1126         'requirements':     [],
1127     },
1128     {
1129         'test_id':          15,
1130         'test_class':       FioJobTest_t0015,
1131         'job':              't0015-e78980ff.fio',
1132         'success':          SUCCESS_DEFAULT,
1133         'pre_job':          None,
1134         'pre_success':      None,
1135         'output_format':    'json',
1136         'requirements':     [Requirements.linux, Requirements.libaio],
1137     },
1138     {
1139         'test_id':          16,
1140         'test_class':       FioJobTest_t0015,
1141         'job':              't0016-d54ae22.fio',
1142         'success':          SUCCESS_DEFAULT,
1143         'pre_job':          None,
1144         'pre_success':      None,
1145         'output_format':    'json',
1146         'requirements':     [],
1147     },
1148     {
1149         'test_id':          17,
1150         'test_class':       FioJobTest_t0015,
1151         'job':              't0017.fio',
1152         'success':          SUCCESS_DEFAULT,
1153         'pre_job':          None,
1154         'pre_success':      None,
1155         'output_format':    'json',
1156         'requirements':     [Requirements.not_windows],
1157     },
1158     {
1159         'test_id':          18,
1160         'test_class':       FioJobTest,
1161         'job':              't0018.fio',
1162         'success':          SUCCESS_DEFAULT,
1163         'pre_job':          None,
1164         'pre_success':      None,
1165         'requirements':     [Requirements.linux, Requirements.io_uring],
1166     },
1167     {
1168         'test_id':          19,
1169         'test_class':       FioJobTest_t0019,
1170         'job':              't0019.fio',
1171         'success':          SUCCESS_DEFAULT,
1172         'pre_job':          None,
1173         'pre_success':      None,
1174         'requirements':     [],
1175     },
1176     {
1177         'test_id':          20,
1178         'test_class':       FioJobTest_t0020,
1179         'job':              't0020.fio',
1180         'success':          SUCCESS_DEFAULT,
1181         'pre_job':          None,
1182         'pre_success':      None,
1183         'requirements':     [],
1184     },
1185     {
1186         'test_id':          21,
1187         'test_class':       FioJobTest_t0020,
1188         'job':              't0021.fio',
1189         'success':          SUCCESS_DEFAULT,
1190         'pre_job':          None,
1191         'pre_success':      None,
1192         'requirements':     [],
1193     },
1194     {
1195         'test_id':          22,
1196         'test_class':       FioJobTest_t0022,
1197         'job':              't0022.fio',
1198         'success':          SUCCESS_DEFAULT,
1199         'pre_job':          None,
1200         'pre_success':      None,
1201         'requirements':     [],
1202     },
1203     {
1204         'test_id':          23,
1205         'test_class':       FioJobTest_t0023,
1206         'job':              't0023.fio',
1207         'success':          SUCCESS_DEFAULT,
1208         'pre_job':          None,
1209         'pre_success':      None,
1210         'requirements':     [],
1211     },
1212     {
1213         'test_id':          24,
1214         'test_class':       FioJobTest_t0024,
1215         'job':              't0024.fio',
1216         'success':          SUCCESS_DEFAULT,
1217         'pre_job':          None,
1218         'pre_success':      None,
1219         'requirements':     [],
1220     },
1221     {
1222         'test_id':          25,
1223         'test_class':       FioJobTest_t0025,
1224         'job':              't0025.fio',
1225         'success':          SUCCESS_DEFAULT,
1226         'pre_job':          None,
1227         'pre_success':      None,
1228         'output_format':    'json',
1229         'requirements':     [],
1230     },
1231     {
1232         'test_id':          26,
1233         'test_class':       FioJobTest,
1234         'job':              't0026.fio',
1235         'success':          SUCCESS_DEFAULT,
1236         'pre_job':          None,
1237         'pre_success':      None,
1238         'requirements':     [Requirements.not_windows],
1239     },
1240     {
1241         'test_id':          27,
1242         'test_class':       FioJobTest_t0027,
1243         'job':              't0027.fio',
1244         'success':          SUCCESS_DEFAULT,
1245         'pre_job':          None,
1246         'pre_success':      None,
1247         'requirements':     [],
1248     },
1249     {
1250         'test_id':          1000,
1251         'test_class':       FioExeTest,
1252         'exe':              't/axmap',
1253         'parameters':       None,
1254         'success':          SUCCESS_DEFAULT,
1255         'requirements':     [],
1256     },
1257     {
1258         'test_id':          1001,
1259         'test_class':       FioExeTest,
1260         'exe':              't/ieee754',
1261         'parameters':       None,
1262         'success':          SUCCESS_DEFAULT,
1263         'requirements':     [],
1264     },
1265     {
1266         'test_id':          1002,
1267         'test_class':       FioExeTest,
1268         'exe':              't/lfsr-test',
1269         'parameters':       ['0xFFFFFF', '0', '0', 'verify'],
1270         'success':          SUCCESS_STDERR,
1271         'requirements':     [],
1272     },
1273     {
1274         'test_id':          1003,
1275         'test_class':       FioExeTest,
1276         'exe':              't/readonly.py',
1277         'parameters':       ['-f', '{fio_path}'],
1278         'success':          SUCCESS_DEFAULT,
1279         'requirements':     [],
1280     },
1281     {
1282         'test_id':          1004,
1283         'test_class':       FioExeTest,
1284         'exe':              't/steadystate_tests.py',
1285         'parameters':       ['{fio_path}'],
1286         'success':          SUCCESS_DEFAULT,
1287         'requirements':     [],
1288     },
1289     {
1290         'test_id':          1005,
1291         'test_class':       FioExeTest,
1292         'exe':              't/stest',
1293         'parameters':       None,
1294         'success':          SUCCESS_STDERR,
1295         'requirements':     [],
1296     },
1297     {
1298         'test_id':          1006,
1299         'test_class':       FioExeTest,
1300         'exe':              't/strided.py',
1301         'parameters':       ['{fio_path}'],
1302         'success':          SUCCESS_DEFAULT,
1303         'requirements':     [],
1304     },
1305     {
1306         'test_id':          1007,
1307         'test_class':       FioExeTest,
1308         'exe':              't/zbd/run-tests-against-nullb',
1309         'parameters':       ['-s', '1'],
1310         'success':          SUCCESS_DEFAULT,
1311         'requirements':     [Requirements.linux, Requirements.zbd,
1312                              Requirements.root],
1313     },
1314     {
1315         'test_id':          1008,
1316         'test_class':       FioExeTest,
1317         'exe':              't/zbd/run-tests-against-nullb',
1318         'parameters':       ['-s', '2'],
1319         'success':          SUCCESS_DEFAULT,
1320         'requirements':     [Requirements.linux, Requirements.zbd,
1321                              Requirements.root, Requirements.zoned_nullb],
1322     },
1323     {
1324         'test_id':          1009,
1325         'test_class':       FioExeTest,
1326         'exe':              'unittests/unittest',
1327         'parameters':       None,
1328         'success':          SUCCESS_DEFAULT,
1329         'requirements':     [Requirements.unittests],
1330     },
1331     {
1332         'test_id':          1010,
1333         'test_class':       FioExeTest,
1334         'exe':              't/latency_percentiles.py',
1335         'parameters':       ['-f', '{fio_path}'],
1336         'success':          SUCCESS_DEFAULT,
1337         'requirements':     [],
1338     },
1339     {
1340         'test_id':          1011,
1341         'test_class':       FioExeTest,
1342         'exe':              't/jsonplus2csv_test.py',
1343         'parameters':       ['-f', '{fio_path}'],
1344         'success':          SUCCESS_DEFAULT,
1345         'requirements':     [],
1346     },
1347     {
1348         'test_id':          1012,
1349         'test_class':       FioExeTest,
1350         'exe':              't/log_compression.py',
1351         'parameters':       ['-f', '{fio_path}'],
1352         'success':          SUCCESS_DEFAULT,
1353         'requirements':     [],
1354     },
1355 ]
1356
1357
1358 def parse_args():
1359     """Parse command-line arguments."""
1360
1361     parser = argparse.ArgumentParser()
1362     parser.add_argument('-r', '--fio-root',
1363                         help='fio root path')
1364     parser.add_argument('-f', '--fio',
1365                         help='path to fio executable (e.g., ./fio)')
1366     parser.add_argument('-a', '--artifact-root',
1367                         help='artifact root directory')
1368     parser.add_argument('-s', '--skip', nargs='+', type=int,
1369                         help='list of test(s) to skip')
1370     parser.add_argument('-o', '--run-only', nargs='+', type=int,
1371                         help='list of test(s) to run, skipping all others')
1372     parser.add_argument('-d', '--debug', action='store_true',
1373                         help='provide debug output')
1374     parser.add_argument('-k', '--skip-req', action='store_true',
1375                         help='skip requirements checking')
1376     parser.add_argument('-p', '--pass-through', action='append',
1377                         help='pass-through an argument to an executable test')
1378     args = parser.parse_args()
1379
1380     return args
1381
1382
1383 def main():
1384     """Entry point."""
1385
1386     args = parse_args()
1387     if args.debug:
1388         logging.basicConfig(level=logging.DEBUG)
1389     else:
1390         logging.basicConfig(level=logging.INFO)
1391
1392     pass_through = {}
1393     if args.pass_through:
1394         for arg in args.pass_through:
1395             if not ':' in arg:
1396                 print("Invalid --pass-through argument '%s'" % arg)
1397                 print("Syntax for --pass-through is TESTNUMBER:ARGUMENT")
1398                 return
1399             split = arg.split(":", 1)
1400             pass_through[int(split[0])] = split[1]
1401         logging.debug("Pass-through arguments: %s", pass_through)
1402
1403     if args.fio_root:
1404         fio_root = args.fio_root
1405     else:
1406         fio_root = str(Path(__file__).absolute().parent.parent)
1407     print("fio root is %s" % fio_root)
1408
1409     if args.fio:
1410         fio_path = args.fio
1411     else:
1412         if platform.system() == "Windows":
1413             fio_exe = "fio.exe"
1414         else:
1415             fio_exe = "fio"
1416         fio_path = os.path.join(fio_root, fio_exe)
1417     print("fio path is %s" % fio_path)
1418     if not shutil.which(fio_path):
1419         print("Warning: fio executable not found")
1420
1421     artifact_root = args.artifact_root if args.artifact_root else \
1422         "fio-test-{0}".format(time.strftime("%Y%m%d-%H%M%S"))
1423     os.mkdir(artifact_root)
1424     print("Artifact directory is %s" % artifact_root)
1425
1426     if not args.skip_req:
1427         req = Requirements(fio_root)
1428
1429     passed = 0
1430     failed = 0
1431     skipped = 0
1432
1433     for config in TEST_LIST:
1434         if (args.skip and config['test_id'] in args.skip) or \
1435            (args.run_only and config['test_id'] not in args.run_only):
1436             skipped = skipped + 1
1437             print("Test {0} SKIPPED (User request)".format(config['test_id']))
1438             continue
1439
1440         if issubclass(config['test_class'], FioJobTest):
1441             if config['pre_job']:
1442                 fio_pre_job = os.path.join(fio_root, 't', 'jobs',
1443                                            config['pre_job'])
1444             else:
1445                 fio_pre_job = None
1446             if config['pre_success']:
1447                 fio_pre_success = config['pre_success']
1448             else:
1449                 fio_pre_success = None
1450             if 'output_format' in config:
1451                 output_format = config['output_format']
1452             else:
1453                 output_format = 'normal'
1454             test = config['test_class'](
1455                 fio_path,
1456                 os.path.join(fio_root, 't', 'jobs', config['job']),
1457                 config['success'],
1458                 fio_pre_job=fio_pre_job,
1459                 fio_pre_success=fio_pre_success,
1460                 output_format=output_format)
1461             desc = config['job']
1462         elif issubclass(config['test_class'], FioExeTest):
1463             exe_path = os.path.join(fio_root, config['exe'])
1464             if config['parameters']:
1465                 parameters = [p.format(fio_path=fio_path) for p in config['parameters']]
1466             else:
1467                 parameters = []
1468             if Path(exe_path).suffix == '.py' and platform.system() == "Windows":
1469                 parameters.insert(0, exe_path)
1470                 exe_path = "python.exe"
1471             if config['test_id'] in pass_through:
1472                 parameters += pass_through[config['test_id']].split()
1473             test = config['test_class'](exe_path, parameters,
1474                                         config['success'])
1475             desc = config['exe']
1476         else:
1477             print("Test {0} FAILED: unable to process test config".format(config['test_id']))
1478             failed = failed + 1
1479             continue
1480
1481         if not args.skip_req:
1482             reqs_met = True
1483             for req in config['requirements']:
1484                 reqs_met, reason = req()
1485                 logging.debug("Test %d: Requirement '%s' met? %s", config['test_id'], reason,
1486                               reqs_met)
1487                 if not reqs_met:
1488                     break
1489             if not reqs_met:
1490                 print("Test {0} SKIPPED ({1}) {2}".format(config['test_id'], reason, desc))
1491                 skipped = skipped + 1
1492                 continue
1493
1494         try:
1495             test.setup(artifact_root, config['test_id'])
1496             test.run()
1497             test.check_result()
1498         except KeyboardInterrupt:
1499             break
1500         except Exception as e:
1501             test.passed = False
1502             test.failure_reason += str(e)
1503             logging.debug("Test %d exception:\n%s\n", config['test_id'], traceback.format_exc())
1504         if test.passed:
1505             result = "PASSED"
1506             passed = passed + 1
1507         else:
1508             result = "FAILED: {0}".format(test.failure_reason)
1509             failed = failed + 1
1510             contents, _ = FioJobTest.get_file(test.stderr_file)
1511             logging.debug("Test %d: stderr:\n%s", config['test_id'], contents)
1512             contents, _ = FioJobTest.get_file(test.stdout_file)
1513             logging.debug("Test %d: stdout:\n%s", config['test_id'], contents)
1514         print("Test {0} {1} {2}".format(config['test_id'], result, desc))
1515
1516     print("{0} test(s) passed, {1} failed, {2} skipped".format(passed, failed, skipped))
1517
1518     sys.exit(failed)
1519
1520
1521 if __name__ == '__main__':
1522     main()