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