t/run-fio-tests: catch modprobe exceptions
[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
f0c7ae7a
BVA
450class FioJobTest_t0013(FioJobTest):
451 """Runs fio test job t0013"""
452
453 def check_result(self):
454 super(FioJobTest_t0013, self).check_result()
455
456
b1bc705e
VF
457class Requirements(object):
458 """Requirements consists of multiple run environment characteristics.
459 These are to determine if a particular test can be run"""
460
461 _linux = False
462 _libaio = False
463 _zbd = False
464 _root = False
465 _zoned_nullb = False
466 _not_macos = False
c58b33b4 467 _not_windows = False
b1bc705e
VF
468 _unittests = False
469 _cpucount4 = False
470
471 def __init__(self, fio_root):
472 Requirements._not_macos = platform.system() != "Darwin"
c58b33b4 473 Requirements._not_windows = platform.system() != "Windows"
b1bc705e
VF
474 Requirements._linux = platform.system() == "Linux"
475
476 if Requirements._linux:
15a73987
VF
477 config_file = os.path.join(fio_root, "config-host.h")
478 contents, success = FioJobTest.get_file(config_file)
479 if not success:
b1bc705e
VF
480 print("Unable to open {0} to check requirements".format(config_file))
481 Requirements._zbd = True
482 else:
b7694961 483 Requirements._zbd = "CONFIG_HAS_BLKZONED" in contents
b1bc705e
VF
484 Requirements._libaio = "CONFIG_LIBAIO" in contents
485
486 Requirements._root = (os.geteuid() == 0)
487 if Requirements._zbd and Requirements._root:
8854e368
VF
488 try:
489 subprocess.run(["modprobe", "null_blk"],
490 stdout=subprocess.PIPE,
491 stderr=subprocess.PIPE)
492 if os.path.exists("/sys/module/null_blk/parameters/zoned"):
493 Requirements._zoned_nullb = True
494 except Exception:
495 pass
b1bc705e 496
742b8799
VF
497 if platform.system() == "Windows":
498 utest_exe = "unittest.exe"
499 else:
500 utest_exe = "unittest"
501 unittest_path = os.path.join(fio_root, "unittests", utest_exe)
b1bc705e
VF
502 Requirements._unittests = os.path.exists(unittest_path)
503
504 Requirements._cpucount4 = multiprocessing.cpu_count() >= 4
505
506 req_list = [Requirements.linux,
507 Requirements.libaio,
508 Requirements.zbd,
509 Requirements.root,
510 Requirements.zoned_nullb,
511 Requirements.not_macos,
c58b33b4 512 Requirements.not_windows,
b1bc705e
VF
513 Requirements.unittests,
514 Requirements.cpucount4]
515 for req in req_list:
516 value, desc = req()
704cc4df 517 logging.debug("Requirements: Requirement '%s' met? %s", desc, value)
b1bc705e 518
704cc4df
VF
519 @classmethod
520 def linux(cls):
521 """Are we running on Linux?"""
b1bc705e
VF
522 return Requirements._linux, "Linux required"
523
704cc4df
VF
524 @classmethod
525 def libaio(cls):
526 """Is libaio available?"""
b1bc705e
VF
527 return Requirements._libaio, "libaio required"
528
704cc4df
VF
529 @classmethod
530 def zbd(cls):
531 """Is ZBD support available?"""
b1bc705e
VF
532 return Requirements._zbd, "Zoned block device support required"
533
704cc4df
VF
534 @classmethod
535 def root(cls):
536 """Are we running as root?"""
b1bc705e
VF
537 return Requirements._root, "root required"
538
704cc4df
VF
539 @classmethod
540 def zoned_nullb(cls):
541 """Are zoned null block devices available?"""
b1bc705e
VF
542 return Requirements._zoned_nullb, "Zoned null block device support required"
543
704cc4df
VF
544 @classmethod
545 def not_macos(cls):
546 """Are we running on a platform other than macOS?"""
b1bc705e
VF
547 return Requirements._not_macos, "platform other than macOS required"
548
704cc4df
VF
549 @classmethod
550 def not_windows(cls):
551 """Are we running on a platform other than Windws?"""
c58b33b4
VF
552 return Requirements._not_windows, "platform other than Windows required"
553
704cc4df
VF
554 @classmethod
555 def unittests(cls):
556 """Were unittests built?"""
b1bc705e
VF
557 return Requirements._unittests, "Unittests support required"
558
704cc4df
VF
559 @classmethod
560 def cpucount4(cls):
561 """Do we have at least 4 CPUs?"""
b1bc705e
VF
562 return Requirements._cpucount4, "4+ CPUs required"
563
564
df1eaa36 565SUCCESS_DEFAULT = {
704cc4df
VF
566 'zero_return': True,
567 'stderr_empty': True,
568 'timeout': 600,
569 }
df1eaa36 570SUCCESS_NONZERO = {
704cc4df
VF
571 'zero_return': False,
572 'stderr_empty': False,
573 'timeout': 600,
574 }
df1eaa36 575SUCCESS_STDERR = {
704cc4df
VF
576 'zero_return': True,
577 'stderr_empty': False,
578 'timeout': 600,
579 }
df1eaa36 580TEST_LIST = [
704cc4df
VF
581 {
582 'test_id': 1,
583 'test_class': FioJobTest,
584 'job': 't0001-52c58027.fio',
585 'success': SUCCESS_DEFAULT,
586 'pre_job': None,
587 'pre_success': None,
588 'requirements': [],
589 },
590 {
591 'test_id': 2,
592 'test_class': FioJobTest,
593 'job': 't0002-13af05ae-post.fio',
594 'success': SUCCESS_DEFAULT,
595 'pre_job': 't0002-13af05ae-pre.fio',
596 'pre_success': None,
597 'requirements': [Requirements.linux, Requirements.libaio],
598 },
599 {
600 'test_id': 3,
601 'test_class': FioJobTest,
602 'job': 't0003-0ae2c6e1-post.fio',
603 'success': SUCCESS_NONZERO,
604 'pre_job': 't0003-0ae2c6e1-pre.fio',
605 'pre_success': SUCCESS_DEFAULT,
606 'requirements': [Requirements.linux, Requirements.libaio],
607 },
608 {
609 'test_id': 4,
610 'test_class': FioJobTest,
611 'job': 't0004-8a99fdf6.fio',
612 'success': SUCCESS_DEFAULT,
613 'pre_job': None,
614 'pre_success': None,
615 'requirements': [Requirements.linux, Requirements.libaio],
616 },
617 {
618 'test_id': 5,
619 'test_class': FioJobTest_t0005,
620 'job': 't0005-f7078f7b.fio',
621 'success': SUCCESS_DEFAULT,
622 'pre_job': None,
623 'pre_success': None,
624 'output_format': 'json',
625 'requirements': [Requirements.not_windows],
626 },
627 {
628 'test_id': 6,
629 'test_class': FioJobTest_t0006,
630 'job': 't0006-82af2a7c.fio',
631 'success': SUCCESS_DEFAULT,
632 'pre_job': None,
633 'pre_success': None,
634 'output_format': 'json',
635 'requirements': [Requirements.linux, Requirements.libaio],
636 },
637 {
638 'test_id': 7,
639 'test_class': FioJobTest_t0007,
640 'job': 't0007-37cf9e3c.fio',
641 'success': SUCCESS_DEFAULT,
642 'pre_job': None,
643 'pre_success': None,
644 'output_format': 'json',
645 'requirements': [],
646 },
647 {
648 'test_id': 8,
649 'test_class': FioJobTest_t0008,
650 'job': 't0008-ae2fafc8.fio',
651 'success': SUCCESS_DEFAULT,
652 'pre_job': None,
653 'pre_success': None,
654 'output_format': 'json',
655 'requirements': [],
656 },
657 {
658 'test_id': 9,
659 'test_class': FioJobTest_t0009,
660 'job': 't0009-f8b0bd10.fio',
661 'success': SUCCESS_DEFAULT,
662 'pre_job': None,
663 'pre_success': None,
664 'output_format': 'json',
665 'requirements': [Requirements.not_macos,
666 Requirements.cpucount4],
667 # mac os does not support CPU affinity
668 },
669 {
670 'test_id': 10,
671 'test_class': FioJobTest,
672 'job': 't0010-b7aae4ba.fio',
673 'success': SUCCESS_DEFAULT,
674 'pre_job': None,
675 'pre_success': None,
676 'requirements': [],
677 },
678 {
679 'test_id': 11,
0a602473 680 'test_class': FioJobTest_iops_rate,
704cc4df
VF
681 'job': 't0011-5d2788d5.fio',
682 'success': SUCCESS_DEFAULT,
683 'pre_job': None,
684 'pre_success': None,
685 'output_format': 'json',
686 'requirements': [],
687 },
0a602473
BVA
688 {
689 'test_id': 12,
690 'test_class': FioJobTest_iops_rate,
691 'job': 't0012.fio',
692 'success': SUCCESS_DEFAULT,
693 'pre_job': None,
694 'pre_success': None,
695 'output_format': 'json',
696 'requirements': [],
697 'requirements': [Requirements.not_macos],
698 # mac os does not support CPU affinity
699 },
f0c7ae7a
BVA
700 {
701 'test_id': 13,
702 'test_class': FioJobTest_t0013,
703 'job': 't0013.fio',
704 'success': SUCCESS_DEFAULT,
705 'pre_job': None,
706 'pre_success': None,
707 'output_format': 'json',
708 'requirements': [],
709 },
704cc4df
VF
710 {
711 'test_id': 1000,
712 'test_class': FioExeTest,
713 'exe': 't/axmap',
714 'parameters': None,
715 'success': SUCCESS_DEFAULT,
716 'requirements': [],
717 },
718 {
719 'test_id': 1001,
720 'test_class': FioExeTest,
721 'exe': 't/ieee754',
722 'parameters': None,
723 'success': SUCCESS_DEFAULT,
724 'requirements': [],
725 },
726 {
727 'test_id': 1002,
728 'test_class': FioExeTest,
729 'exe': 't/lfsr-test',
730 'parameters': ['0xFFFFFF', '0', '0', 'verify'],
731 'success': SUCCESS_STDERR,
732 'requirements': [],
733 },
734 {
735 'test_id': 1003,
736 'test_class': FioExeTest,
737 'exe': 't/readonly.py',
738 'parameters': ['-f', '{fio_path}'],
739 'success': SUCCESS_DEFAULT,
740 'requirements': [],
741 },
742 {
743 'test_id': 1004,
744 'test_class': FioExeTest,
745 'exe': 't/steadystate_tests.py',
746 'parameters': ['{fio_path}'],
747 'success': SUCCESS_DEFAULT,
748 'requirements': [],
749 },
750 {
751 'test_id': 1005,
752 'test_class': FioExeTest,
753 'exe': 't/stest',
754 'parameters': None,
755 'success': SUCCESS_STDERR,
756 'requirements': [],
757 },
758 {
759 'test_id': 1006,
760 'test_class': FioExeTest,
761 'exe': 't/strided.py',
762 'parameters': ['{fio_path}'],
763 'success': SUCCESS_DEFAULT,
764 'requirements': [],
765 },
766 {
767 'test_id': 1007,
768 'test_class': FioExeTest,
769 'exe': 't/zbd/run-tests-against-regular-nullb',
770 'parameters': None,
771 'success': SUCCESS_DEFAULT,
772 'requirements': [Requirements.linux, Requirements.zbd,
773 Requirements.root],
774 },
775 {
776 'test_id': 1008,
777 'test_class': FioExeTest,
778 'exe': 't/zbd/run-tests-against-zoned-nullb',
779 'parameters': None,
780 'success': SUCCESS_DEFAULT,
781 'requirements': [Requirements.linux, Requirements.zbd,
782 Requirements.root, Requirements.zoned_nullb],
783 },
784 {
785 'test_id': 1009,
786 'test_class': FioExeTest,
787 'exe': 'unittests/unittest',
788 'parameters': None,
789 'success': SUCCESS_DEFAULT,
790 'requirements': [Requirements.unittests],
791 },
792 {
793 'test_id': 1010,
794 'test_class': FioExeTest,
795 'exe': 't/latency_percentiles.py',
796 'parameters': ['-f', '{fio_path}'],
797 'success': SUCCESS_DEFAULT,
798 'requirements': [],
799 },
8403eca6
VF
800 {
801 'test_id': 1011,
802 'test_class': FioExeTest,
803 'exe': 't/jsonplus2csv_test.py',
804 'parameters': ['-f', '{fio_path}'],
805 'success': SUCCESS_DEFAULT,
806 'requirements': [],
807 },
df1eaa36
VF
808]
809
810
811def parse_args():
704cc4df
VF
812 """Parse command-line arguments."""
813
df1eaa36
VF
814 parser = argparse.ArgumentParser()
815 parser.add_argument('-r', '--fio-root',
816 help='fio root path')
817 parser.add_argument('-f', '--fio',
818 help='path to fio executable (e.g., ./fio)')
819 parser.add_argument('-a', '--artifact-root',
820 help='artifact root directory')
821 parser.add_argument('-s', '--skip', nargs='+', type=int,
822 help='list of test(s) to skip')
823 parser.add_argument('-o', '--run-only', nargs='+', type=int,
824 help='list of test(s) to run, skipping all others')
6d5470c3
VF
825 parser.add_argument('-d', '--debug', action='store_true',
826 help='provide debug output')
b1bc705e
VF
827 parser.add_argument('-k', '--skip-req', action='store_true',
828 help='skip requirements checking')
58a77d2a
VF
829 parser.add_argument('-p', '--pass-through', action='append',
830 help='pass-through an argument to an executable test')
df1eaa36
VF
831 args = parser.parse_args()
832
833 return args
834
835
836def main():
704cc4df
VF
837 """Entry point."""
838
df1eaa36 839 args = parse_args()
6d5470c3
VF
840 if args.debug:
841 logging.basicConfig(level=logging.DEBUG)
842 else:
843 logging.basicConfig(level=logging.INFO)
844
58a77d2a
VF
845 pass_through = {}
846 if args.pass_through:
847 for arg in args.pass_through:
848 if not ':' in arg:
849 print("Invalid --pass-through argument '%s'" % arg)
850 print("Syntax for --pass-through is TESTNUMBER:ARGUMENT")
851 return
852 split = arg.split(":",1)
853 pass_through[int(split[0])] = split[1]
854 logging.debug("Pass-through arguments: %s" % pass_through)
855
df1eaa36
VF
856 if args.fio_root:
857 fio_root = args.fio_root
858 else:
6d5470c3
VF
859 fio_root = str(Path(__file__).absolute().parent.parent)
860 print("fio root is %s" % fio_root)
df1eaa36
VF
861
862 if args.fio:
863 fio_path = args.fio
864 else:
742b8799
VF
865 if platform.system() == "Windows":
866 fio_exe = "fio.exe"
867 else:
868 fio_exe = "fio"
869 fio_path = os.path.join(fio_root, fio_exe)
6d5470c3
VF
870 print("fio path is %s" % fio_path)
871 if not shutil.which(fio_path):
872 print("Warning: fio executable not found")
df1eaa36
VF
873
874 artifact_root = args.artifact_root if args.artifact_root else \
875 "fio-test-{0}".format(time.strftime("%Y%m%d-%H%M%S"))
876 os.mkdir(artifact_root)
877 print("Artifact directory is %s" % artifact_root)
878
b1bc705e
VF
879 if not args.skip_req:
880 req = Requirements(fio_root)
881
df1eaa36
VF
882 passed = 0
883 failed = 0
884 skipped = 0
885
886 for config in TEST_LIST:
887 if (args.skip and config['test_id'] in args.skip) or \
888 (args.run_only and config['test_id'] not in args.run_only):
889 skipped = skipped + 1
b1bc705e 890 print("Test {0} SKIPPED (User request)".format(config['test_id']))
df1eaa36
VF
891 continue
892
893 if issubclass(config['test_class'], FioJobTest):
894 if config['pre_job']:
895 fio_pre_job = os.path.join(fio_root, 't', 'jobs',
896 config['pre_job'])
897 else:
898 fio_pre_job = None
899 if config['pre_success']:
900 fio_pre_success = config['pre_success']
901 else:
902 fio_pre_success = None
903 if 'output_format' in config:
904 output_format = config['output_format']
905 else:
906 output_format = 'normal'
907 test = config['test_class'](
908 fio_path,
909 os.path.join(fio_root, 't', 'jobs', config['job']),
910 config['success'],
911 fio_pre_job=fio_pre_job,
912 fio_pre_success=fio_pre_success,
913 output_format=output_format)
914 elif issubclass(config['test_class'], FioExeTest):
915 exe_path = os.path.join(fio_root, config['exe'])
916 if config['parameters']:
917 parameters = [p.format(fio_path=fio_path) for p in config['parameters']]
918 else:
58a77d2a 919 parameters = []
742b8799 920 if Path(exe_path).suffix == '.py' and platform.system() == "Windows":
58a77d2a 921 parameters.insert(0, exe_path)
742b8799 922 exe_path = "python.exe"
58a77d2a
VF
923 if config['test_id'] in pass_through:
924 parameters += pass_through[config['test_id']].split()
df1eaa36
VF
925 test = config['test_class'](exe_path, parameters,
926 config['success'])
927 else:
928 print("Test {0} FAILED: unable to process test config".format(config['test_id']))
929 failed = failed + 1
930 continue
931
b1bc705e 932 if not args.skip_req:
704cc4df 933 reqs_met = True
b1bc705e 934 for req in config['requirements']:
704cc4df
VF
935 reqs_met, reason = req()
936 logging.debug("Test %d: Requirement '%s' met? %s", config['test_id'], reason,
937 reqs_met)
938 if not reqs_met:
b1bc705e 939 break
704cc4df 940 if not reqs_met:
b1bc705e
VF
941 print("Test {0} SKIPPED ({1})".format(config['test_id'], reason))
942 skipped = skipped + 1
943 continue
944
df1eaa36
VF
945 test.setup(artifact_root, config['test_id'])
946 test.run()
947 test.check_result()
948 if test.passed:
949 result = "PASSED"
950 passed = passed + 1
951 else:
952 result = "FAILED: {0}".format(test.failure_reason)
953 failed = failed + 1
15a73987
VF
954 contents, _ = FioJobTest.get_file(test.stderr_file)
955 logging.debug("Test %d: stderr:\n%s", config['test_id'], contents)
956 contents, _ = FioJobTest.get_file(test.stdout_file)
957 logging.debug("Test %d: stdout:\n%s", config['test_id'], contents)
df1eaa36
VF
958 print("Test {0} {1}".format(config['test_id'], result))
959
960 print("{0} test(s) passed, {1} failed, {2} skipped".format(passed, failed, skipped))
961
962 sys.exit(failed)
963
964
965if __name__ == '__main__':
966 main()