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