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