gfio: add high/low priority latency results
[fio.git] / t / run-fio-tests.py
CommitLineData
df1eaa36
VF
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
b048455f 17# # git clone git://git.kernel.dk/fio.git
df1eaa36
VF
18# # cd fio
19# # make -j
20# # python3 t/run-fio-tests.py
21#
22#
23# REQUIREMENTS
b1bc705e 24# - Python 3.5 (subprocess.run)
df1eaa36
VF
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)
df1eaa36
VF
42#
43
44import os
45import sys
46import json
47import time
6d5470c3 48import shutil
df1eaa36
VF
49import logging
50import argparse
b1bc705e 51import platform
df1eaa36 52import subprocess
b1bc705e 53import multiprocessing
df1eaa36
VF
54from pathlib import Path
55
56
57class 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
98class 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:
b048455f 128 proc = None
df1eaa36
VF
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))
285622dc 140 logging.debug("Test %d: return code: %d" % (self.testnum, proc.returncode))
df1eaa36
VF
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:
b048455f
VF
148 if proc:
149 if not proc.poll():
150 proc.terminate()
151 proc.communicate()
df1eaa36
VF
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
6d5470c3 182 stderr_size = os.path.getsize(self.stderr_file)
df1eaa36 183 if 'stderr_empty' in self.success:
df1eaa36
VF
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
194class 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:
285622dc 257 logging.debug("Test %d: precondition step failed" % self.testnum)
df1eaa36
VF
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
6d5470c3
VF
267 if not self.passed:
268 return
269
742b8799
VF
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:])
df1eaa36 289 try:
742b8799
VF
290 self.json_data = json.loads(file_data)
291 except json.JSONDecodeError:
292 continue
6d5470c3 293 else:
285622dc 294 logging.debug("Test %d: skipped %d lines decoding JSON data" % (self.testnum, i))
742b8799
VF
295 return
296
297 self.failure_reason = "{0} unable to decode JSON data,".format(self.failure_reason)
298 self.passed = False
df1eaa36
VF
299
300
301class 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
319class 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']
285622dc 331 logging.debug("Test %d: ratio: %f" % (self.testnum, ratio))
df1eaa36
VF
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
337class 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
352class 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
285622dc 367 logging.debug("Test %d: ratio: %f" % (self.testnum, ratio))
df1eaa36
VF
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
377class 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
285622dc 387 logging.debug('Test %d: elapsed: %d' % (self.testnum, self.json_data['jobs'][0]['elapsed']))
df1eaa36
VF
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
394class 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
41ceb6c7 409 logging.debug("Test %d: iops1: %f" % (self.testnum, iops1))
285622dc 410 logging.debug("Test %d: ratio: %f" % (self.testnum, ratio))
df1eaa36 411
41ceb6c7 412 if iops1 < 998 or iops1 > 1002:
df1eaa36
VF
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
b1bc705e
VF
421class 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
c58b33b4 431 _not_windows = False
b1bc705e
VF
432 _unittests = False
433 _cpucount4 = False
434
435 def __init__(self, fio_root):
436 Requirements._not_macos = platform.system() != "Darwin"
c58b33b4 437 Requirements._not_windows = platform.system() != "Windows"
b1bc705e
VF
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
742b8799
VF
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)
b1bc705e
VF
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,
c58b33b4 475 Requirements.not_windows,
b1bc705e
VF
476 Requirements.unittests,
477 Requirements.cpucount4]
478 for req in req_list:
479 value, desc = req()
285622dc 480 logging.debug("Requirements: Requirement '%s' met? %s" % (desc, value))
b1bc705e
VF
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
c58b33b4
VF
500 def not_windows():
501 return Requirements._not_windows, "platform other than Windows required"
502
b1bc705e
VF
503 def unittests():
504 return Requirements._unittests, "Unittests support required"
505
506 def cpucount4():
507 return Requirements._cpucount4, "4+ CPUs required"
508
509
df1eaa36
VF
510SUCCESS_DEFAULT = {
511 'zero_return': True,
512 'stderr_empty': True,
36c3a187 513 'timeout': 600,
df1eaa36
VF
514 }
515SUCCESS_NONZERO = {
516 'zero_return': False,
517 'stderr_empty': False,
36c3a187 518 'timeout': 600,
df1eaa36
VF
519 }
520SUCCESS_STDERR = {
521 'zero_return': True,
522 'stderr_empty': False,
36c3a187 523 'timeout': 600,
df1eaa36
VF
524 }
525TEST_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,
b1bc705e 533 'requirements': [],
df1eaa36
VF
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,
b1bc705e 542 'requirements': [Requirements.linux, Requirements.libaio],
df1eaa36
VF
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,
b1bc705e 551 'requirements': [Requirements.linux, Requirements.libaio],
df1eaa36
VF
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,
b1bc705e 560 'requirements': [Requirements.linux, Requirements.libaio],
df1eaa36
VF
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',
c58b33b4 570 'requirements': [Requirements.not_windows],
df1eaa36
VF
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',
b1bc705e 580 'requirements': [Requirements.linux, Requirements.libaio],
df1eaa36
VF
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',
b1bc705e 590 'requirements': [],
df1eaa36
VF
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',
b1bc705e 600 'requirements': [],
df1eaa36
VF
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',
b1bc705e
VF
610 'requirements': [Requirements.not_macos,
611 Requirements.cpucount4],
612 # mac os does not support CPU affinity
df1eaa36
VF
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,
b1bc705e 621 'requirements': [],
df1eaa36
VF
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',
b1bc705e 631 'requirements': [],
df1eaa36
VF
632 },
633 {
634 'test_id': 1000,
635 'test_class': FioExeTest,
636 'exe': 't/axmap',
637 'parameters': None,
638 'success': SUCCESS_DEFAULT,
b1bc705e 639 'requirements': [],
df1eaa36
VF
640 },
641 {
642 'test_id': 1001,
643 'test_class': FioExeTest,
644 'exe': 't/ieee754',
645 'parameters': None,
646 'success': SUCCESS_DEFAULT,
b1bc705e 647 'requirements': [],
df1eaa36
VF
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,
b1bc705e 655 'requirements': [],
df1eaa36
VF
656 },
657 {
658 'test_id': 1003,
659 'test_class': FioExeTest,
660 'exe': 't/readonly.py',
661 'parameters': ['-f', '{fio_path}'],
662 'success': SUCCESS_DEFAULT,
b1bc705e 663 'requirements': [],
df1eaa36
VF
664 },
665 {
666 'test_id': 1004,
667 'test_class': FioExeTest,
668 'exe': 't/steadystate_tests.py',
669 'parameters': ['{fio_path}'],
670 'success': SUCCESS_DEFAULT,
b1bc705e 671 'requirements': [],
df1eaa36
VF
672 },
673 {
674 'test_id': 1005,
675 'test_class': FioExeTest,
676 'exe': 't/stest',
677 'parameters': None,
678 'success': SUCCESS_STDERR,
b1bc705e 679 'requirements': [],
df1eaa36
VF
680 },
681 {
682 'test_id': 1006,
683 'test_class': FioExeTest,
684 'exe': 't/strided.py',
685 'parameters': ['{fio_path}'],
686 'success': SUCCESS_DEFAULT,
b1bc705e 687 'requirements': [],
df1eaa36
VF
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,
b1bc705e
VF
695 'requirements': [Requirements.linux, Requirements.zbd,
696 Requirements.root],
df1eaa36
VF
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,
b1bc705e
VF
704 'requirements': [Requirements.linux, Requirements.zbd,
705 Requirements.root, Requirements.zoned_nullb],
df1eaa36
VF
706 },
707 {
708 'test_id': 1009,
709 'test_class': FioExeTest,
710 'exe': 'unittests/unittest',
711 'parameters': None,
712 'success': SUCCESS_DEFAULT,
b1bc705e 713 'requirements': [Requirements.unittests],
df1eaa36 714 },
7fd8e3e3
VF
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 },
df1eaa36
VF
723]
724
725
726def 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')
6d5470c3
VF
738 parser.add_argument('-d', '--debug', action='store_true',
739 help='provide debug output')
b1bc705e
VF
740 parser.add_argument('-k', '--skip-req', action='store_true',
741 help='skip requirements checking')
df1eaa36
VF
742 args = parser.parse_args()
743
744 return args
745
746
747def main():
df1eaa36 748 args = parse_args()
6d5470c3
VF
749 if args.debug:
750 logging.basicConfig(level=logging.DEBUG)
751 else:
752 logging.basicConfig(level=logging.INFO)
753
df1eaa36
VF
754 if args.fio_root:
755 fio_root = args.fio_root
756 else:
6d5470c3
VF
757 fio_root = str(Path(__file__).absolute().parent.parent)
758 print("fio root is %s" % fio_root)
df1eaa36
VF
759
760 if args.fio:
761 fio_path = args.fio
762 else:
742b8799
VF
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)
6d5470c3
VF
768 print("fio path is %s" % fio_path)
769 if not shutil.which(fio_path):
770 print("Warning: fio executable not found")
df1eaa36
VF
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
b1bc705e
VF
777 if not args.skip_req:
778 req = Requirements(fio_root)
779
df1eaa36
VF
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
b1bc705e 788 print("Test {0} SKIPPED (User request)".format(config['test_id']))
df1eaa36
VF
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
742b8799
VF
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"
df1eaa36
VF
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
b1bc705e
VF
831 if not args.skip_req:
832 skip = False
833 for req in config['requirements']:
834 ok, reason = req()
835 skip = not ok
285622dc 836 logging.debug("Test %d: Requirement '%s' met? %s" % (config['test_id'], reason, ok))
b1bc705e
VF
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
df1eaa36
VF
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
6d5470c3 853 with open(test.stderr_file, "r") as stderr_file:
285622dc 854 logging.debug("Test %d: stderr:\n%s" % (config['test_id'], stderr_file.read()))
6d5470c3 855 with open(test.stdout_file, "r") as stdout_file:
285622dc 856 logging.debug("Test %d: stdout:\n%s" % (config['test_id'], stdout_file.read()))
df1eaa36
VF
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
864if __name__ == '__main__':
865 main()