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