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