Merge branch 'master' 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
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': 600,
514         }
515 SUCCESS_NONZERO = {
516         'zero_return': False,
517         'stderr_empty': False,
518         'timeout': 600,
519         }
520 SUCCESS_STDERR = {
521         'zero_return': True,
522         'stderr_empty': False,
523         'timeout': 600,
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             'test_id':          1010,
717             'test_class':       FioExeTest,
718             'exe':              't/latency_percentiles.py',
719             'parameters':       ['-f', '{fio_path}'],
720             'success':          SUCCESS_DEFAULT,
721             'requirements':     [],
722         },
723 ]
724
725
726 def parse_args():
727     parser = argparse.ArgumentParser()
728     parser.add_argument('-r', '--fio-root',
729                         help='fio root path')
730     parser.add_argument('-f', '--fio',
731                         help='path to fio executable (e.g., ./fio)')
732     parser.add_argument('-a', '--artifact-root',
733                         help='artifact root directory')
734     parser.add_argument('-s', '--skip', nargs='+', type=int,
735                         help='list of test(s) to skip')
736     parser.add_argument('-o', '--run-only', nargs='+', type=int,
737                         help='list of test(s) to run, skipping all others')
738     parser.add_argument('-d', '--debug', action='store_true',
739                         help='provide debug output')
740     parser.add_argument('-k', '--skip-req', action='store_true',
741                         help='skip requirements checking')
742     args = parser.parse_args()
743
744     return args
745
746
747 def main():
748     args = parse_args()
749     if args.debug:
750         logging.basicConfig(level=logging.DEBUG)
751     else:
752         logging.basicConfig(level=logging.INFO)
753
754     if args.fio_root:
755         fio_root = args.fio_root
756     else:
757         fio_root = str(Path(__file__).absolute().parent.parent)
758     print("fio root is %s" % fio_root)
759
760     if args.fio:
761         fio_path = args.fio
762     else:
763         if platform.system() == "Windows":
764             fio_exe = "fio.exe"
765         else:
766             fio_exe = "fio"
767         fio_path = os.path.join(fio_root, fio_exe)
768     print("fio path is %s" % fio_path)
769     if not shutil.which(fio_path):
770         print("Warning: fio executable not found")
771
772     artifact_root = args.artifact_root if args.artifact_root else \
773         "fio-test-{0}".format(time.strftime("%Y%m%d-%H%M%S"))
774     os.mkdir(artifact_root)
775     print("Artifact directory is %s" % artifact_root)
776
777     if not args.skip_req:
778         req = Requirements(fio_root)
779
780     passed = 0
781     failed = 0
782     skipped = 0
783
784     for config in TEST_LIST:
785         if (args.skip and config['test_id'] in args.skip) or \
786            (args.run_only and config['test_id'] not in args.run_only):
787             skipped = skipped + 1
788             print("Test {0} SKIPPED (User request)".format(config['test_id']))
789             continue
790
791         if issubclass(config['test_class'], FioJobTest):
792             if config['pre_job']:
793                 fio_pre_job = os.path.join(fio_root, 't', 'jobs',
794                                            config['pre_job'])
795             else:
796                 fio_pre_job = None
797             if config['pre_success']:
798                 fio_pre_success = config['pre_success']
799             else:
800                 fio_pre_success = None
801             if 'output_format' in config:
802                 output_format = config['output_format']
803             else:
804                 output_format = 'normal'
805             test = config['test_class'](
806                 fio_path,
807                 os.path.join(fio_root, 't', 'jobs', config['job']),
808                 config['success'],
809                 fio_pre_job=fio_pre_job,
810                 fio_pre_success=fio_pre_success,
811                 output_format=output_format)
812         elif issubclass(config['test_class'], FioExeTest):
813             exe_path = os.path.join(fio_root, config['exe'])
814             if config['parameters']:
815                 parameters = [p.format(fio_path=fio_path) for p in config['parameters']]
816             else:
817                 parameters = None
818             if Path(exe_path).suffix == '.py' and platform.system() == "Windows":
819                 if parameters:
820                     parameters.insert(0, exe_path)
821                 else:
822                     parameters = [exe_path]
823                 exe_path = "python.exe"
824             test = config['test_class'](exe_path, parameters,
825                                         config['success'])
826         else:
827             print("Test {0} FAILED: unable to process test config".format(config['test_id']))
828             failed = failed + 1
829             continue
830
831         if not args.skip_req:
832             skip = False
833             for req in config['requirements']:
834                 ok, reason = req()
835                 skip = not ok
836                 logging.debug("Test %d: Requirement '%s' met? %s" % (config['test_id'], reason, ok))
837                 if skip:
838                     break
839             if skip:
840                 print("Test {0} SKIPPED ({1})".format(config['test_id'], reason))
841                 skipped = skipped + 1
842                 continue
843
844         test.setup(artifact_root, config['test_id'])
845         test.run()
846         test.check_result()
847         if test.passed:
848             result = "PASSED"
849             passed = passed + 1
850         else:
851             result = "FAILED: {0}".format(test.failure_reason)
852             failed = failed + 1
853             with open(test.stderr_file, "r") as stderr_file:
854                 logging.debug("Test %d: stderr:\n%s" % (config['test_id'], stderr_file.read()))
855             with open(test.stdout_file, "r") as stdout_file:
856                 logging.debug("Test %d: stdout:\n%s" % (config['test_id'], stdout_file.read()))
857         print("Test {0} {1}".format(config['test_id'], result))
858
859     print("{0} test(s) passed, {1} failed, {2} skipped".format(passed, failed, skipped))
860
861     sys.exit(failed)
862
863
864 if __name__ == '__main__':
865     main()