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
0a602473 423class FioJobTest_iops_rate(FioJobTest):
df1eaa36
VF
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):
0a602473 430 super(FioJobTest_iops_rate, self).check_result()
df1eaa36
VF
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
0a9b8988 441 if iops1 < 950 or iops1 > 1050:
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,
0a602473 670 'test_class': FioJobTest_iops_rate,
704cc4df
VF
671 'job': 't0011-5d2788d5.fio',
672 'success': SUCCESS_DEFAULT,
673 'pre_job': None,
674 'pre_success': None,
675 'output_format': 'json',
676 'requirements': [],
677 },
0a602473
BVA
678 {
679 'test_id': 12,
680 'test_class': FioJobTest_iops_rate,
681 'job': 't0012.fio',
682 'success': SUCCESS_DEFAULT,
683 'pre_job': None,
684 'pre_success': None,
685 'output_format': 'json',
686 'requirements': [],
687 'requirements': [Requirements.not_macos],
688 # mac os does not support CPU affinity
689 },
704cc4df
VF
690 {
691 'test_id': 1000,
692 'test_class': FioExeTest,
693 'exe': 't/axmap',
694 'parameters': None,
695 'success': SUCCESS_DEFAULT,
696 'requirements': [],
697 },
698 {
699 'test_id': 1001,
700 'test_class': FioExeTest,
701 'exe': 't/ieee754',
702 'parameters': None,
703 'success': SUCCESS_DEFAULT,
704 'requirements': [],
705 },
706 {
707 'test_id': 1002,
708 'test_class': FioExeTest,
709 'exe': 't/lfsr-test',
710 'parameters': ['0xFFFFFF', '0', '0', 'verify'],
711 'success': SUCCESS_STDERR,
712 'requirements': [],
713 },
714 {
715 'test_id': 1003,
716 'test_class': FioExeTest,
717 'exe': 't/readonly.py',
718 'parameters': ['-f', '{fio_path}'],
719 'success': SUCCESS_DEFAULT,
720 'requirements': [],
721 },
722 {
723 'test_id': 1004,
724 'test_class': FioExeTest,
725 'exe': 't/steadystate_tests.py',
726 'parameters': ['{fio_path}'],
727 'success': SUCCESS_DEFAULT,
728 'requirements': [],
729 },
730 {
731 'test_id': 1005,
732 'test_class': FioExeTest,
733 'exe': 't/stest',
734 'parameters': None,
735 'success': SUCCESS_STDERR,
736 'requirements': [],
737 },
738 {
739 'test_id': 1006,
740 'test_class': FioExeTest,
741 'exe': 't/strided.py',
742 'parameters': ['{fio_path}'],
743 'success': SUCCESS_DEFAULT,
744 'requirements': [],
745 },
746 {
747 'test_id': 1007,
748 'test_class': FioExeTest,
749 'exe': 't/zbd/run-tests-against-regular-nullb',
750 'parameters': None,
751 'success': SUCCESS_DEFAULT,
752 'requirements': [Requirements.linux, Requirements.zbd,
753 Requirements.root],
754 },
755 {
756 'test_id': 1008,
757 'test_class': FioExeTest,
758 'exe': 't/zbd/run-tests-against-zoned-nullb',
759 'parameters': None,
760 'success': SUCCESS_DEFAULT,
761 'requirements': [Requirements.linux, Requirements.zbd,
762 Requirements.root, Requirements.zoned_nullb],
763 },
764 {
765 'test_id': 1009,
766 'test_class': FioExeTest,
767 'exe': 'unittests/unittest',
768 'parameters': None,
769 'success': SUCCESS_DEFAULT,
770 'requirements': [Requirements.unittests],
771 },
772 {
773 'test_id': 1010,
774 'test_class': FioExeTest,
775 'exe': 't/latency_percentiles.py',
776 'parameters': ['-f', '{fio_path}'],
777 'success': SUCCESS_DEFAULT,
778 'requirements': [],
779 },
8403eca6
VF
780 {
781 'test_id': 1011,
782 'test_class': FioExeTest,
783 'exe': 't/jsonplus2csv_test.py',
784 'parameters': ['-f', '{fio_path}'],
785 'success': SUCCESS_DEFAULT,
786 'requirements': [],
787 },
df1eaa36
VF
788]
789
790
791def parse_args():
704cc4df
VF
792 """Parse command-line arguments."""
793
df1eaa36
VF
794 parser = argparse.ArgumentParser()
795 parser.add_argument('-r', '--fio-root',
796 help='fio root path')
797 parser.add_argument('-f', '--fio',
798 help='path to fio executable (e.g., ./fio)')
799 parser.add_argument('-a', '--artifact-root',
800 help='artifact root directory')
801 parser.add_argument('-s', '--skip', nargs='+', type=int,
802 help='list of test(s) to skip')
803 parser.add_argument('-o', '--run-only', nargs='+', type=int,
804 help='list of test(s) to run, skipping all others')
6d5470c3
VF
805 parser.add_argument('-d', '--debug', action='store_true',
806 help='provide debug output')
b1bc705e
VF
807 parser.add_argument('-k', '--skip-req', action='store_true',
808 help='skip requirements checking')
58a77d2a
VF
809 parser.add_argument('-p', '--pass-through', action='append',
810 help='pass-through an argument to an executable test')
df1eaa36
VF
811 args = parser.parse_args()
812
813 return args
814
815
816def main():
704cc4df
VF
817 """Entry point."""
818
df1eaa36 819 args = parse_args()
6d5470c3
VF
820 if args.debug:
821 logging.basicConfig(level=logging.DEBUG)
822 else:
823 logging.basicConfig(level=logging.INFO)
824
58a77d2a
VF
825 pass_through = {}
826 if args.pass_through:
827 for arg in args.pass_through:
828 if not ':' in arg:
829 print("Invalid --pass-through argument '%s'" % arg)
830 print("Syntax for --pass-through is TESTNUMBER:ARGUMENT")
831 return
832 split = arg.split(":",1)
833 pass_through[int(split[0])] = split[1]
834 logging.debug("Pass-through arguments: %s" % pass_through)
835
df1eaa36
VF
836 if args.fio_root:
837 fio_root = args.fio_root
838 else:
6d5470c3
VF
839 fio_root = str(Path(__file__).absolute().parent.parent)
840 print("fio root is %s" % fio_root)
df1eaa36
VF
841
842 if args.fio:
843 fio_path = args.fio
844 else:
742b8799
VF
845 if platform.system() == "Windows":
846 fio_exe = "fio.exe"
847 else:
848 fio_exe = "fio"
849 fio_path = os.path.join(fio_root, fio_exe)
6d5470c3
VF
850 print("fio path is %s" % fio_path)
851 if not shutil.which(fio_path):
852 print("Warning: fio executable not found")
df1eaa36
VF
853
854 artifact_root = args.artifact_root if args.artifact_root else \
855 "fio-test-{0}".format(time.strftime("%Y%m%d-%H%M%S"))
856 os.mkdir(artifact_root)
857 print("Artifact directory is %s" % artifact_root)
858
b1bc705e
VF
859 if not args.skip_req:
860 req = Requirements(fio_root)
861
df1eaa36
VF
862 passed = 0
863 failed = 0
864 skipped = 0
865
866 for config in TEST_LIST:
867 if (args.skip and config['test_id'] in args.skip) or \
868 (args.run_only and config['test_id'] not in args.run_only):
869 skipped = skipped + 1
b1bc705e 870 print("Test {0} SKIPPED (User request)".format(config['test_id']))
df1eaa36
VF
871 continue
872
873 if issubclass(config['test_class'], FioJobTest):
874 if config['pre_job']:
875 fio_pre_job = os.path.join(fio_root, 't', 'jobs',
876 config['pre_job'])
877 else:
878 fio_pre_job = None
879 if config['pre_success']:
880 fio_pre_success = config['pre_success']
881 else:
882 fio_pre_success = None
883 if 'output_format' in config:
884 output_format = config['output_format']
885 else:
886 output_format = 'normal'
887 test = config['test_class'](
888 fio_path,
889 os.path.join(fio_root, 't', 'jobs', config['job']),
890 config['success'],
891 fio_pre_job=fio_pre_job,
892 fio_pre_success=fio_pre_success,
893 output_format=output_format)
894 elif issubclass(config['test_class'], FioExeTest):
895 exe_path = os.path.join(fio_root, config['exe'])
896 if config['parameters']:
897 parameters = [p.format(fio_path=fio_path) for p in config['parameters']]
898 else:
58a77d2a 899 parameters = []
742b8799 900 if Path(exe_path).suffix == '.py' and platform.system() == "Windows":
58a77d2a 901 parameters.insert(0, exe_path)
742b8799 902 exe_path = "python.exe"
58a77d2a
VF
903 if config['test_id'] in pass_through:
904 parameters += pass_through[config['test_id']].split()
df1eaa36
VF
905 test = config['test_class'](exe_path, parameters,
906 config['success'])
907 else:
908 print("Test {0} FAILED: unable to process test config".format(config['test_id']))
909 failed = failed + 1
910 continue
911
b1bc705e 912 if not args.skip_req:
704cc4df 913 reqs_met = True
b1bc705e 914 for req in config['requirements']:
704cc4df
VF
915 reqs_met, reason = req()
916 logging.debug("Test %d: Requirement '%s' met? %s", config['test_id'], reason,
917 reqs_met)
918 if not reqs_met:
b1bc705e 919 break
704cc4df 920 if not reqs_met:
b1bc705e
VF
921 print("Test {0} SKIPPED ({1})".format(config['test_id'], reason))
922 skipped = skipped + 1
923 continue
924
df1eaa36
VF
925 test.setup(artifact_root, config['test_id'])
926 test.run()
927 test.check_result()
928 if test.passed:
929 result = "PASSED"
930 passed = passed + 1
931 else:
932 result = "FAILED: {0}".format(test.failure_reason)
933 failed = failed + 1
15a73987
VF
934 contents, _ = FioJobTest.get_file(test.stderr_file)
935 logging.debug("Test %d: stderr:\n%s", config['test_id'], contents)
936 contents, _ = FioJobTest.get_file(test.stdout_file)
937 logging.debug("Test %d: stdout:\n%s", config['test_id'], contents)
df1eaa36
VF
938 print("Test {0} {1}".format(config['test_id'], result))
939
940 print("{0} test(s) passed, {1} failed, {2} skipped".format(passed, failed, skipped))
941
942 sys.exit(failed)
943
944
945if __name__ == '__main__':
946 main()