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