Merge branch 'overlap' of https://github.com/bvanassche/fio
[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 subprocess
53 import multiprocessing
54 from pathlib import Path
55
56
57 class FioTest(object):
58     """Base for all fio tests."""
59
60     def __init__(self, exe_path, parameters, success):
61         self.exe_path = exe_path
62         self.parameters = parameters
63         self.success = success
64         self.output = {}
65         self.artifact_root = None
66         self.testnum = None
67         self.test_dir = None
68         self.passed = True
69         self.failure_reason = ''
70         self.command_file = None
71         self.stdout_file = None
72         self.stderr_file = None
73         self.exitcode_file = None
74
75     def setup(self, artifact_root, testnum):
76         """Setup instance variables for test."""
77
78         self.artifact_root = artifact_root
79         self.testnum = testnum
80         self.test_dir = os.path.join(artifact_root, "{:04d}".format(testnum))
81         if not os.path.exists(self.test_dir):
82             os.mkdir(self.test_dir)
83
84         self.command_file = os.path.join(
85             self.test_dir,
86             "{0}.command".format(os.path.basename(self.exe_path)))
87         self.stdout_file = os.path.join(
88             self.test_dir,
89             "{0}.stdout".format(os.path.basename(self.exe_path)))
90         self.stderr_file = os.path.join(
91             self.test_dir,
92             "{0}.stderr".format(os.path.basename(self.exe_path)))
93         self.exitcode_file = os.path.join(
94             self.test_dir,
95             "{0}.exitcode".format(os.path.basename(self.exe_path)))
96
97     def run(self):
98         """Run the test."""
99
100         raise NotImplementedError()
101
102     def check_result(self):
103         """Check test results."""
104
105         raise NotImplementedError()
106
107
108 class FioExeTest(FioTest):
109     """Test consists of an executable binary or script"""
110
111     def __init__(self, exe_path, parameters, success):
112         """Construct a FioExeTest which is a FioTest consisting of an
113         executable binary or script.
114
115         exe_path:       location of executable binary or script
116         parameters:     list of parameters for executable
117         success:        Definition of test success
118         """
119
120         FioTest.__init__(self, exe_path, parameters, success)
121
122     def run(self):
123         """Execute the binary or script described by this instance."""
124
125         command = [self.exe_path] + self.parameters
126         command_file = open(self.command_file, "w+")
127         command_file.write("%s\n" % command)
128         command_file.close()
129
130         stdout_file = open(self.stdout_file, "w+")
131         stderr_file = open(self.stderr_file, "w+")
132         exitcode_file = open(self.exitcode_file, "w+")
133         try:
134             proc = None
135             # Avoid using subprocess.run() here because when a timeout occurs,
136             # fio will be stopped with SIGKILL. This does not give fio a
137             # chance to clean up and means that child processes may continue
138             # running and submitting IO.
139             proc = subprocess.Popen(command,
140                                     stdout=stdout_file,
141                                     stderr=stderr_file,
142                                     cwd=self.test_dir,
143                                     universal_newlines=True)
144             proc.communicate(timeout=self.success['timeout'])
145             exitcode_file.write('{0}\n'.format(proc.returncode))
146             logging.debug("Test %d: return code: %d", self.testnum, proc.returncode)
147             self.output['proc'] = proc
148         except subprocess.TimeoutExpired:
149             proc.terminate()
150             proc.communicate()
151             assert proc.poll()
152             self.output['failure'] = 'timeout'
153         except Exception:
154             if proc:
155                 if not proc.poll():
156                     proc.terminate()
157                     proc.communicate()
158             self.output['failure'] = 'exception'
159             self.output['exc_info'] = sys.exc_info()
160         finally:
161             stdout_file.close()
162             stderr_file.close()
163             exitcode_file.close()
164
165     def check_result(self):
166         """Check results of test run."""
167
168         if 'proc' not in self.output:
169             if self.output['failure'] == 'timeout':
170                 self.failure_reason = "{0} timeout,".format(self.failure_reason)
171             else:
172                 assert self.output['failure'] == 'exception'
173                 self.failure_reason = '{0} exception: {1}, {2}'.format(
174                     self.failure_reason, self.output['exc_info'][0],
175                     self.output['exc_info'][1])
176
177             self.passed = False
178             return
179
180         if 'zero_return' in self.success:
181             if self.success['zero_return']:
182                 if self.output['proc'].returncode != 0:
183                     self.passed = False
184                     self.failure_reason = "{0} non-zero return code,".format(self.failure_reason)
185             else:
186                 if self.output['proc'].returncode == 0:
187                     self.failure_reason = "{0} zero return code,".format(self.failure_reason)
188                     self.passed = False
189
190         stderr_size = os.path.getsize(self.stderr_file)
191         if 'stderr_empty' in self.success:
192             if self.success['stderr_empty']:
193                 if stderr_size != 0:
194                     self.failure_reason = "{0} stderr not empty,".format(self.failure_reason)
195                     self.passed = False
196             else:
197                 if stderr_size == 0:
198                     self.failure_reason = "{0} stderr empty,".format(self.failure_reason)
199                     self.passed = False
200
201
202 class FioJobTest(FioExeTest):
203     """Test consists of a fio job"""
204
205     def __init__(self, fio_path, fio_job, success, fio_pre_job=None,
206                  fio_pre_success=None, output_format="normal"):
207         """Construct a FioJobTest which is a FioExeTest consisting of a
208         single fio job file with an optional setup step.
209
210         fio_path:           location of fio executable
211         fio_job:            location of fio job file
212         success:            Definition of test success
213         fio_pre_job:        fio job for preconditioning
214         fio_pre_success:    Definition of test success for fio precon job
215         output_format:      normal (default), json, jsonplus, or terse
216         """
217
218         self.fio_job = fio_job
219         self.fio_pre_job = fio_pre_job
220         self.fio_pre_success = fio_pre_success if fio_pre_success else success
221         self.output_format = output_format
222         self.precon_failed = False
223         self.json_data = None
224         self.fio_output = "{0}.output".format(os.path.basename(self.fio_job))
225         self.fio_args = [
226             "--max-jobs=16",
227             "--output-format={0}".format(self.output_format),
228             "--output={0}".format(self.fio_output),
229             self.fio_job,
230             ]
231         FioExeTest.__init__(self, fio_path, self.fio_args, success)
232
233     def setup(self, artifact_root, testnum):
234         """Setup instance variables for fio job test."""
235
236         super(FioJobTest, self).setup(artifact_root, testnum)
237
238         self.command_file = os.path.join(
239             self.test_dir,
240             "{0}.command".format(os.path.basename(self.fio_job)))
241         self.stdout_file = os.path.join(
242             self.test_dir,
243             "{0}.stdout".format(os.path.basename(self.fio_job)))
244         self.stderr_file = os.path.join(
245             self.test_dir,
246             "{0}.stderr".format(os.path.basename(self.fio_job)))
247         self.exitcode_file = os.path.join(
248             self.test_dir,
249             "{0}.exitcode".format(os.path.basename(self.fio_job)))
250
251     def run_pre_job(self):
252         """Run fio job precondition step."""
253
254         precon = FioJobTest(self.exe_path, self.fio_pre_job,
255                             self.fio_pre_success,
256                             output_format=self.output_format)
257         precon.setup(self.artifact_root, self.testnum)
258         precon.run()
259         precon.check_result()
260         self.precon_failed = not precon.passed
261         self.failure_reason = precon.failure_reason
262
263     def run(self):
264         """Run fio job test."""
265
266         if self.fio_pre_job:
267             self.run_pre_job()
268
269         if not self.precon_failed:
270             super(FioJobTest, self).run()
271         else:
272             logging.debug("Test %d: precondition step failed", self.testnum)
273
274     @classmethod
275     def get_file(cls, filename):
276         """Safely read a file."""
277         file_data = ''
278         success = True
279
280         try:
281             with open(filename, "r") as output_file:
282                 file_data = output_file.read()
283         except OSError:
284             success = False
285
286         return file_data, success
287
288     def check_result(self):
289         """Check fio job results."""
290
291         if self.precon_failed:
292             self.passed = False
293             self.failure_reason = "{0} precondition step failed,".format(self.failure_reason)
294             return
295
296         super(FioJobTest, self).check_result()
297
298         if not self.passed:
299             return
300
301         if 'json' not in self.output_format:
302             return
303
304         file_data, success = self.get_file(os.path.join(self.test_dir, self.fio_output))
305         if not success:
306             self.failure_reason = "{0} unable to open output file,".format(self.failure_reason)
307             self.passed = False
308             return
309
310         #
311         # Sometimes fio informational messages are included at the top of the
312         # JSON output, especially under Windows. Try to decode output as JSON
313         # data, lopping off up to the first four lines
314         #
315         lines = file_data.splitlines()
316         for i in range(5):
317             file_data = '\n'.join(lines[i:])
318             try:
319                 self.json_data = json.loads(file_data)
320             except json.JSONDecodeError:
321                 continue
322             else:
323                 logging.debug("Test %d: skipped %d lines decoding JSON data", self.testnum, i)
324                 return
325
326         self.failure_reason = "{0} unable to decode JSON data,".format(self.failure_reason)
327         self.passed = False
328
329
330 class FioJobTest_t0005(FioJobTest):
331     """Test consists of fio test job t0005
332     Confirm that read['io_kbytes'] == write['io_kbytes'] == 102400"""
333
334     def check_result(self):
335         super(FioJobTest_t0005, self).check_result()
336
337         if not self.passed:
338             return
339
340         if self.json_data['jobs'][0]['read']['io_kbytes'] != 102400:
341             self.failure_reason = "{0} bytes read mismatch,".format(self.failure_reason)
342             self.passed = False
343         if self.json_data['jobs'][0]['write']['io_kbytes'] != 102400:
344             self.failure_reason = "{0} bytes written mismatch,".format(self.failure_reason)
345             self.passed = False
346
347
348 class FioJobTest_t0006(FioJobTest):
349     """Test consists of fio test job t0006
350     Confirm that read['io_kbytes'] ~ 2*write['io_kbytes']"""
351
352     def check_result(self):
353         super(FioJobTest_t0006, self).check_result()
354
355         if not self.passed:
356             return
357
358         ratio = self.json_data['jobs'][0]['read']['io_kbytes'] \
359             / self.json_data['jobs'][0]['write']['io_kbytes']
360         logging.debug("Test %d: ratio: %f", self.testnum, ratio)
361         if ratio < 1.99 or ratio > 2.01:
362             self.failure_reason = "{0} read/write ratio mismatch,".format(self.failure_reason)
363             self.passed = False
364
365
366 class FioJobTest_t0007(FioJobTest):
367     """Test consists of fio test job t0007
368     Confirm that read['io_kbytes'] = 87040"""
369
370     def check_result(self):
371         super(FioJobTest_t0007, self).check_result()
372
373         if not self.passed:
374             return
375
376         if self.json_data['jobs'][0]['read']['io_kbytes'] != 87040:
377             self.failure_reason = "{0} bytes read mismatch,".format(self.failure_reason)
378             self.passed = False
379
380
381 class FioJobTest_t0008(FioJobTest):
382     """Test consists of fio test job t0008
383     Confirm that read['io_kbytes'] = 32768 and that
384                 write['io_kbytes'] ~ 16568
385
386     I did runs with fio-ae2fafc8 and saw write['io_kbytes'] values of
387     16585, 16588. With two runs of fio-3.16 I obtained 16568"""
388
389     def check_result(self):
390         super(FioJobTest_t0008, self).check_result()
391
392         if not self.passed:
393             return
394
395         ratio = self.json_data['jobs'][0]['write']['io_kbytes'] / 16568
396         logging.debug("Test %d: ratio: %f", self.testnum, ratio)
397
398         if ratio < 0.99 or ratio > 1.01:
399             self.failure_reason = "{0} bytes written mismatch,".format(self.failure_reason)
400             self.passed = False
401         if self.json_data['jobs'][0]['read']['io_kbytes'] != 32768:
402             self.failure_reason = "{0} bytes read mismatch,".format(self.failure_reason)
403             self.passed = False
404
405
406 class FioJobTest_t0009(FioJobTest):
407     """Test consists of fio test job t0009
408     Confirm that runtime >= 60s"""
409
410     def check_result(self):
411         super(FioJobTest_t0009, self).check_result()
412
413         if not self.passed:
414             return
415
416         logging.debug('Test %d: elapsed: %d', self.testnum, self.json_data['jobs'][0]['elapsed'])
417
418         if self.json_data['jobs'][0]['elapsed'] < 60:
419             self.failure_reason = "{0} elapsed time mismatch,".format(self.failure_reason)
420             self.passed = False
421
422
423 class FioJobTest_iops_rate(FioJobTest):
424     """Test consists of fio test job t0009
425     Confirm that job0 iops == 1000
426     and that job1_iops / job0_iops ~ 8
427     With two runs of fio-3.16 I observed a ratio of 8.3"""
428
429     def check_result(self):
430         super(FioJobTest_iops_rate, self).check_result()
431
432         if not self.passed:
433             return
434
435         iops1 = self.json_data['jobs'][0]['read']['iops']
436         iops2 = self.json_data['jobs'][1]['read']['iops']
437         ratio = iops2 / iops1
438         logging.debug("Test %d: iops1: %f", self.testnum, iops1)
439         logging.debug("Test %d: ratio: %f", self.testnum, ratio)
440
441         if iops1 < 950 or iops1 > 1050:
442             self.failure_reason = "{0} iops value mismatch,".format(self.failure_reason)
443             self.passed = False
444
445         if ratio < 7 or ratio > 9:
446             self.failure_reason = "{0} iops ratio mismatch,".format(self.failure_reason)
447             self.passed = False
448
449
450 class FioJobTest_t0013(FioJobTest):
451     """Runs fio test job t0013"""
452
453     def check_result(self):
454         super(FioJobTest_t0013, self).check_result()
455
456
457 class Requirements(object):
458     """Requirements consists of multiple run environment characteristics.
459     These are to determine if a particular test can be run"""
460
461     _linux = False
462     _libaio = False
463     _zbd = False
464     _root = False
465     _zoned_nullb = False
466     _not_macos = False
467     _not_windows = False
468     _unittests = False
469     _cpucount4 = False
470
471     def __init__(self, fio_root):
472         Requirements._not_macos = platform.system() != "Darwin"
473         Requirements._not_windows = platform.system() != "Windows"
474         Requirements._linux = platform.system() == "Linux"
475
476         if Requirements._linux:
477             config_file = os.path.join(fio_root, "config-host.h")
478             contents, success = FioJobTest.get_file(config_file)
479             if not success:
480                 print("Unable to open {0} to check requirements".format(config_file))
481                 Requirements._zbd = True
482             else:
483                 Requirements._zbd = "CONFIG_HAS_BLKZONED" in contents
484                 Requirements._libaio = "CONFIG_LIBAIO" in contents
485
486             Requirements._root = (os.geteuid() == 0)
487             if Requirements._zbd and Requirements._root:
488                 subprocess.run(["modprobe", "null_blk"],
489                                stdout=subprocess.PIPE,
490                                stderr=subprocess.PIPE)
491                 if os.path.exists("/sys/module/null_blk/parameters/zoned"):
492                     Requirements._zoned_nullb = True
493
494         if platform.system() == "Windows":
495             utest_exe = "unittest.exe"
496         else:
497             utest_exe = "unittest"
498         unittest_path = os.path.join(fio_root, "unittests", utest_exe)
499         Requirements._unittests = os.path.exists(unittest_path)
500
501         Requirements._cpucount4 = multiprocessing.cpu_count() >= 4
502
503         req_list = [Requirements.linux,
504                     Requirements.libaio,
505                     Requirements.zbd,
506                     Requirements.root,
507                     Requirements.zoned_nullb,
508                     Requirements.not_macos,
509                     Requirements.not_windows,
510                     Requirements.unittests,
511                     Requirements.cpucount4]
512         for req in req_list:
513             value, desc = req()
514             logging.debug("Requirements: Requirement '%s' met? %s", desc, value)
515
516     @classmethod
517     def linux(cls):
518         """Are we running on Linux?"""
519         return Requirements._linux, "Linux required"
520
521     @classmethod
522     def libaio(cls):
523         """Is libaio available?"""
524         return Requirements._libaio, "libaio required"
525
526     @classmethod
527     def zbd(cls):
528         """Is ZBD support available?"""
529         return Requirements._zbd, "Zoned block device support required"
530
531     @classmethod
532     def root(cls):
533         """Are we running as root?"""
534         return Requirements._root, "root required"
535
536     @classmethod
537     def zoned_nullb(cls):
538         """Are zoned null block devices available?"""
539         return Requirements._zoned_nullb, "Zoned null block device support required"
540
541     @classmethod
542     def not_macos(cls):
543         """Are we running on a platform other than macOS?"""
544         return Requirements._not_macos, "platform other than macOS required"
545
546     @classmethod
547     def not_windows(cls):
548         """Are we running on a platform other than Windws?"""
549         return Requirements._not_windows, "platform other than Windows required"
550
551     @classmethod
552     def unittests(cls):
553         """Were unittests built?"""
554         return Requirements._unittests, "Unittests support required"
555
556     @classmethod
557     def cpucount4(cls):
558         """Do we have at least 4 CPUs?"""
559         return Requirements._cpucount4, "4+ CPUs required"
560
561
562 SUCCESS_DEFAULT = {
563     'zero_return': True,
564     'stderr_empty': True,
565     'timeout': 600,
566     }
567 SUCCESS_NONZERO = {
568     'zero_return': False,
569     'stderr_empty': False,
570     'timeout': 600,
571     }
572 SUCCESS_STDERR = {
573     'zero_return': True,
574     'stderr_empty': False,
575     'timeout': 600,
576     }
577 TEST_LIST = [
578     {
579         'test_id':          1,
580         'test_class':       FioJobTest,
581         'job':              't0001-52c58027.fio',
582         'success':          SUCCESS_DEFAULT,
583         'pre_job':          None,
584         'pre_success':      None,
585         'requirements':     [],
586     },
587     {
588         'test_id':          2,
589         'test_class':       FioJobTest,
590         'job':              't0002-13af05ae-post.fio',
591         'success':          SUCCESS_DEFAULT,
592         'pre_job':          't0002-13af05ae-pre.fio',
593         'pre_success':      None,
594         'requirements':     [Requirements.linux, Requirements.libaio],
595     },
596     {
597         'test_id':          3,
598         'test_class':       FioJobTest,
599         'job':              't0003-0ae2c6e1-post.fio',
600         'success':          SUCCESS_NONZERO,
601         'pre_job':          't0003-0ae2c6e1-pre.fio',
602         'pre_success':      SUCCESS_DEFAULT,
603         'requirements':     [Requirements.linux, Requirements.libaio],
604     },
605     {
606         'test_id':          4,
607         'test_class':       FioJobTest,
608         'job':              't0004-8a99fdf6.fio',
609         'success':          SUCCESS_DEFAULT,
610         'pre_job':          None,
611         'pre_success':      None,
612         'requirements':     [Requirements.linux, Requirements.libaio],
613     },
614     {
615         'test_id':          5,
616         'test_class':       FioJobTest_t0005,
617         'job':              't0005-f7078f7b.fio',
618         'success':          SUCCESS_DEFAULT,
619         'pre_job':          None,
620         'pre_success':      None,
621         'output_format':    'json',
622         'requirements':     [Requirements.not_windows],
623     },
624     {
625         'test_id':          6,
626         'test_class':       FioJobTest_t0006,
627         'job':              't0006-82af2a7c.fio',
628         'success':          SUCCESS_DEFAULT,
629         'pre_job':          None,
630         'pre_success':      None,
631         'output_format':    'json',
632         'requirements':     [Requirements.linux, Requirements.libaio],
633     },
634     {
635         'test_id':          7,
636         'test_class':       FioJobTest_t0007,
637         'job':              't0007-37cf9e3c.fio',
638         'success':          SUCCESS_DEFAULT,
639         'pre_job':          None,
640         'pre_success':      None,
641         'output_format':    'json',
642         'requirements':     [],
643     },
644     {
645         'test_id':          8,
646         'test_class':       FioJobTest_t0008,
647         'job':              't0008-ae2fafc8.fio',
648         'success':          SUCCESS_DEFAULT,
649         'pre_job':          None,
650         'pre_success':      None,
651         'output_format':    'json',
652         'requirements':     [],
653     },
654     {
655         'test_id':          9,
656         'test_class':       FioJobTest_t0009,
657         'job':              't0009-f8b0bd10.fio',
658         'success':          SUCCESS_DEFAULT,
659         'pre_job':          None,
660         'pre_success':      None,
661         'output_format':    'json',
662         'requirements':     [Requirements.not_macos,
663                              Requirements.cpucount4],
664         # mac os does not support CPU affinity
665     },
666     {
667         'test_id':          10,
668         'test_class':       FioJobTest,
669         'job':              't0010-b7aae4ba.fio',
670         'success':          SUCCESS_DEFAULT,
671         'pre_job':          None,
672         'pre_success':      None,
673         'requirements':     [],
674     },
675     {
676         'test_id':          11,
677         'test_class':       FioJobTest_iops_rate,
678         'job':              't0011-5d2788d5.fio',
679         'success':          SUCCESS_DEFAULT,
680         'pre_job':          None,
681         'pre_success':      None,
682         'output_format':    'json',
683         'requirements':     [],
684     },
685     {
686         'test_id':          12,
687         'test_class':       FioJobTest_iops_rate,
688         'job':              't0012.fio',
689         'success':          SUCCESS_DEFAULT,
690         'pre_job':          None,
691         'pre_success':      None,
692         'output_format':    'json',
693         'requirements':     [],
694         'requirements':     [Requirements.not_macos],
695         # mac os does not support CPU affinity
696     },
697     {
698         'test_id':          13,
699         'test_class':       FioJobTest_t0013,
700         'job':              't0013.fio',
701         'success':          SUCCESS_DEFAULT,
702         'pre_job':          None,
703         'pre_success':      None,
704         'output_format':    'json',
705         'requirements':     [],
706     },
707     {
708         'test_id':          1000,
709         'test_class':       FioExeTest,
710         'exe':              't/axmap',
711         'parameters':       None,
712         'success':          SUCCESS_DEFAULT,
713         'requirements':     [],
714     },
715     {
716         'test_id':          1001,
717         'test_class':       FioExeTest,
718         'exe':              't/ieee754',
719         'parameters':       None,
720         'success':          SUCCESS_DEFAULT,
721         'requirements':     [],
722     },
723     {
724         'test_id':          1002,
725         'test_class':       FioExeTest,
726         'exe':              't/lfsr-test',
727         'parameters':       ['0xFFFFFF', '0', '0', 'verify'],
728         'success':          SUCCESS_STDERR,
729         'requirements':     [],
730     },
731     {
732         'test_id':          1003,
733         'test_class':       FioExeTest,
734         'exe':              't/readonly.py',
735         'parameters':       ['-f', '{fio_path}'],
736         'success':          SUCCESS_DEFAULT,
737         'requirements':     [],
738     },
739     {
740         'test_id':          1004,
741         'test_class':       FioExeTest,
742         'exe':              't/steadystate_tests.py',
743         'parameters':       ['{fio_path}'],
744         'success':          SUCCESS_DEFAULT,
745         'requirements':     [],
746     },
747     {
748         'test_id':          1005,
749         'test_class':       FioExeTest,
750         'exe':              't/stest',
751         'parameters':       None,
752         'success':          SUCCESS_STDERR,
753         'requirements':     [],
754     },
755     {
756         'test_id':          1006,
757         'test_class':       FioExeTest,
758         'exe':              't/strided.py',
759         'parameters':       ['{fio_path}'],
760         'success':          SUCCESS_DEFAULT,
761         'requirements':     [],
762     },
763     {
764         'test_id':          1007,
765         'test_class':       FioExeTest,
766         'exe':              't/zbd/run-tests-against-regular-nullb',
767         'parameters':       None,
768         'success':          SUCCESS_DEFAULT,
769         'requirements':     [Requirements.linux, Requirements.zbd,
770                              Requirements.root],
771     },
772     {
773         'test_id':          1008,
774         'test_class':       FioExeTest,
775         'exe':              't/zbd/run-tests-against-zoned-nullb',
776         'parameters':       None,
777         'success':          SUCCESS_DEFAULT,
778         'requirements':     [Requirements.linux, Requirements.zbd,
779                              Requirements.root, Requirements.zoned_nullb],
780     },
781     {
782         'test_id':          1009,
783         'test_class':       FioExeTest,
784         'exe':              'unittests/unittest',
785         'parameters':       None,
786         'success':          SUCCESS_DEFAULT,
787         'requirements':     [Requirements.unittests],
788     },
789     {
790         'test_id':          1010,
791         'test_class':       FioExeTest,
792         'exe':              't/latency_percentiles.py',
793         'parameters':       ['-f', '{fio_path}'],
794         'success':          SUCCESS_DEFAULT,
795         'requirements':     [],
796     },
797     {
798         'test_id':          1011,
799         'test_class':       FioExeTest,
800         'exe':              't/jsonplus2csv_test.py',
801         'parameters':       ['-f', '{fio_path}'],
802         'success':          SUCCESS_DEFAULT,
803         'requirements':     [],
804     },
805 ]
806
807
808 def parse_args():
809     """Parse command-line arguments."""
810
811     parser = argparse.ArgumentParser()
812     parser.add_argument('-r', '--fio-root',
813                         help='fio root path')
814     parser.add_argument('-f', '--fio',
815                         help='path to fio executable (e.g., ./fio)')
816     parser.add_argument('-a', '--artifact-root',
817                         help='artifact root directory')
818     parser.add_argument('-s', '--skip', nargs='+', type=int,
819                         help='list of test(s) to skip')
820     parser.add_argument('-o', '--run-only', nargs='+', type=int,
821                         help='list of test(s) to run, skipping all others')
822     parser.add_argument('-d', '--debug', action='store_true',
823                         help='provide debug output')
824     parser.add_argument('-k', '--skip-req', action='store_true',
825                         help='skip requirements checking')
826     parser.add_argument('-p', '--pass-through', action='append',
827                         help='pass-through an argument to an executable test')
828     args = parser.parse_args()
829
830     return args
831
832
833 def main():
834     """Entry point."""
835
836     args = parse_args()
837     if args.debug:
838         logging.basicConfig(level=logging.DEBUG)
839     else:
840         logging.basicConfig(level=logging.INFO)
841
842     pass_through = {}
843     if args.pass_through:
844         for arg in args.pass_through:
845             if not ':' in arg:
846                 print("Invalid --pass-through argument '%s'" % arg)
847                 print("Syntax for --pass-through is TESTNUMBER:ARGUMENT")
848                 return
849             split = arg.split(":",1)
850             pass_through[int(split[0])] = split[1]
851         logging.debug("Pass-through arguments: %s" % pass_through)
852
853     if args.fio_root:
854         fio_root = args.fio_root
855     else:
856         fio_root = str(Path(__file__).absolute().parent.parent)
857     print("fio root is %s" % fio_root)
858
859     if args.fio:
860         fio_path = args.fio
861     else:
862         if platform.system() == "Windows":
863             fio_exe = "fio.exe"
864         else:
865             fio_exe = "fio"
866         fio_path = os.path.join(fio_root, fio_exe)
867     print("fio path is %s" % fio_path)
868     if not shutil.which(fio_path):
869         print("Warning: fio executable not found")
870
871     artifact_root = args.artifact_root if args.artifact_root else \
872         "fio-test-{0}".format(time.strftime("%Y%m%d-%H%M%S"))
873     os.mkdir(artifact_root)
874     print("Artifact directory is %s" % artifact_root)
875
876     if not args.skip_req:
877         req = Requirements(fio_root)
878
879     passed = 0
880     failed = 0
881     skipped = 0
882
883     for config in TEST_LIST:
884         if (args.skip and config['test_id'] in args.skip) or \
885            (args.run_only and config['test_id'] not in args.run_only):
886             skipped = skipped + 1
887             print("Test {0} SKIPPED (User request)".format(config['test_id']))
888             continue
889
890         if issubclass(config['test_class'], FioJobTest):
891             if config['pre_job']:
892                 fio_pre_job = os.path.join(fio_root, 't', 'jobs',
893                                            config['pre_job'])
894             else:
895                 fio_pre_job = None
896             if config['pre_success']:
897                 fio_pre_success = config['pre_success']
898             else:
899                 fio_pre_success = None
900             if 'output_format' in config:
901                 output_format = config['output_format']
902             else:
903                 output_format = 'normal'
904             test = config['test_class'](
905                 fio_path,
906                 os.path.join(fio_root, 't', 'jobs', config['job']),
907                 config['success'],
908                 fio_pre_job=fio_pre_job,
909                 fio_pre_success=fio_pre_success,
910                 output_format=output_format)
911         elif issubclass(config['test_class'], FioExeTest):
912             exe_path = os.path.join(fio_root, config['exe'])
913             if config['parameters']:
914                 parameters = [p.format(fio_path=fio_path) for p in config['parameters']]
915             else:
916                 parameters = []
917             if Path(exe_path).suffix == '.py' and platform.system() == "Windows":
918                 parameters.insert(0, exe_path)
919                 exe_path = "python.exe"
920             if config['test_id'] in pass_through:
921                 parameters += pass_through[config['test_id']].split()
922             test = config['test_class'](exe_path, parameters,
923                                         config['success'])
924         else:
925             print("Test {0} FAILED: unable to process test config".format(config['test_id']))
926             failed = failed + 1
927             continue
928
929         if not args.skip_req:
930             reqs_met = True
931             for req in config['requirements']:
932                 reqs_met, reason = req()
933                 logging.debug("Test %d: Requirement '%s' met? %s", config['test_id'], reason,
934                               reqs_met)
935                 if not reqs_met:
936                     break
937             if not reqs_met:
938                 print("Test {0} SKIPPED ({1})".format(config['test_id'], reason))
939                 skipped = skipped + 1
940                 continue
941
942         test.setup(artifact_root, config['test_id'])
943         test.run()
944         test.check_result()
945         if test.passed:
946             result = "PASSED"
947             passed = passed + 1
948         else:
949             result = "FAILED: {0}".format(test.failure_reason)
950             failed = failed + 1
951             contents, _ = FioJobTest.get_file(test.stderr_file)
952             logging.debug("Test %d: stderr:\n%s", config['test_id'], contents)
953             contents, _ = FioJobTest.get_file(test.stdout_file)
954             logging.debug("Test %d: stdout:\n%s", config['test_id'], contents)
955         print("Test {0} {1}".format(config['test_id'], result))
956
957     print("{0} test(s) passed, {1} failed, {2} skipped".format(passed, failed, skipped))
958
959     sys.exit(failed)
960
961
962 if __name__ == '__main__':
963     main()