updated logging of iops1, iops2, ratio in FioJobTest_iops_rate
[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
aa9f2627 52import traceback
df1eaa36 53import subprocess
b1bc705e 54import multiprocessing
df1eaa36
VF
55from pathlib import Path
56
57
58class FioTest(object):
59 """Base for all fio tests."""
60
61 def __init__(self, exe_path, parameters, success):
62 self.exe_path = exe_path
63 self.parameters = parameters
64 self.success = success
65 self.output = {}
66 self.artifact_root = None
67 self.testnum = None
68 self.test_dir = None
69 self.passed = True
70 self.failure_reason = ''
704cc4df
VF
71 self.command_file = None
72 self.stdout_file = None
73 self.stderr_file = None
74 self.exitcode_file = None
df1eaa36
VF
75
76 def setup(self, artifact_root, testnum):
704cc4df
VF
77 """Setup instance variables for test."""
78
df1eaa36
VF
79 self.artifact_root = artifact_root
80 self.testnum = testnum
81 self.test_dir = os.path.join(artifact_root, "{:04d}".format(testnum))
82 if not os.path.exists(self.test_dir):
83 os.mkdir(self.test_dir)
84
85 self.command_file = os.path.join(
704cc4df
VF
86 self.test_dir,
87 "{0}.command".format(os.path.basename(self.exe_path)))
df1eaa36 88 self.stdout_file = os.path.join(
704cc4df
VF
89 self.test_dir,
90 "{0}.stdout".format(os.path.basename(self.exe_path)))
df1eaa36 91 self.stderr_file = os.path.join(
704cc4df
VF
92 self.test_dir,
93 "{0}.stderr".format(os.path.basename(self.exe_path)))
94 self.exitcode_file = os.path.join(
95 self.test_dir,
96 "{0}.exitcode".format(os.path.basename(self.exe_path)))
df1eaa36
VF
97
98 def run(self):
704cc4df
VF
99 """Run the test."""
100
df1eaa36
VF
101 raise NotImplementedError()
102
103 def check_result(self):
704cc4df
VF
104 """Check test results."""
105
df1eaa36
VF
106 raise NotImplementedError()
107
108
109class FioExeTest(FioTest):
110 """Test consists of an executable binary or script"""
111
112 def __init__(self, exe_path, parameters, success):
113 """Construct a FioExeTest which is a FioTest consisting of an
114 executable binary or script.
115
116 exe_path: location of executable binary or script
117 parameters: list of parameters for executable
118 success: Definition of test success
119 """
120
121 FioTest.__init__(self, exe_path, parameters, success)
122
df1eaa36 123 def run(self):
704cc4df
VF
124 """Execute the binary or script described by this instance."""
125
58a77d2a 126 command = [self.exe_path] + self.parameters
df1eaa36
VF
127 command_file = open(self.command_file, "w+")
128 command_file.write("%s\n" % command)
129 command_file.close()
130
131 stdout_file = open(self.stdout_file, "w+")
132 stderr_file = open(self.stderr_file, "w+")
704cc4df 133 exitcode_file = open(self.exitcode_file, "w+")
df1eaa36 134 try:
b048455f 135 proc = None
df1eaa36
VF
136 # Avoid using subprocess.run() here because when a timeout occurs,
137 # fio will be stopped with SIGKILL. This does not give fio a
138 # chance to clean up and means that child processes may continue
139 # running and submitting IO.
140 proc = subprocess.Popen(command,
141 stdout=stdout_file,
142 stderr=stderr_file,
143 cwd=self.test_dir,
144 universal_newlines=True)
145 proc.communicate(timeout=self.success['timeout'])
704cc4df
VF
146 exitcode_file.write('{0}\n'.format(proc.returncode))
147 logging.debug("Test %d: return code: %d", self.testnum, proc.returncode)
df1eaa36
VF
148 self.output['proc'] = proc
149 except subprocess.TimeoutExpired:
150 proc.terminate()
151 proc.communicate()
152 assert proc.poll()
153 self.output['failure'] = 'timeout'
154 except Exception:
b048455f
VF
155 if proc:
156 if not proc.poll():
157 proc.terminate()
158 proc.communicate()
df1eaa36
VF
159 self.output['failure'] = 'exception'
160 self.output['exc_info'] = sys.exc_info()
161 finally:
162 stdout_file.close()
163 stderr_file.close()
704cc4df 164 exitcode_file.close()
df1eaa36
VF
165
166 def check_result(self):
704cc4df
VF
167 """Check results of test run."""
168
df1eaa36
VF
169 if 'proc' not in self.output:
170 if self.output['failure'] == 'timeout':
171 self.failure_reason = "{0} timeout,".format(self.failure_reason)
172 else:
173 assert self.output['failure'] == 'exception'
174 self.failure_reason = '{0} exception: {1}, {2}'.format(
704cc4df
VF
175 self.failure_reason, self.output['exc_info'][0],
176 self.output['exc_info'][1])
df1eaa36
VF
177
178 self.passed = False
179 return
180
181 if 'zero_return' in self.success:
182 if self.success['zero_return']:
183 if self.output['proc'].returncode != 0:
184 self.passed = False
185 self.failure_reason = "{0} non-zero return code,".format(self.failure_reason)
186 else:
187 if self.output['proc'].returncode == 0:
188 self.failure_reason = "{0} zero return code,".format(self.failure_reason)
189 self.passed = False
190
6d5470c3 191 stderr_size = os.path.getsize(self.stderr_file)
df1eaa36 192 if 'stderr_empty' in self.success:
df1eaa36
VF
193 if self.success['stderr_empty']:
194 if stderr_size != 0:
195 self.failure_reason = "{0} stderr not empty,".format(self.failure_reason)
196 self.passed = False
197 else:
198 if stderr_size == 0:
199 self.failure_reason = "{0} stderr empty,".format(self.failure_reason)
200 self.passed = False
201
202
203class FioJobTest(FioExeTest):
204 """Test consists of a fio job"""
205
206 def __init__(self, fio_path, fio_job, success, fio_pre_job=None,
207 fio_pre_success=None, output_format="normal"):
208 """Construct a FioJobTest which is a FioExeTest consisting of a
209 single fio job file with an optional setup step.
210
211 fio_path: location of fio executable
212 fio_job: location of fio job file
213 success: Definition of test success
214 fio_pre_job: fio job for preconditioning
215 fio_pre_success: Definition of test success for fio precon job
216 output_format: normal (default), json, jsonplus, or terse
217 """
218
219 self.fio_job = fio_job
220 self.fio_pre_job = fio_pre_job
221 self.fio_pre_success = fio_pre_success if fio_pre_success else success
222 self.output_format = output_format
223 self.precon_failed = False
224 self.json_data = None
225 self.fio_output = "{0}.output".format(os.path.basename(self.fio_job))
226 self.fio_args = [
771dbb52 227 "--max-jobs=16",
df1eaa36
VF
228 "--output-format={0}".format(self.output_format),
229 "--output={0}".format(self.fio_output),
230 self.fio_job,
231 ]
232 FioExeTest.__init__(self, fio_path, self.fio_args, success)
233
234 def setup(self, artifact_root, testnum):
704cc4df
VF
235 """Setup instance variables for fio job test."""
236
df1eaa36
VF
237 super(FioJobTest, self).setup(artifact_root, testnum)
238
239 self.command_file = os.path.join(
704cc4df
VF
240 self.test_dir,
241 "{0}.command".format(os.path.basename(self.fio_job)))
df1eaa36 242 self.stdout_file = os.path.join(
704cc4df
VF
243 self.test_dir,
244 "{0}.stdout".format(os.path.basename(self.fio_job)))
df1eaa36 245 self.stderr_file = os.path.join(
704cc4df
VF
246 self.test_dir,
247 "{0}.stderr".format(os.path.basename(self.fio_job)))
248 self.exitcode_file = os.path.join(
249 self.test_dir,
250 "{0}.exitcode".format(os.path.basename(self.fio_job)))
df1eaa36
VF
251
252 def run_pre_job(self):
704cc4df
VF
253 """Run fio job precondition step."""
254
df1eaa36
VF
255 precon = FioJobTest(self.exe_path, self.fio_pre_job,
256 self.fio_pre_success,
257 output_format=self.output_format)
258 precon.setup(self.artifact_root, self.testnum)
259 precon.run()
260 precon.check_result()
261 self.precon_failed = not precon.passed
262 self.failure_reason = precon.failure_reason
263
264 def run(self):
704cc4df
VF
265 """Run fio job test."""
266
df1eaa36
VF
267 if self.fio_pre_job:
268 self.run_pre_job()
269
270 if not self.precon_failed:
271 super(FioJobTest, self).run()
272 else:
704cc4df 273 logging.debug("Test %d: precondition step failed", self.testnum)
df1eaa36 274
15a73987
VF
275 @classmethod
276 def get_file(cls, filename):
277 """Safely read a file."""
278 file_data = ''
279 success = True
280
281 try:
282 with open(filename, "r") as output_file:
283 file_data = output_file.read()
284 except OSError:
285 success = False
286
287 return file_data, success
288
df1eaa36 289 def check_result(self):
704cc4df
VF
290 """Check fio job results."""
291
df1eaa36
VF
292 if self.precon_failed:
293 self.passed = False
294 self.failure_reason = "{0} precondition step failed,".format(self.failure_reason)
295 return
296
297 super(FioJobTest, self).check_result()
298
6d5470c3
VF
299 if not self.passed:
300 return
301
704cc4df 302 if 'json' not in self.output_format:
742b8799
VF
303 return
304
15a73987
VF
305 file_data, success = self.get_file(os.path.join(self.test_dir, self.fio_output))
306 if not success:
742b8799
VF
307 self.failure_reason = "{0} unable to open output file,".format(self.failure_reason)
308 self.passed = False
309 return
310
311 #
312 # Sometimes fio informational messages are included at the top of the
313 # JSON output, especially under Windows. Try to decode output as JSON
314 # data, lopping off up to the first four lines
315 #
316 lines = file_data.splitlines()
317 for i in range(5):
318 file_data = '\n'.join(lines[i:])
df1eaa36 319 try:
742b8799
VF
320 self.json_data = json.loads(file_data)
321 except json.JSONDecodeError:
322 continue
6d5470c3 323 else:
704cc4df 324 logging.debug("Test %d: skipped %d lines decoding JSON data", self.testnum, i)
742b8799
VF
325 return
326
327 self.failure_reason = "{0} unable to decode JSON data,".format(self.failure_reason)
328 self.passed = False
df1eaa36
VF
329
330
331class FioJobTest_t0005(FioJobTest):
332 """Test consists of fio test job t0005
333 Confirm that read['io_kbytes'] == write['io_kbytes'] == 102400"""
334
335 def check_result(self):
336 super(FioJobTest_t0005, self).check_result()
337
338 if not self.passed:
339 return
340
341 if self.json_data['jobs'][0]['read']['io_kbytes'] != 102400:
342 self.failure_reason = "{0} bytes read mismatch,".format(self.failure_reason)
343 self.passed = False
344 if self.json_data['jobs'][0]['write']['io_kbytes'] != 102400:
345 self.failure_reason = "{0} bytes written mismatch,".format(self.failure_reason)
346 self.passed = False
347
348
349class FioJobTest_t0006(FioJobTest):
350 """Test consists of fio test job t0006
351 Confirm that read['io_kbytes'] ~ 2*write['io_kbytes']"""
352
353 def check_result(self):
354 super(FioJobTest_t0006, self).check_result()
355
356 if not self.passed:
357 return
358
359 ratio = self.json_data['jobs'][0]['read']['io_kbytes'] \
360 / self.json_data['jobs'][0]['write']['io_kbytes']
704cc4df 361 logging.debug("Test %d: ratio: %f", self.testnum, ratio)
df1eaa36
VF
362 if ratio < 1.99 or ratio > 2.01:
363 self.failure_reason = "{0} read/write ratio mismatch,".format(self.failure_reason)
364 self.passed = False
365
366
367class FioJobTest_t0007(FioJobTest):
368 """Test consists of fio test job t0007
369 Confirm that read['io_kbytes'] = 87040"""
370
371 def check_result(self):
372 super(FioJobTest_t0007, self).check_result()
373
374 if not self.passed:
375 return
376
377 if self.json_data['jobs'][0]['read']['io_kbytes'] != 87040:
378 self.failure_reason = "{0} bytes read mismatch,".format(self.failure_reason)
379 self.passed = False
380
381
382class FioJobTest_t0008(FioJobTest):
383 """Test consists of fio test job t0008
384 Confirm that read['io_kbytes'] = 32768 and that
385 write['io_kbytes'] ~ 16568
386
387 I did runs with fio-ae2fafc8 and saw write['io_kbytes'] values of
388 16585, 16588. With two runs of fio-3.16 I obtained 16568"""
389
390 def check_result(self):
391 super(FioJobTest_t0008, self).check_result()
392
393 if not self.passed:
394 return
395
396 ratio = self.json_data['jobs'][0]['write']['io_kbytes'] / 16568
704cc4df 397 logging.debug("Test %d: ratio: %f", self.testnum, ratio)
df1eaa36
VF
398
399 if ratio < 0.99 or ratio > 1.01:
400 self.failure_reason = "{0} bytes written mismatch,".format(self.failure_reason)
401 self.passed = False
402 if self.json_data['jobs'][0]['read']['io_kbytes'] != 32768:
403 self.failure_reason = "{0} bytes read mismatch,".format(self.failure_reason)
404 self.passed = False
405
406
407class FioJobTest_t0009(FioJobTest):
408 """Test consists of fio test job t0009
409 Confirm that runtime >= 60s"""
410
411 def check_result(self):
412 super(FioJobTest_t0009, self).check_result()
413
414 if not self.passed:
415 return
416
704cc4df 417 logging.debug('Test %d: elapsed: %d', self.testnum, self.json_data['jobs'][0]['elapsed'])
df1eaa36
VF
418
419 if self.json_data['jobs'][0]['elapsed'] < 60:
420 self.failure_reason = "{0} elapsed time mismatch,".format(self.failure_reason)
421 self.passed = False
422
423
d4e74fda
DB
424class FioJobTest_t0012(FioJobTest):
425 """Test consists of fio test job t0012
426 Confirm ratios of job iops are 1:5:10
427 job1,job2,job3 respectively"""
428
429 def check_result(self):
430 super(FioJobTest_t0012, self).check_result()
431
432 if not self.passed:
433 return
434
435 iops_files = []
436 for i in range(1,4):
437 file_data, success = self.get_file(os.path.join(self.test_dir, "{0}_iops.{1}.log".format(os.path.basename(self.fio_job), i)))
438
439 if not success:
440 self.failure_reason = "{0} unable to open output file,".format(self.failure_reason)
441 self.passed = False
442 return
443
444 iops_files.append(file_data.splitlines())
445
446 # there are 9 samples for job1 and job2, 4 samples for job3
447 iops1 = 0.0
448 iops2 = 0.0
449 iops3 = 0.0
450 for i in range(9):
451 iops1 = iops1 + float(iops_files[0][i].split(',')[1])
452 iops2 = iops2 + float(iops_files[1][i].split(',')[1])
453 iops3 = iops3 + float(iops_files[2][i].split(',')[1])
454
455 ratio1 = iops3/iops2
456 ratio2 = iops3/iops1
457 logging.debug(
458 "sample {0}: job1 iops={1} job2 iops={2} job3 iops={3} job3/job2={4:.3f} job3/job1={5:.3f}".format(
459 i, iops1, iops2, iops3, ratio1, ratio2
460 )
461 )
462
463 # test job1 and job2 succeeded to recalibrate
464 if ratio1 < 1 or ratio1 > 3 or ratio2 < 7 or ratio2 > 13:
465 self.failure_reason = "{0} iops ratio mismatch iops1={1} iops2={2} iops3={3} expected r1~2 r2~10 got r1={4:.3f} r2={5:.3f},".format(
466 self.failure_reason, iops1, iops2, iops3, ratio1, ratio2
467 )
468 self.passed = False
469 return
470
471
472class FioJobTest_t0014(FioJobTest):
473 """Test consists of fio test job t0014
474 Confirm that job1_iops / job2_iops ~ 1:2 for entire duration
475 and that job1_iops / job3_iops ~ 1:3 for first half of duration.
476
477 The test is about making sure the flow feature can
478 re-calibrate the activity dynamically"""
479
480 def check_result(self):
481 super(FioJobTest_t0014, self).check_result()
482
483 if not self.passed:
484 return
485
486 iops_files = []
487 for i in range(1,4):
488 file_data, success = self.get_file(os.path.join(self.test_dir, "{0}_iops.{1}.log".format(os.path.basename(self.fio_job), i)))
489
490 if not success:
491 self.failure_reason = "{0} unable to open output file,".format(self.failure_reason)
492 self.passed = False
493 return
494
495 iops_files.append(file_data.splitlines())
496
497 # there are 9 samples for job1 and job2, 4 samples for job3
498 iops1 = 0.0
499 iops2 = 0.0
500 iops3 = 0.0
501 for i in range(9):
502 if i < 4:
503 iops3 = iops3 + float(iops_files[2][i].split(',')[1])
504 elif i == 4:
505 ratio1 = iops1 / iops2
506 ratio2 = iops1 / iops3
507
508
509 if ratio1 < 0.43 or ratio1 > 0.57 or ratio2 < 0.21 or ratio2 > 0.45:
510 self.failure_reason = "{0} iops ratio mismatch iops1={1} iops2={2} iops3={3}\
511 expected r1~0.5 r2~0.33 got r1={4:.3f} r2={5:.3f},".format(
512 self.failure_reason, iops1, iops2, iops3, ratio1, ratio2
513 )
514 self.passed = False
515
516 iops1 = iops1 + float(iops_files[0][i].split(',')[1])
517 iops2 = iops2 + float(iops_files[1][i].split(',')[1])
518
519 ratio1 = iops1/iops2
520 ratio2 = iops1/iops3
521 logging.debug(
522 "sample {0}: job1 iops={1} job2 iops={2} job3 iops={3} job1/job2={4:.3f} job1/job3={5:.3f}".format(
523 i, iops1, iops2, iops3, ratio1, ratio2
524 )
525 )
526
527 # test job1 and job2 succeeded to recalibrate
528 if ratio1 < 0.43 or ratio1 > 0.57:
529 self.failure_reason = "{0} iops ratio mismatch iops1={1} iops2={2} expected ratio~0.5 got ratio={3:.3f},".format(
530 self.failure_reason, iops1, iops2, ratio1
531 )
532 self.passed = False
533 return
534
535
0a602473 536class FioJobTest_iops_rate(FioJobTest):
df1eaa36
VF
537 """Test consists of fio test job t0009
538 Confirm that job0 iops == 1000
539 and that job1_iops / job0_iops ~ 8
540 With two runs of fio-3.16 I observed a ratio of 8.3"""
541
542 def check_result(self):
0a602473 543 super(FioJobTest_iops_rate, self).check_result()
df1eaa36
VF
544
545 if not self.passed:
546 return
547
548 iops1 = self.json_data['jobs'][0]['read']['iops']
7ddc4ed1 549 logging.debug("Test %d: iops1: %f", self.testnum, iops1)
df1eaa36 550 iops2 = self.json_data['jobs'][1]['read']['iops']
7ddc4ed1 551 logging.debug("Test %d: iops2: %f", self.testnum, iops2)
df1eaa36 552 ratio = iops2 / iops1
704cc4df 553 logging.debug("Test %d: ratio: %f", self.testnum, ratio)
df1eaa36 554
0a9b8988 555 if iops1 < 950 or iops1 > 1050:
df1eaa36
VF
556 self.failure_reason = "{0} iops value mismatch,".format(self.failure_reason)
557 self.passed = False
558
d4e74fda 559 if ratio < 6 or ratio > 10:
df1eaa36
VF
560 self.failure_reason = "{0} iops ratio mismatch,".format(self.failure_reason)
561 self.passed = False
562
563
b1bc705e
VF
564class Requirements(object):
565 """Requirements consists of multiple run environment characteristics.
566 These are to determine if a particular test can be run"""
567
568 _linux = False
569 _libaio = False
570 _zbd = False
571 _root = False
572 _zoned_nullb = False
573 _not_macos = False
c58b33b4 574 _not_windows = False
b1bc705e
VF
575 _unittests = False
576 _cpucount4 = False
577
578 def __init__(self, fio_root):
579 Requirements._not_macos = platform.system() != "Darwin"
c58b33b4 580 Requirements._not_windows = platform.system() != "Windows"
b1bc705e
VF
581 Requirements._linux = platform.system() == "Linux"
582
583 if Requirements._linux:
15a73987
VF
584 config_file = os.path.join(fio_root, "config-host.h")
585 contents, success = FioJobTest.get_file(config_file)
586 if not success:
b1bc705e
VF
587 print("Unable to open {0} to check requirements".format(config_file))
588 Requirements._zbd = True
589 else:
b7694961 590 Requirements._zbd = "CONFIG_HAS_BLKZONED" in contents
b1bc705e
VF
591 Requirements._libaio = "CONFIG_LIBAIO" in contents
592
593 Requirements._root = (os.geteuid() == 0)
594 if Requirements._zbd and Requirements._root:
8854e368
VF
595 try:
596 subprocess.run(["modprobe", "null_blk"],
597 stdout=subprocess.PIPE,
598 stderr=subprocess.PIPE)
599 if os.path.exists("/sys/module/null_blk/parameters/zoned"):
600 Requirements._zoned_nullb = True
601 except Exception:
602 pass
b1bc705e 603
742b8799
VF
604 if platform.system() == "Windows":
605 utest_exe = "unittest.exe"
606 else:
607 utest_exe = "unittest"
608 unittest_path = os.path.join(fio_root, "unittests", utest_exe)
b1bc705e
VF
609 Requirements._unittests = os.path.exists(unittest_path)
610
611 Requirements._cpucount4 = multiprocessing.cpu_count() >= 4
612
613 req_list = [Requirements.linux,
614 Requirements.libaio,
615 Requirements.zbd,
616 Requirements.root,
617 Requirements.zoned_nullb,
618 Requirements.not_macos,
c58b33b4 619 Requirements.not_windows,
b1bc705e
VF
620 Requirements.unittests,
621 Requirements.cpucount4]
622 for req in req_list:
623 value, desc = req()
704cc4df 624 logging.debug("Requirements: Requirement '%s' met? %s", desc, value)
b1bc705e 625
704cc4df
VF
626 @classmethod
627 def linux(cls):
628 """Are we running on Linux?"""
b1bc705e
VF
629 return Requirements._linux, "Linux required"
630
704cc4df
VF
631 @classmethod
632 def libaio(cls):
633 """Is libaio available?"""
b1bc705e
VF
634 return Requirements._libaio, "libaio required"
635
704cc4df
VF
636 @classmethod
637 def zbd(cls):
638 """Is ZBD support available?"""
b1bc705e
VF
639 return Requirements._zbd, "Zoned block device support required"
640
704cc4df
VF
641 @classmethod
642 def root(cls):
643 """Are we running as root?"""
b1bc705e
VF
644 return Requirements._root, "root required"
645
704cc4df
VF
646 @classmethod
647 def zoned_nullb(cls):
648 """Are zoned null block devices available?"""
b1bc705e
VF
649 return Requirements._zoned_nullb, "Zoned null block device support required"
650
704cc4df
VF
651 @classmethod
652 def not_macos(cls):
653 """Are we running on a platform other than macOS?"""
b1bc705e
VF
654 return Requirements._not_macos, "platform other than macOS required"
655
704cc4df
VF
656 @classmethod
657 def not_windows(cls):
658 """Are we running on a platform other than Windws?"""
c58b33b4
VF
659 return Requirements._not_windows, "platform other than Windows required"
660
704cc4df
VF
661 @classmethod
662 def unittests(cls):
663 """Were unittests built?"""
b1bc705e
VF
664 return Requirements._unittests, "Unittests support required"
665
704cc4df
VF
666 @classmethod
667 def cpucount4(cls):
668 """Do we have at least 4 CPUs?"""
b1bc705e
VF
669 return Requirements._cpucount4, "4+ CPUs required"
670
671
df1eaa36 672SUCCESS_DEFAULT = {
704cc4df
VF
673 'zero_return': True,
674 'stderr_empty': True,
675 'timeout': 600,
676 }
df1eaa36 677SUCCESS_NONZERO = {
704cc4df
VF
678 'zero_return': False,
679 'stderr_empty': False,
680 'timeout': 600,
681 }
df1eaa36 682SUCCESS_STDERR = {
704cc4df
VF
683 'zero_return': True,
684 'stderr_empty': False,
685 'timeout': 600,
686 }
df1eaa36 687TEST_LIST = [
704cc4df
VF
688 {
689 'test_id': 1,
690 'test_class': FioJobTest,
691 'job': 't0001-52c58027.fio',
692 'success': SUCCESS_DEFAULT,
693 'pre_job': None,
694 'pre_success': None,
695 'requirements': [],
696 },
697 {
698 'test_id': 2,
699 'test_class': FioJobTest,
700 'job': 't0002-13af05ae-post.fio',
701 'success': SUCCESS_DEFAULT,
702 'pre_job': 't0002-13af05ae-pre.fio',
703 'pre_success': None,
704 'requirements': [Requirements.linux, Requirements.libaio],
705 },
706 {
707 'test_id': 3,
708 'test_class': FioJobTest,
709 'job': 't0003-0ae2c6e1-post.fio',
710 'success': SUCCESS_NONZERO,
711 'pre_job': 't0003-0ae2c6e1-pre.fio',
712 'pre_success': SUCCESS_DEFAULT,
713 'requirements': [Requirements.linux, Requirements.libaio],
714 },
715 {
716 'test_id': 4,
717 'test_class': FioJobTest,
718 'job': 't0004-8a99fdf6.fio',
719 'success': SUCCESS_DEFAULT,
720 'pre_job': None,
721 'pre_success': None,
722 'requirements': [Requirements.linux, Requirements.libaio],
723 },
724 {
725 'test_id': 5,
726 'test_class': FioJobTest_t0005,
727 'job': 't0005-f7078f7b.fio',
728 'success': SUCCESS_DEFAULT,
729 'pre_job': None,
730 'pre_success': None,
731 'output_format': 'json',
732 'requirements': [Requirements.not_windows],
733 },
734 {
735 'test_id': 6,
736 'test_class': FioJobTest_t0006,
737 'job': 't0006-82af2a7c.fio',
738 'success': SUCCESS_DEFAULT,
739 'pre_job': None,
740 'pre_success': None,
741 'output_format': 'json',
742 'requirements': [Requirements.linux, Requirements.libaio],
743 },
744 {
745 'test_id': 7,
746 'test_class': FioJobTest_t0007,
747 'job': 't0007-37cf9e3c.fio',
748 'success': SUCCESS_DEFAULT,
749 'pre_job': None,
750 'pre_success': None,
751 'output_format': 'json',
752 'requirements': [],
753 },
754 {
755 'test_id': 8,
756 'test_class': FioJobTest_t0008,
757 'job': 't0008-ae2fafc8.fio',
758 'success': SUCCESS_DEFAULT,
759 'pre_job': None,
760 'pre_success': None,
761 'output_format': 'json',
762 'requirements': [],
763 },
764 {
765 'test_id': 9,
766 'test_class': FioJobTest_t0009,
767 'job': 't0009-f8b0bd10.fio',
768 'success': SUCCESS_DEFAULT,
769 'pre_job': None,
770 'pre_success': None,
771 'output_format': 'json',
772 'requirements': [Requirements.not_macos,
773 Requirements.cpucount4],
774 # mac os does not support CPU affinity
775 },
776 {
777 'test_id': 10,
778 'test_class': FioJobTest,
779 'job': 't0010-b7aae4ba.fio',
780 'success': SUCCESS_DEFAULT,
781 'pre_job': None,
782 'pre_success': None,
783 'requirements': [],
784 },
785 {
786 'test_id': 11,
0a602473 787 'test_class': FioJobTest_iops_rate,
704cc4df
VF
788 'job': 't0011-5d2788d5.fio',
789 'success': SUCCESS_DEFAULT,
790 'pre_job': None,
791 'pre_success': None,
792 'output_format': 'json',
793 'requirements': [],
794 },
0a602473
BVA
795 {
796 'test_id': 12,
d4e74fda 797 'test_class': FioJobTest_t0012,
0a602473
BVA
798 'job': 't0012.fio',
799 'success': SUCCESS_DEFAULT,
800 'pre_job': None,
801 'pre_success': None,
802 'output_format': 'json',
d4e74fda 803 'requirements': [],
0a602473 804 },
f0c7ae7a
BVA
805 {
806 'test_id': 13,
061a0773 807 'test_class': FioJobTest,
f0c7ae7a
BVA
808 'job': 't0013.fio',
809 'success': SUCCESS_DEFAULT,
810 'pre_job': None,
811 'pre_success': None,
812 'output_format': 'json',
813 'requirements': [],
814 },
d4e74fda
DB
815 {
816 'test_id': 14,
817 'test_class': FioJobTest_t0014,
818 'job': 't0014.fio',
819 'success': SUCCESS_DEFAULT,
820 'pre_job': None,
821 'pre_success': None,
822 'output_format': 'json',
823 'requirements': [],
824 },
704cc4df
VF
825 {
826 'test_id': 1000,
827 'test_class': FioExeTest,
828 'exe': 't/axmap',
829 'parameters': None,
830 'success': SUCCESS_DEFAULT,
831 'requirements': [],
832 },
833 {
834 'test_id': 1001,
835 'test_class': FioExeTest,
836 'exe': 't/ieee754',
837 'parameters': None,
838 'success': SUCCESS_DEFAULT,
839 'requirements': [],
840 },
841 {
842 'test_id': 1002,
843 'test_class': FioExeTest,
844 'exe': 't/lfsr-test',
845 'parameters': ['0xFFFFFF', '0', '0', 'verify'],
846 'success': SUCCESS_STDERR,
847 'requirements': [],
848 },
849 {
850 'test_id': 1003,
851 'test_class': FioExeTest,
852 'exe': 't/readonly.py',
853 'parameters': ['-f', '{fio_path}'],
854 'success': SUCCESS_DEFAULT,
855 'requirements': [],
856 },
857 {
858 'test_id': 1004,
859 'test_class': FioExeTest,
860 'exe': 't/steadystate_tests.py',
861 'parameters': ['{fio_path}'],
862 'success': SUCCESS_DEFAULT,
863 'requirements': [],
864 },
865 {
866 'test_id': 1005,
867 'test_class': FioExeTest,
868 'exe': 't/stest',
869 'parameters': None,
870 'success': SUCCESS_STDERR,
871 'requirements': [],
872 },
873 {
874 'test_id': 1006,
875 'test_class': FioExeTest,
876 'exe': 't/strided.py',
877 'parameters': ['{fio_path}'],
878 'success': SUCCESS_DEFAULT,
879 'requirements': [],
880 },
881 {
882 'test_id': 1007,
883 'test_class': FioExeTest,
037b2b50
DF
884 'exe': 't/zbd/run-tests-against-nullb',
885 'parameters': ['-s', '1'],
704cc4df
VF
886 'success': SUCCESS_DEFAULT,
887 'requirements': [Requirements.linux, Requirements.zbd,
888 Requirements.root],
889 },
890 {
891 'test_id': 1008,
892 'test_class': FioExeTest,
037b2b50
DF
893 'exe': 't/zbd/run-tests-against-nullb',
894 'parameters': ['-s', '2'],
704cc4df
VF
895 'success': SUCCESS_DEFAULT,
896 'requirements': [Requirements.linux, Requirements.zbd,
897 Requirements.root, Requirements.zoned_nullb],
898 },
899 {
900 'test_id': 1009,
901 'test_class': FioExeTest,
902 'exe': 'unittests/unittest',
903 'parameters': None,
904 'success': SUCCESS_DEFAULT,
905 'requirements': [Requirements.unittests],
906 },
907 {
908 'test_id': 1010,
909 'test_class': FioExeTest,
910 'exe': 't/latency_percentiles.py',
911 'parameters': ['-f', '{fio_path}'],
912 'success': SUCCESS_DEFAULT,
913 'requirements': [],
914 },
8403eca6
VF
915 {
916 'test_id': 1011,
917 'test_class': FioExeTest,
918 'exe': 't/jsonplus2csv_test.py',
919 'parameters': ['-f', '{fio_path}'],
920 'success': SUCCESS_DEFAULT,
921 'requirements': [],
922 },
df1eaa36
VF
923]
924
925
926def parse_args():
704cc4df
VF
927 """Parse command-line arguments."""
928
df1eaa36
VF
929 parser = argparse.ArgumentParser()
930 parser.add_argument('-r', '--fio-root',
931 help='fio root path')
932 parser.add_argument('-f', '--fio',
933 help='path to fio executable (e.g., ./fio)')
934 parser.add_argument('-a', '--artifact-root',
935 help='artifact root directory')
936 parser.add_argument('-s', '--skip', nargs='+', type=int,
937 help='list of test(s) to skip')
938 parser.add_argument('-o', '--run-only', nargs='+', type=int,
939 help='list of test(s) to run, skipping all others')
6d5470c3
VF
940 parser.add_argument('-d', '--debug', action='store_true',
941 help='provide debug output')
b1bc705e
VF
942 parser.add_argument('-k', '--skip-req', action='store_true',
943 help='skip requirements checking')
58a77d2a
VF
944 parser.add_argument('-p', '--pass-through', action='append',
945 help='pass-through an argument to an executable test')
df1eaa36
VF
946 args = parser.parse_args()
947
948 return args
949
950
951def main():
704cc4df
VF
952 """Entry point."""
953
df1eaa36 954 args = parse_args()
6d5470c3
VF
955 if args.debug:
956 logging.basicConfig(level=logging.DEBUG)
957 else:
958 logging.basicConfig(level=logging.INFO)
959
58a77d2a
VF
960 pass_through = {}
961 if args.pass_through:
962 for arg in args.pass_through:
963 if not ':' in arg:
964 print("Invalid --pass-through argument '%s'" % arg)
965 print("Syntax for --pass-through is TESTNUMBER:ARGUMENT")
966 return
061a0773 967 split = arg.split(":", 1)
58a77d2a 968 pass_through[int(split[0])] = split[1]
061a0773 969 logging.debug("Pass-through arguments: %s", pass_through)
58a77d2a 970
df1eaa36
VF
971 if args.fio_root:
972 fio_root = args.fio_root
973 else:
6d5470c3
VF
974 fio_root = str(Path(__file__).absolute().parent.parent)
975 print("fio root is %s" % fio_root)
df1eaa36
VF
976
977 if args.fio:
978 fio_path = args.fio
979 else:
742b8799
VF
980 if platform.system() == "Windows":
981 fio_exe = "fio.exe"
982 else:
983 fio_exe = "fio"
984 fio_path = os.path.join(fio_root, fio_exe)
6d5470c3
VF
985 print("fio path is %s" % fio_path)
986 if not shutil.which(fio_path):
987 print("Warning: fio executable not found")
df1eaa36
VF
988
989 artifact_root = args.artifact_root if args.artifact_root else \
990 "fio-test-{0}".format(time.strftime("%Y%m%d-%H%M%S"))
991 os.mkdir(artifact_root)
992 print("Artifact directory is %s" % artifact_root)
993
b1bc705e
VF
994 if not args.skip_req:
995 req = Requirements(fio_root)
996
df1eaa36
VF
997 passed = 0
998 failed = 0
999 skipped = 0
1000
1001 for config in TEST_LIST:
1002 if (args.skip and config['test_id'] in args.skip) or \
1003 (args.run_only and config['test_id'] not in args.run_only):
1004 skipped = skipped + 1
b1bc705e 1005 print("Test {0} SKIPPED (User request)".format(config['test_id']))
df1eaa36
VF
1006 continue
1007
1008 if issubclass(config['test_class'], FioJobTest):
1009 if config['pre_job']:
1010 fio_pre_job = os.path.join(fio_root, 't', 'jobs',
1011 config['pre_job'])
1012 else:
1013 fio_pre_job = None
1014 if config['pre_success']:
1015 fio_pre_success = config['pre_success']
1016 else:
1017 fio_pre_success = None
1018 if 'output_format' in config:
1019 output_format = config['output_format']
1020 else:
1021 output_format = 'normal'
1022 test = config['test_class'](
1023 fio_path,
1024 os.path.join(fio_root, 't', 'jobs', config['job']),
1025 config['success'],
1026 fio_pre_job=fio_pre_job,
1027 fio_pre_success=fio_pre_success,
1028 output_format=output_format)
9393cdaa 1029 desc = config['job']
df1eaa36
VF
1030 elif issubclass(config['test_class'], FioExeTest):
1031 exe_path = os.path.join(fio_root, config['exe'])
1032 if config['parameters']:
1033 parameters = [p.format(fio_path=fio_path) for p in config['parameters']]
1034 else:
58a77d2a 1035 parameters = []
742b8799 1036 if Path(exe_path).suffix == '.py' and platform.system() == "Windows":
58a77d2a 1037 parameters.insert(0, exe_path)
742b8799 1038 exe_path = "python.exe"
58a77d2a
VF
1039 if config['test_id'] in pass_through:
1040 parameters += pass_through[config['test_id']].split()
df1eaa36
VF
1041 test = config['test_class'](exe_path, parameters,
1042 config['success'])
9393cdaa 1043 desc = config['exe']
df1eaa36
VF
1044 else:
1045 print("Test {0} FAILED: unable to process test config".format(config['test_id']))
1046 failed = failed + 1
1047 continue
1048
b1bc705e 1049 if not args.skip_req:
704cc4df 1050 reqs_met = True
b1bc705e 1051 for req in config['requirements']:
704cc4df
VF
1052 reqs_met, reason = req()
1053 logging.debug("Test %d: Requirement '%s' met? %s", config['test_id'], reason,
1054 reqs_met)
1055 if not reqs_met:
b1bc705e 1056 break
704cc4df 1057 if not reqs_met:
9393cdaa 1058 print("Test {0} SKIPPED ({1}) {2}".format(config['test_id'], reason, desc))
b1bc705e
VF
1059 skipped = skipped + 1
1060 continue
1061
aa9f2627
VF
1062 try:
1063 test.setup(artifact_root, config['test_id'])
1064 test.run()
1065 test.check_result()
1066 except KeyboardInterrupt:
1067 break
1068 except Exception as e:
1069 test.passed = False
1070 test.failure_reason += str(e)
1071 logging.debug("Test %d exception:\n%s\n", config['test_id'], traceback.format_exc())
df1eaa36
VF
1072 if test.passed:
1073 result = "PASSED"
1074 passed = passed + 1
1075 else:
1076 result = "FAILED: {0}".format(test.failure_reason)
1077 failed = failed + 1
15a73987
VF
1078 contents, _ = FioJobTest.get_file(test.stderr_file)
1079 logging.debug("Test %d: stderr:\n%s", config['test_id'], contents)
1080 contents, _ = FioJobTest.get_file(test.stdout_file)
1081 logging.debug("Test %d: stdout:\n%s", config['test_id'], contents)
9393cdaa 1082 print("Test {0} {1} {2}".format(config['test_id'], result, desc))
df1eaa36
VF
1083
1084 print("{0} test(s) passed, {1} failed, {2} skipped".format(passed, failed, skipped))
1085
1086 sys.exit(failed)
1087
1088
1089if __name__ == '__main__':
1090 main()