updated logging of iops1, iops2, ratio in FioJobTest_iops_rate
[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(object):
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, lopping off up to the first four lines
315         #
316         lines = file_data.splitlines()
317         for i in range(5):
318             file_data = '\n'.join(lines[i:])
319             try:
320                 self.json_data = json.loads(file_data)
321             except json.JSONDecodeError:
322                 continue
323             else:
324                 logging.debug("Test %d: skipped %d lines decoding JSON data", self.testnum, i)
325                 return
326
327         self.failure_reason = "{0} unable to decode JSON data,".format(self.failure_reason)
328         self.passed = False
329
330
331 class FioJobTest_t0005(FioJobTest):
332     """Test consists of fio test job t0005
333     Confirm that read['io_kbytes'] == write['io_kbytes'] == 102400"""
334
335     def check_result(self):
336         super(FioJobTest_t0005, self).check_result()
337
338         if not self.passed:
339             return
340
341         if self.json_data['jobs'][0]['read']['io_kbytes'] != 102400:
342             self.failure_reason = "{0} bytes read mismatch,".format(self.failure_reason)
343             self.passed = False
344         if self.json_data['jobs'][0]['write']['io_kbytes'] != 102400:
345             self.failure_reason = "{0} bytes written mismatch,".format(self.failure_reason)
346             self.passed = False
347
348
349 class FioJobTest_t0006(FioJobTest):
350     """Test consists of fio test job t0006
351     Confirm that read['io_kbytes'] ~ 2*write['io_kbytes']"""
352
353     def check_result(self):
354         super(FioJobTest_t0006, self).check_result()
355
356         if not self.passed:
357             return
358
359         ratio = self.json_data['jobs'][0]['read']['io_kbytes'] \
360             / self.json_data['jobs'][0]['write']['io_kbytes']
361         logging.debug("Test %d: ratio: %f", self.testnum, ratio)
362         if ratio < 1.99 or ratio > 2.01:
363             self.failure_reason = "{0} read/write ratio mismatch,".format(self.failure_reason)
364             self.passed = False
365
366
367 class FioJobTest_t0007(FioJobTest):
368     """Test consists of fio test job t0007
369     Confirm that read['io_kbytes'] = 87040"""
370
371     def check_result(self):
372         super(FioJobTest_t0007, self).check_result()
373
374         if not self.passed:
375             return
376
377         if self.json_data['jobs'][0]['read']['io_kbytes'] != 87040:
378             self.failure_reason = "{0} bytes read mismatch,".format(self.failure_reason)
379             self.passed = False
380
381
382 class FioJobTest_t0008(FioJobTest):
383     """Test consists of fio test job t0008
384     Confirm that read['io_kbytes'] = 32768 and that
385                 write['io_kbytes'] ~ 16568
386
387     I did runs with fio-ae2fafc8 and saw write['io_kbytes'] values of
388     16585, 16588. With two runs of fio-3.16 I obtained 16568"""
389
390     def check_result(self):
391         super(FioJobTest_t0008, self).check_result()
392
393         if not self.passed:
394             return
395
396         ratio = self.json_data['jobs'][0]['write']['io_kbytes'] / 16568
397         logging.debug("Test %d: ratio: %f", self.testnum, ratio)
398
399         if ratio < 0.99 or ratio > 1.01:
400             self.failure_reason = "{0} bytes written mismatch,".format(self.failure_reason)
401             self.passed = False
402         if self.json_data['jobs'][0]['read']['io_kbytes'] != 32768:
403             self.failure_reason = "{0} bytes read mismatch,".format(self.failure_reason)
404             self.passed = False
405
406
407 class FioJobTest_t0009(FioJobTest):
408     """Test consists of fio test job t0009
409     Confirm that runtime >= 60s"""
410
411     def check_result(self):
412         super(FioJobTest_t0009, self).check_result()
413
414         if not self.passed:
415             return
416
417         logging.debug('Test %d: elapsed: %d', self.testnum, self.json_data['jobs'][0]['elapsed'])
418
419         if self.json_data['jobs'][0]['elapsed'] < 60:
420             self.failure_reason = "{0} elapsed time mismatch,".format(self.failure_reason)
421             self.passed = False
422
423
424 class FioJobTest_t0012(FioJobTest):
425     """Test consists of fio test job t0012
426     Confirm ratios of job iops are 1:5:10
427     job1,job2,job3 respectively"""
428
429     def check_result(self):
430         super(FioJobTest_t0012, self).check_result()
431
432         if not self.passed:
433             return
434
435         iops_files = []
436         for i in range(1,4):
437             file_data, success = self.get_file(os.path.join(self.test_dir, "{0}_iops.{1}.log".format(os.path.basename(self.fio_job), i)))
438
439             if not success:
440                 self.failure_reason = "{0} unable to open output file,".format(self.failure_reason)
441                 self.passed = False
442                 return
443
444             iops_files.append(file_data.splitlines())
445
446         # there are 9 samples for job1 and job2, 4 samples for job3
447         iops1 = 0.0
448         iops2 = 0.0
449         iops3 = 0.0
450         for i in range(9):
451             iops1 = iops1 + float(iops_files[0][i].split(',')[1])
452             iops2 = iops2 + float(iops_files[1][i].split(',')[1])
453             iops3 = iops3 + float(iops_files[2][i].split(',')[1])
454
455             ratio1 = iops3/iops2
456             ratio2 = iops3/iops1
457             logging.debug(
458                 "sample {0}: job1 iops={1} job2 iops={2} job3 iops={3} job3/job2={4:.3f} job3/job1={5:.3f}".format(
459                     i, iops1, iops2, iops3, ratio1, ratio2
460                 )
461             )
462
463         # test job1 and job2 succeeded to recalibrate
464         if ratio1 < 1 or ratio1 > 3 or ratio2 < 7 or ratio2 > 13:
465             self.failure_reason = "{0} iops ratio mismatch iops1={1} iops2={2} iops3={3} expected r1~2 r2~10 got r1={4:.3f} r2={5:.3f},".format(
466                 self.failure_reason, iops1, iops2, iops3, ratio1, ratio2
467             )
468             self.passed = False
469             return
470
471
472 class FioJobTest_t0014(FioJobTest):
473     """Test consists of fio test job t0014
474         Confirm that job1_iops / job2_iops ~ 1:2 for entire duration
475         and that job1_iops / job3_iops ~ 1:3 for first half of duration.
476
477     The test is about making sure the flow feature can
478     re-calibrate the activity dynamically"""
479
480     def check_result(self):
481         super(FioJobTest_t0014, self).check_result()
482
483         if not self.passed:
484             return
485
486         iops_files = []
487         for i in range(1,4):
488             file_data, success = self.get_file(os.path.join(self.test_dir, "{0}_iops.{1}.log".format(os.path.basename(self.fio_job), i)))
489
490             if not success:
491                 self.failure_reason = "{0} unable to open output file,".format(self.failure_reason)
492                 self.passed = False
493                 return
494
495             iops_files.append(file_data.splitlines())
496
497         # there are 9 samples for job1 and job2, 4 samples for job3
498         iops1 = 0.0
499         iops2 = 0.0
500         iops3 = 0.0
501         for i in range(9):
502             if i < 4:
503                 iops3 = iops3 + float(iops_files[2][i].split(',')[1])
504             elif i == 4:
505                 ratio1 = iops1 / iops2
506                 ratio2 = iops1 / iops3
507
508
509                 if ratio1 < 0.43 or ratio1 > 0.57 or ratio2 < 0.21 or ratio2 > 0.45:
510                     self.failure_reason = "{0} iops ratio mismatch iops1={1} iops2={2} iops3={3}\
511                                                 expected r1~0.5 r2~0.33 got r1={4:.3f} r2={5:.3f},".format(
512                         self.failure_reason, iops1, iops2, iops3, ratio1, ratio2
513                     )
514                     self.passed = False
515
516             iops1 = iops1 + float(iops_files[0][i].split(',')[1])
517             iops2 = iops2 + float(iops_files[1][i].split(',')[1])
518
519             ratio1 = iops1/iops2
520             ratio2 = iops1/iops3
521             logging.debug(
522                 "sample {0}: job1 iops={1} job2 iops={2} job3 iops={3} job1/job2={4:.3f} job1/job3={5:.3f}".format(
523                     i, iops1, iops2, iops3, ratio1, ratio2
524                 )
525             )
526
527         # test job1 and job2 succeeded to recalibrate
528         if ratio1 < 0.43 or ratio1 > 0.57:
529             self.failure_reason = "{0} iops ratio mismatch iops1={1} iops2={2} expected ratio~0.5 got ratio={3:.3f},".format(
530                 self.failure_reason, iops1, iops2, ratio1
531             )
532             self.passed = False
533             return
534
535
536 class FioJobTest_iops_rate(FioJobTest):
537     """Test consists of fio test job t0009
538     Confirm that job0 iops == 1000
539     and that job1_iops / job0_iops ~ 8
540     With two runs of fio-3.16 I observed a ratio of 8.3"""
541
542     def check_result(self):
543         super(FioJobTest_iops_rate, self).check_result()
544
545         if not self.passed:
546             return
547
548         iops1 = self.json_data['jobs'][0]['read']['iops']
549         logging.debug("Test %d: iops1: %f", self.testnum, iops1)
550         iops2 = self.json_data['jobs'][1]['read']['iops']
551         logging.debug("Test %d: iops2: %f", self.testnum, iops2)
552         ratio = iops2 / iops1
553         logging.debug("Test %d: ratio: %f", self.testnum, ratio)
554
555         if iops1 < 950 or iops1 > 1050:
556             self.failure_reason = "{0} iops value mismatch,".format(self.failure_reason)
557             self.passed = False
558
559         if ratio < 6 or ratio > 10:
560             self.failure_reason = "{0} iops ratio mismatch,".format(self.failure_reason)
561             self.passed = False
562
563
564 class Requirements(object):
565     """Requirements consists of multiple run environment characteristics.
566     These are to determine if a particular test can be run"""
567
568     _linux = False
569     _libaio = False
570     _zbd = False
571     _root = False
572     _zoned_nullb = False
573     _not_macos = False
574     _not_windows = False
575     _unittests = False
576     _cpucount4 = False
577
578     def __init__(self, fio_root):
579         Requirements._not_macos = platform.system() != "Darwin"
580         Requirements._not_windows = platform.system() != "Windows"
581         Requirements._linux = platform.system() == "Linux"
582
583         if Requirements._linux:
584             config_file = os.path.join(fio_root, "config-host.h")
585             contents, success = FioJobTest.get_file(config_file)
586             if not success:
587                 print("Unable to open {0} to check requirements".format(config_file))
588                 Requirements._zbd = True
589             else:
590                 Requirements._zbd = "CONFIG_HAS_BLKZONED" in contents
591                 Requirements._libaio = "CONFIG_LIBAIO" in contents
592
593             Requirements._root = (os.geteuid() == 0)
594             if Requirements._zbd and Requirements._root:
595                 try:
596                     subprocess.run(["modprobe", "null_blk"],
597                                    stdout=subprocess.PIPE,
598                                    stderr=subprocess.PIPE)
599                     if os.path.exists("/sys/module/null_blk/parameters/zoned"):
600                         Requirements._zoned_nullb = True
601                 except Exception:
602                     pass
603
604         if platform.system() == "Windows":
605             utest_exe = "unittest.exe"
606         else:
607             utest_exe = "unittest"
608         unittest_path = os.path.join(fio_root, "unittests", utest_exe)
609         Requirements._unittests = os.path.exists(unittest_path)
610
611         Requirements._cpucount4 = multiprocessing.cpu_count() >= 4
612
613         req_list = [Requirements.linux,
614                     Requirements.libaio,
615                     Requirements.zbd,
616                     Requirements.root,
617                     Requirements.zoned_nullb,
618                     Requirements.not_macos,
619                     Requirements.not_windows,
620                     Requirements.unittests,
621                     Requirements.cpucount4]
622         for req in req_list:
623             value, desc = req()
624             logging.debug("Requirements: Requirement '%s' met? %s", desc, value)
625
626     @classmethod
627     def linux(cls):
628         """Are we running on Linux?"""
629         return Requirements._linux, "Linux required"
630
631     @classmethod
632     def libaio(cls):
633         """Is libaio available?"""
634         return Requirements._libaio, "libaio required"
635
636     @classmethod
637     def zbd(cls):
638         """Is ZBD support available?"""
639         return Requirements._zbd, "Zoned block device support required"
640
641     @classmethod
642     def root(cls):
643         """Are we running as root?"""
644         return Requirements._root, "root required"
645
646     @classmethod
647     def zoned_nullb(cls):
648         """Are zoned null block devices available?"""
649         return Requirements._zoned_nullb, "Zoned null block device support required"
650
651     @classmethod
652     def not_macos(cls):
653         """Are we running on a platform other than macOS?"""
654         return Requirements._not_macos, "platform other than macOS required"
655
656     @classmethod
657     def not_windows(cls):
658         """Are we running on a platform other than Windws?"""
659         return Requirements._not_windows, "platform other than Windows required"
660
661     @classmethod
662     def unittests(cls):
663         """Were unittests built?"""
664         return Requirements._unittests, "Unittests support required"
665
666     @classmethod
667     def cpucount4(cls):
668         """Do we have at least 4 CPUs?"""
669         return Requirements._cpucount4, "4+ CPUs required"
670
671
672 SUCCESS_DEFAULT = {
673     'zero_return': True,
674     'stderr_empty': True,
675     'timeout': 600,
676     }
677 SUCCESS_NONZERO = {
678     'zero_return': False,
679     'stderr_empty': False,
680     'timeout': 600,
681     }
682 SUCCESS_STDERR = {
683     'zero_return': True,
684     'stderr_empty': False,
685     'timeout': 600,
686     }
687 TEST_LIST = [
688     {
689         'test_id':          1,
690         'test_class':       FioJobTest,
691         'job':              't0001-52c58027.fio',
692         'success':          SUCCESS_DEFAULT,
693         'pre_job':          None,
694         'pre_success':      None,
695         'requirements':     [],
696     },
697     {
698         'test_id':          2,
699         'test_class':       FioJobTest,
700         'job':              't0002-13af05ae-post.fio',
701         'success':          SUCCESS_DEFAULT,
702         'pre_job':          't0002-13af05ae-pre.fio',
703         'pre_success':      None,
704         'requirements':     [Requirements.linux, Requirements.libaio],
705     },
706     {
707         'test_id':          3,
708         'test_class':       FioJobTest,
709         'job':              't0003-0ae2c6e1-post.fio',
710         'success':          SUCCESS_NONZERO,
711         'pre_job':          't0003-0ae2c6e1-pre.fio',
712         'pre_success':      SUCCESS_DEFAULT,
713         'requirements':     [Requirements.linux, Requirements.libaio],
714     },
715     {
716         'test_id':          4,
717         'test_class':       FioJobTest,
718         'job':              't0004-8a99fdf6.fio',
719         'success':          SUCCESS_DEFAULT,
720         'pre_job':          None,
721         'pre_success':      None,
722         'requirements':     [Requirements.linux, Requirements.libaio],
723     },
724     {
725         'test_id':          5,
726         'test_class':       FioJobTest_t0005,
727         'job':              't0005-f7078f7b.fio',
728         'success':          SUCCESS_DEFAULT,
729         'pre_job':          None,
730         'pre_success':      None,
731         'output_format':    'json',
732         'requirements':     [Requirements.not_windows],
733     },
734     {
735         'test_id':          6,
736         'test_class':       FioJobTest_t0006,
737         'job':              't0006-82af2a7c.fio',
738         'success':          SUCCESS_DEFAULT,
739         'pre_job':          None,
740         'pre_success':      None,
741         'output_format':    'json',
742         'requirements':     [Requirements.linux, Requirements.libaio],
743     },
744     {
745         'test_id':          7,
746         'test_class':       FioJobTest_t0007,
747         'job':              't0007-37cf9e3c.fio',
748         'success':          SUCCESS_DEFAULT,
749         'pre_job':          None,
750         'pre_success':      None,
751         'output_format':    'json',
752         'requirements':     [],
753     },
754     {
755         'test_id':          8,
756         'test_class':       FioJobTest_t0008,
757         'job':              't0008-ae2fafc8.fio',
758         'success':          SUCCESS_DEFAULT,
759         'pre_job':          None,
760         'pre_success':      None,
761         'output_format':    'json',
762         'requirements':     [],
763     },
764     {
765         'test_id':          9,
766         'test_class':       FioJobTest_t0009,
767         'job':              't0009-f8b0bd10.fio',
768         'success':          SUCCESS_DEFAULT,
769         'pre_job':          None,
770         'pre_success':      None,
771         'output_format':    'json',
772         'requirements':     [Requirements.not_macos,
773                              Requirements.cpucount4],
774         # mac os does not support CPU affinity
775     },
776     {
777         'test_id':          10,
778         'test_class':       FioJobTest,
779         'job':              't0010-b7aae4ba.fio',
780         'success':          SUCCESS_DEFAULT,
781         'pre_job':          None,
782         'pre_success':      None,
783         'requirements':     [],
784     },
785     {
786         'test_id':          11,
787         'test_class':       FioJobTest_iops_rate,
788         'job':              't0011-5d2788d5.fio',
789         'success':          SUCCESS_DEFAULT,
790         'pre_job':          None,
791         'pre_success':      None,
792         'output_format':    'json',
793         'requirements':     [],
794     },
795     {
796         'test_id':          12,
797         'test_class':       FioJobTest_t0012,
798         'job':              't0012.fio',
799         'success':          SUCCESS_DEFAULT,
800         'pre_job':          None,
801         'pre_success':      None,
802         'output_format':    'json',
803         'requirements':     [],
804     },
805     {
806         'test_id':          13,
807         'test_class':       FioJobTest,
808         'job':              't0013.fio',
809         'success':          SUCCESS_DEFAULT,
810         'pre_job':          None,
811         'pre_success':      None,
812         'output_format':    'json',
813         'requirements':     [],
814     },
815     {
816         'test_id':          14,
817         'test_class':       FioJobTest_t0014,
818         'job':              't0014.fio',
819         'success':          SUCCESS_DEFAULT,
820         'pre_job':          None,
821         'pre_success':      None,
822         'output_format':    'json',
823         'requirements':     [],
824     },
825     {
826         'test_id':          1000,
827         'test_class':       FioExeTest,
828         'exe':              't/axmap',
829         'parameters':       None,
830         'success':          SUCCESS_DEFAULT,
831         'requirements':     [],
832     },
833     {
834         'test_id':          1001,
835         'test_class':       FioExeTest,
836         'exe':              't/ieee754',
837         'parameters':       None,
838         'success':          SUCCESS_DEFAULT,
839         'requirements':     [],
840     },
841     {
842         'test_id':          1002,
843         'test_class':       FioExeTest,
844         'exe':              't/lfsr-test',
845         'parameters':       ['0xFFFFFF', '0', '0', 'verify'],
846         'success':          SUCCESS_STDERR,
847         'requirements':     [],
848     },
849     {
850         'test_id':          1003,
851         'test_class':       FioExeTest,
852         'exe':              't/readonly.py',
853         'parameters':       ['-f', '{fio_path}'],
854         'success':          SUCCESS_DEFAULT,
855         'requirements':     [],
856     },
857     {
858         'test_id':          1004,
859         'test_class':       FioExeTest,
860         'exe':              't/steadystate_tests.py',
861         'parameters':       ['{fio_path}'],
862         'success':          SUCCESS_DEFAULT,
863         'requirements':     [],
864     },
865     {
866         'test_id':          1005,
867         'test_class':       FioExeTest,
868         'exe':              't/stest',
869         'parameters':       None,
870         'success':          SUCCESS_STDERR,
871         'requirements':     [],
872     },
873     {
874         'test_id':          1006,
875         'test_class':       FioExeTest,
876         'exe':              't/strided.py',
877         'parameters':       ['{fio_path}'],
878         'success':          SUCCESS_DEFAULT,
879         'requirements':     [],
880     },
881     {
882         'test_id':          1007,
883         'test_class':       FioExeTest,
884         'exe':              't/zbd/run-tests-against-nullb',
885         'parameters':       ['-s', '1'],
886         'success':          SUCCESS_DEFAULT,
887         'requirements':     [Requirements.linux, Requirements.zbd,
888                              Requirements.root],
889     },
890     {
891         'test_id':          1008,
892         'test_class':       FioExeTest,
893         'exe':              't/zbd/run-tests-against-nullb',
894         'parameters':       ['-s', '2'],
895         'success':          SUCCESS_DEFAULT,
896         'requirements':     [Requirements.linux, Requirements.zbd,
897                              Requirements.root, Requirements.zoned_nullb],
898     },
899     {
900         'test_id':          1009,
901         'test_class':       FioExeTest,
902         'exe':              'unittests/unittest',
903         'parameters':       None,
904         'success':          SUCCESS_DEFAULT,
905         'requirements':     [Requirements.unittests],
906     },
907     {
908         'test_id':          1010,
909         'test_class':       FioExeTest,
910         'exe':              't/latency_percentiles.py',
911         'parameters':       ['-f', '{fio_path}'],
912         'success':          SUCCESS_DEFAULT,
913         'requirements':     [],
914     },
915     {
916         'test_id':          1011,
917         'test_class':       FioExeTest,
918         'exe':              't/jsonplus2csv_test.py',
919         'parameters':       ['-f', '{fio_path}'],
920         'success':          SUCCESS_DEFAULT,
921         'requirements':     [],
922     },
923 ]
924
925
926 def parse_args():
927     """Parse command-line arguments."""
928
929     parser = argparse.ArgumentParser()
930     parser.add_argument('-r', '--fio-root',
931                         help='fio root path')
932     parser.add_argument('-f', '--fio',
933                         help='path to fio executable (e.g., ./fio)')
934     parser.add_argument('-a', '--artifact-root',
935                         help='artifact root directory')
936     parser.add_argument('-s', '--skip', nargs='+', type=int,
937                         help='list of test(s) to skip')
938     parser.add_argument('-o', '--run-only', nargs='+', type=int,
939                         help='list of test(s) to run, skipping all others')
940     parser.add_argument('-d', '--debug', action='store_true',
941                         help='provide debug output')
942     parser.add_argument('-k', '--skip-req', action='store_true',
943                         help='skip requirements checking')
944     parser.add_argument('-p', '--pass-through', action='append',
945                         help='pass-through an argument to an executable test')
946     args = parser.parse_args()
947
948     return args
949
950
951 def main():
952     """Entry point."""
953
954     args = parse_args()
955     if args.debug:
956         logging.basicConfig(level=logging.DEBUG)
957     else:
958         logging.basicConfig(level=logging.INFO)
959
960     pass_through = {}
961     if args.pass_through:
962         for arg in args.pass_through:
963             if not ':' in arg:
964                 print("Invalid --pass-through argument '%s'" % arg)
965                 print("Syntax for --pass-through is TESTNUMBER:ARGUMENT")
966                 return
967             split = arg.split(":", 1)
968             pass_through[int(split[0])] = split[1]
969         logging.debug("Pass-through arguments: %s", pass_through)
970
971     if args.fio_root:
972         fio_root = args.fio_root
973     else:
974         fio_root = str(Path(__file__).absolute().parent.parent)
975     print("fio root is %s" % fio_root)
976
977     if args.fio:
978         fio_path = args.fio
979     else:
980         if platform.system() == "Windows":
981             fio_exe = "fio.exe"
982         else:
983             fio_exe = "fio"
984         fio_path = os.path.join(fio_root, fio_exe)
985     print("fio path is %s" % fio_path)
986     if not shutil.which(fio_path):
987         print("Warning: fio executable not found")
988
989     artifact_root = args.artifact_root if args.artifact_root else \
990         "fio-test-{0}".format(time.strftime("%Y%m%d-%H%M%S"))
991     os.mkdir(artifact_root)
992     print("Artifact directory is %s" % artifact_root)
993
994     if not args.skip_req:
995         req = Requirements(fio_root)
996
997     passed = 0
998     failed = 0
999     skipped = 0
1000
1001     for config in TEST_LIST:
1002         if (args.skip and config['test_id'] in args.skip) or \
1003            (args.run_only and config['test_id'] not in args.run_only):
1004             skipped = skipped + 1
1005             print("Test {0} SKIPPED (User request)".format(config['test_id']))
1006             continue
1007
1008         if issubclass(config['test_class'], FioJobTest):
1009             if config['pre_job']:
1010                 fio_pre_job = os.path.join(fio_root, 't', 'jobs',
1011                                            config['pre_job'])
1012             else:
1013                 fio_pre_job = None
1014             if config['pre_success']:
1015                 fio_pre_success = config['pre_success']
1016             else:
1017                 fio_pre_success = None
1018             if 'output_format' in config:
1019                 output_format = config['output_format']
1020             else:
1021                 output_format = 'normal'
1022             test = config['test_class'](
1023                 fio_path,
1024                 os.path.join(fio_root, 't', 'jobs', config['job']),
1025                 config['success'],
1026                 fio_pre_job=fio_pre_job,
1027                 fio_pre_success=fio_pre_success,
1028                 output_format=output_format)
1029             desc = config['job']
1030         elif issubclass(config['test_class'], FioExeTest):
1031             exe_path = os.path.join(fio_root, config['exe'])
1032             if config['parameters']:
1033                 parameters = [p.format(fio_path=fio_path) for p in config['parameters']]
1034             else:
1035                 parameters = []
1036             if Path(exe_path).suffix == '.py' and platform.system() == "Windows":
1037                 parameters.insert(0, exe_path)
1038                 exe_path = "python.exe"
1039             if config['test_id'] in pass_through:
1040                 parameters += pass_through[config['test_id']].split()
1041             test = config['test_class'](exe_path, parameters,
1042                                         config['success'])
1043             desc = config['exe']
1044         else:
1045             print("Test {0} FAILED: unable to process test config".format(config['test_id']))
1046             failed = failed + 1
1047             continue
1048
1049         if not args.skip_req:
1050             reqs_met = True
1051             for req in config['requirements']:
1052                 reqs_met, reason = req()
1053                 logging.debug("Test %d: Requirement '%s' met? %s", config['test_id'], reason,
1054                               reqs_met)
1055                 if not reqs_met:
1056                     break
1057             if not reqs_met:
1058                 print("Test {0} SKIPPED ({1}) {2}".format(config['test_id'], reason, desc))
1059                 skipped = skipped + 1
1060                 continue
1061
1062         try:
1063             test.setup(artifact_root, config['test_id'])
1064             test.run()
1065             test.check_result()
1066         except KeyboardInterrupt:
1067             break
1068         except Exception as e:
1069             test.passed = False
1070             test.failure_reason += str(e)
1071             logging.debug("Test %d exception:\n%s\n", config['test_id'], traceback.format_exc())
1072         if test.passed:
1073             result = "PASSED"
1074             passed = passed + 1
1075         else:
1076             result = "FAILED: {0}".format(test.failure_reason)
1077             failed = failed + 1
1078             contents, _ = FioJobTest.get_file(test.stderr_file)
1079             logging.debug("Test %d: stderr:\n%s", config['test_id'], contents)
1080             contents, _ = FioJobTest.get_file(test.stdout_file)
1081             logging.debug("Test %d: stdout:\n%s", config['test_id'], contents)
1082         print("Test {0} {1} {2}".format(config['test_id'], result, desc))
1083
1084     print("{0} test(s) passed, {1} failed, {2} skipped".format(passed, failed, skipped))
1085
1086     sys.exit(failed)
1087
1088
1089 if __name__ == '__main__':
1090     main()