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