fixed bunch of memory leaks in json constructor
[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']
549 iops2 = self.json_data['jobs'][1]['read']['iops']
550 ratio = iops2 / iops1
704cc4df
VF
551 logging.debug("Test %d: iops1: %f", self.testnum, iops1)
552 logging.debug("Test %d: ratio: %f", self.testnum, ratio)
df1eaa36 553
0a9b8988 554 if iops1 < 950 or iops1 > 1050:
df1eaa36
VF
555 self.failure_reason = "{0} iops value mismatch,".format(self.failure_reason)
556 self.passed = False
557
d4e74fda 558 if ratio < 6 or ratio > 10:
df1eaa36
VF
559 self.failure_reason = "{0} iops ratio mismatch,".format(self.failure_reason)
560 self.passed = False
561
562
b1bc705e
VF
563class Requirements(object):
564 """Requirements consists of multiple run environment characteristics.
565 These are to determine if a particular test can be run"""
566
567 _linux = False
568 _libaio = False
569 _zbd = False
570 _root = False
571 _zoned_nullb = False
572 _not_macos = False
c58b33b4 573 _not_windows = False
b1bc705e
VF
574 _unittests = False
575 _cpucount4 = False
576
577 def __init__(self, fio_root):
578 Requirements._not_macos = platform.system() != "Darwin"
c58b33b4 579 Requirements._not_windows = platform.system() != "Windows"
b1bc705e
VF
580 Requirements._linux = platform.system() == "Linux"
581
582 if Requirements._linux:
15a73987
VF
583 config_file = os.path.join(fio_root, "config-host.h")
584 contents, success = FioJobTest.get_file(config_file)
585 if not success:
b1bc705e
VF
586 print("Unable to open {0} to check requirements".format(config_file))
587 Requirements._zbd = True
588 else:
b7694961 589 Requirements._zbd = "CONFIG_HAS_BLKZONED" in contents
b1bc705e
VF
590 Requirements._libaio = "CONFIG_LIBAIO" in contents
591
592 Requirements._root = (os.geteuid() == 0)
593 if Requirements._zbd and Requirements._root:
8854e368
VF
594 try:
595 subprocess.run(["modprobe", "null_blk"],
596 stdout=subprocess.PIPE,
597 stderr=subprocess.PIPE)
598 if os.path.exists("/sys/module/null_blk/parameters/zoned"):
599 Requirements._zoned_nullb = True
600 except Exception:
601 pass
b1bc705e 602
742b8799
VF
603 if platform.system() == "Windows":
604 utest_exe = "unittest.exe"
605 else:
606 utest_exe = "unittest"
607 unittest_path = os.path.join(fio_root, "unittests", utest_exe)
b1bc705e
VF
608 Requirements._unittests = os.path.exists(unittest_path)
609
610 Requirements._cpucount4 = multiprocessing.cpu_count() >= 4
611
612 req_list = [Requirements.linux,
613 Requirements.libaio,
614 Requirements.zbd,
615 Requirements.root,
616 Requirements.zoned_nullb,
617 Requirements.not_macos,
c58b33b4 618 Requirements.not_windows,
b1bc705e
VF
619 Requirements.unittests,
620 Requirements.cpucount4]
621 for req in req_list:
622 value, desc = req()
704cc4df 623 logging.debug("Requirements: Requirement '%s' met? %s", desc, value)
b1bc705e 624
704cc4df
VF
625 @classmethod
626 def linux(cls):
627 """Are we running on Linux?"""
b1bc705e
VF
628 return Requirements._linux, "Linux required"
629
704cc4df
VF
630 @classmethod
631 def libaio(cls):
632 """Is libaio available?"""
b1bc705e
VF
633 return Requirements._libaio, "libaio required"
634
704cc4df
VF
635 @classmethod
636 def zbd(cls):
637 """Is ZBD support available?"""
b1bc705e
VF
638 return Requirements._zbd, "Zoned block device support required"
639
704cc4df
VF
640 @classmethod
641 def root(cls):
642 """Are we running as root?"""
b1bc705e
VF
643 return Requirements._root, "root required"
644
704cc4df
VF
645 @classmethod
646 def zoned_nullb(cls):
647 """Are zoned null block devices available?"""
b1bc705e
VF
648 return Requirements._zoned_nullb, "Zoned null block device support required"
649
704cc4df
VF
650 @classmethod
651 def not_macos(cls):
652 """Are we running on a platform other than macOS?"""
b1bc705e
VF
653 return Requirements._not_macos, "platform other than macOS required"
654
704cc4df
VF
655 @classmethod
656 def not_windows(cls):
657 """Are we running on a platform other than Windws?"""
c58b33b4
VF
658 return Requirements._not_windows, "platform other than Windows required"
659
704cc4df
VF
660 @classmethod
661 def unittests(cls):
662 """Were unittests built?"""
b1bc705e
VF
663 return Requirements._unittests, "Unittests support required"
664
704cc4df
VF
665 @classmethod
666 def cpucount4(cls):
667 """Do we have at least 4 CPUs?"""
b1bc705e
VF
668 return Requirements._cpucount4, "4+ CPUs required"
669
670
df1eaa36 671SUCCESS_DEFAULT = {
704cc4df
VF
672 'zero_return': True,
673 'stderr_empty': True,
674 'timeout': 600,
675 }
df1eaa36 676SUCCESS_NONZERO = {
704cc4df
VF
677 'zero_return': False,
678 'stderr_empty': False,
679 'timeout': 600,
680 }
df1eaa36 681SUCCESS_STDERR = {
704cc4df
VF
682 'zero_return': True,
683 'stderr_empty': False,
684 'timeout': 600,
685 }
df1eaa36 686TEST_LIST = [
704cc4df
VF
687 {
688 'test_id': 1,
689 'test_class': FioJobTest,
690 'job': 't0001-52c58027.fio',
691 'success': SUCCESS_DEFAULT,
692 'pre_job': None,
693 'pre_success': None,
694 'requirements': [],
695 },
696 {
697 'test_id': 2,
698 'test_class': FioJobTest,
699 'job': 't0002-13af05ae-post.fio',
700 'success': SUCCESS_DEFAULT,
701 'pre_job': 't0002-13af05ae-pre.fio',
702 'pre_success': None,
703 'requirements': [Requirements.linux, Requirements.libaio],
704 },
705 {
706 'test_id': 3,
707 'test_class': FioJobTest,
708 'job': 't0003-0ae2c6e1-post.fio',
709 'success': SUCCESS_NONZERO,
710 'pre_job': 't0003-0ae2c6e1-pre.fio',
711 'pre_success': SUCCESS_DEFAULT,
712 'requirements': [Requirements.linux, Requirements.libaio],
713 },
714 {
715 'test_id': 4,
716 'test_class': FioJobTest,
717 'job': 't0004-8a99fdf6.fio',
718 'success': SUCCESS_DEFAULT,
719 'pre_job': None,
720 'pre_success': None,
721 'requirements': [Requirements.linux, Requirements.libaio],
722 },
723 {
724 'test_id': 5,
725 'test_class': FioJobTest_t0005,
726 'job': 't0005-f7078f7b.fio',
727 'success': SUCCESS_DEFAULT,
728 'pre_job': None,
729 'pre_success': None,
730 'output_format': 'json',
731 'requirements': [Requirements.not_windows],
732 },
733 {
734 'test_id': 6,
735 'test_class': FioJobTest_t0006,
736 'job': 't0006-82af2a7c.fio',
737 'success': SUCCESS_DEFAULT,
738 'pre_job': None,
739 'pre_success': None,
740 'output_format': 'json',
741 'requirements': [Requirements.linux, Requirements.libaio],
742 },
743 {
744 'test_id': 7,
745 'test_class': FioJobTest_t0007,
746 'job': 't0007-37cf9e3c.fio',
747 'success': SUCCESS_DEFAULT,
748 'pre_job': None,
749 'pre_success': None,
750 'output_format': 'json',
751 'requirements': [],
752 },
753 {
754 'test_id': 8,
755 'test_class': FioJobTest_t0008,
756 'job': 't0008-ae2fafc8.fio',
757 'success': SUCCESS_DEFAULT,
758 'pre_job': None,
759 'pre_success': None,
760 'output_format': 'json',
761 'requirements': [],
762 },
763 {
764 'test_id': 9,
765 'test_class': FioJobTest_t0009,
766 'job': 't0009-f8b0bd10.fio',
767 'success': SUCCESS_DEFAULT,
768 'pre_job': None,
769 'pre_success': None,
770 'output_format': 'json',
771 'requirements': [Requirements.not_macos,
772 Requirements.cpucount4],
773 # mac os does not support CPU affinity
774 },
775 {
776 'test_id': 10,
777 'test_class': FioJobTest,
778 'job': 't0010-b7aae4ba.fio',
779 'success': SUCCESS_DEFAULT,
780 'pre_job': None,
781 'pre_success': None,
782 'requirements': [],
783 },
784 {
785 'test_id': 11,
0a602473 786 'test_class': FioJobTest_iops_rate,
704cc4df
VF
787 'job': 't0011-5d2788d5.fio',
788 'success': SUCCESS_DEFAULT,
789 'pre_job': None,
790 'pre_success': None,
791 'output_format': 'json',
792 'requirements': [],
793 },
0a602473
BVA
794 {
795 'test_id': 12,
d4e74fda 796 'test_class': FioJobTest_t0012,
0a602473
BVA
797 'job': 't0012.fio',
798 'success': SUCCESS_DEFAULT,
799 'pre_job': None,
800 'pre_success': None,
801 'output_format': 'json',
d4e74fda 802 'requirements': [],
0a602473 803 },
f0c7ae7a
BVA
804 {
805 'test_id': 13,
061a0773 806 'test_class': FioJobTest,
f0c7ae7a
BVA
807 'job': 't0013.fio',
808 'success': SUCCESS_DEFAULT,
809 'pre_job': None,
810 'pre_success': None,
811 'output_format': 'json',
812 'requirements': [],
813 },
d4e74fda
DB
814 {
815 'test_id': 14,
816 'test_class': FioJobTest_t0014,
817 'job': 't0014.fio',
818 'success': SUCCESS_DEFAULT,
819 'pre_job': None,
820 'pre_success': None,
821 'output_format': 'json',
822 'requirements': [],
823 },
704cc4df
VF
824 {
825 'test_id': 1000,
826 'test_class': FioExeTest,
827 'exe': 't/axmap',
828 'parameters': None,
829 'success': SUCCESS_DEFAULT,
830 'requirements': [],
831 },
832 {
833 'test_id': 1001,
834 'test_class': FioExeTest,
835 'exe': 't/ieee754',
836 'parameters': None,
837 'success': SUCCESS_DEFAULT,
838 'requirements': [],
839 },
840 {
841 'test_id': 1002,
842 'test_class': FioExeTest,
843 'exe': 't/lfsr-test',
844 'parameters': ['0xFFFFFF', '0', '0', 'verify'],
845 'success': SUCCESS_STDERR,
846 'requirements': [],
847 },
848 {
849 'test_id': 1003,
850 'test_class': FioExeTest,
851 'exe': 't/readonly.py',
852 'parameters': ['-f', '{fio_path}'],
853 'success': SUCCESS_DEFAULT,
854 'requirements': [],
855 },
856 {
857 'test_id': 1004,
858 'test_class': FioExeTest,
859 'exe': 't/steadystate_tests.py',
860 'parameters': ['{fio_path}'],
861 'success': SUCCESS_DEFAULT,
862 'requirements': [],
863 },
864 {
865 'test_id': 1005,
866 'test_class': FioExeTest,
867 'exe': 't/stest',
868 'parameters': None,
869 'success': SUCCESS_STDERR,
870 'requirements': [],
871 },
872 {
873 'test_id': 1006,
874 'test_class': FioExeTest,
875 'exe': 't/strided.py',
876 'parameters': ['{fio_path}'],
877 'success': SUCCESS_DEFAULT,
878 'requirements': [],
879 },
880 {
881 'test_id': 1007,
882 'test_class': FioExeTest,
037b2b50
DF
883 'exe': 't/zbd/run-tests-against-nullb',
884 'parameters': ['-s', '1'],
704cc4df
VF
885 'success': SUCCESS_DEFAULT,
886 'requirements': [Requirements.linux, Requirements.zbd,
887 Requirements.root],
888 },
889 {
890 'test_id': 1008,
891 'test_class': FioExeTest,
037b2b50
DF
892 'exe': 't/zbd/run-tests-against-nullb',
893 'parameters': ['-s', '2'],
704cc4df
VF
894 'success': SUCCESS_DEFAULT,
895 'requirements': [Requirements.linux, Requirements.zbd,
896 Requirements.root, Requirements.zoned_nullb],
897 },
898 {
899 'test_id': 1009,
900 'test_class': FioExeTest,
901 'exe': 'unittests/unittest',
902 'parameters': None,
903 'success': SUCCESS_DEFAULT,
904 'requirements': [Requirements.unittests],
905 },
906 {
907 'test_id': 1010,
908 'test_class': FioExeTest,
909 'exe': 't/latency_percentiles.py',
910 'parameters': ['-f', '{fio_path}'],
911 'success': SUCCESS_DEFAULT,
912 'requirements': [],
913 },
8403eca6
VF
914 {
915 'test_id': 1011,
916 'test_class': FioExeTest,
917 'exe': 't/jsonplus2csv_test.py',
918 'parameters': ['-f', '{fio_path}'],
919 'success': SUCCESS_DEFAULT,
920 'requirements': [],
921 },
df1eaa36
VF
922]
923
924
925def parse_args():
704cc4df
VF
926 """Parse command-line arguments."""
927
df1eaa36
VF
928 parser = argparse.ArgumentParser()
929 parser.add_argument('-r', '--fio-root',
930 help='fio root path')
931 parser.add_argument('-f', '--fio',
932 help='path to fio executable (e.g., ./fio)')
933 parser.add_argument('-a', '--artifact-root',
934 help='artifact root directory')
935 parser.add_argument('-s', '--skip', nargs='+', type=int,
936 help='list of test(s) to skip')
937 parser.add_argument('-o', '--run-only', nargs='+', type=int,
938 help='list of test(s) to run, skipping all others')
6d5470c3
VF
939 parser.add_argument('-d', '--debug', action='store_true',
940 help='provide debug output')
b1bc705e
VF
941 parser.add_argument('-k', '--skip-req', action='store_true',
942 help='skip requirements checking')
58a77d2a
VF
943 parser.add_argument('-p', '--pass-through', action='append',
944 help='pass-through an argument to an executable test')
df1eaa36
VF
945 args = parser.parse_args()
946
947 return args
948
949
950def main():
704cc4df
VF
951 """Entry point."""
952
df1eaa36 953 args = parse_args()
6d5470c3
VF
954 if args.debug:
955 logging.basicConfig(level=logging.DEBUG)
956 else:
957 logging.basicConfig(level=logging.INFO)
958
58a77d2a
VF
959 pass_through = {}
960 if args.pass_through:
961 for arg in args.pass_through:
962 if not ':' in arg:
963 print("Invalid --pass-through argument '%s'" % arg)
964 print("Syntax for --pass-through is TESTNUMBER:ARGUMENT")
965 return
061a0773 966 split = arg.split(":", 1)
58a77d2a 967 pass_through[int(split[0])] = split[1]
061a0773 968 logging.debug("Pass-through arguments: %s", pass_through)
58a77d2a 969
df1eaa36
VF
970 if args.fio_root:
971 fio_root = args.fio_root
972 else:
6d5470c3
VF
973 fio_root = str(Path(__file__).absolute().parent.parent)
974 print("fio root is %s" % fio_root)
df1eaa36
VF
975
976 if args.fio:
977 fio_path = args.fio
978 else:
742b8799
VF
979 if platform.system() == "Windows":
980 fio_exe = "fio.exe"
981 else:
982 fio_exe = "fio"
983 fio_path = os.path.join(fio_root, fio_exe)
6d5470c3
VF
984 print("fio path is %s" % fio_path)
985 if not shutil.which(fio_path):
986 print("Warning: fio executable not found")
df1eaa36
VF
987
988 artifact_root = args.artifact_root if args.artifact_root else \
989 "fio-test-{0}".format(time.strftime("%Y%m%d-%H%M%S"))
990 os.mkdir(artifact_root)
991 print("Artifact directory is %s" % artifact_root)
992
b1bc705e
VF
993 if not args.skip_req:
994 req = Requirements(fio_root)
995
df1eaa36
VF
996 passed = 0
997 failed = 0
998 skipped = 0
999
1000 for config in TEST_LIST:
1001 if (args.skip and config['test_id'] in args.skip) or \
1002 (args.run_only and config['test_id'] not in args.run_only):
1003 skipped = skipped + 1
b1bc705e 1004 print("Test {0} SKIPPED (User request)".format(config['test_id']))
df1eaa36
VF
1005 continue
1006
1007 if issubclass(config['test_class'], FioJobTest):
1008 if config['pre_job']:
1009 fio_pre_job = os.path.join(fio_root, 't', 'jobs',
1010 config['pre_job'])
1011 else:
1012 fio_pre_job = None
1013 if config['pre_success']:
1014 fio_pre_success = config['pre_success']
1015 else:
1016 fio_pre_success = None
1017 if 'output_format' in config:
1018 output_format = config['output_format']
1019 else:
1020 output_format = 'normal'
1021 test = config['test_class'](
1022 fio_path,
1023 os.path.join(fio_root, 't', 'jobs', config['job']),
1024 config['success'],
1025 fio_pre_job=fio_pre_job,
1026 fio_pre_success=fio_pre_success,
1027 output_format=output_format)
9393cdaa 1028 desc = config['job']
df1eaa36
VF
1029 elif issubclass(config['test_class'], FioExeTest):
1030 exe_path = os.path.join(fio_root, config['exe'])
1031 if config['parameters']:
1032 parameters = [p.format(fio_path=fio_path) for p in config['parameters']]
1033 else:
58a77d2a 1034 parameters = []
742b8799 1035 if Path(exe_path).suffix == '.py' and platform.system() == "Windows":
58a77d2a 1036 parameters.insert(0, exe_path)
742b8799 1037 exe_path = "python.exe"
58a77d2a
VF
1038 if config['test_id'] in pass_through:
1039 parameters += pass_through[config['test_id']].split()
df1eaa36
VF
1040 test = config['test_class'](exe_path, parameters,
1041 config['success'])
9393cdaa 1042 desc = config['exe']
df1eaa36
VF
1043 else:
1044 print("Test {0} FAILED: unable to process test config".format(config['test_id']))
1045 failed = failed + 1
1046 continue
1047
b1bc705e 1048 if not args.skip_req:
704cc4df 1049 reqs_met = True
b1bc705e 1050 for req in config['requirements']:
704cc4df
VF
1051 reqs_met, reason = req()
1052 logging.debug("Test %d: Requirement '%s' met? %s", config['test_id'], reason,
1053 reqs_met)
1054 if not reqs_met:
b1bc705e 1055 break
704cc4df 1056 if not reqs_met:
9393cdaa 1057 print("Test {0} SKIPPED ({1}) {2}".format(config['test_id'], reason, desc))
b1bc705e
VF
1058 skipped = skipped + 1
1059 continue
1060
aa9f2627
VF
1061 try:
1062 test.setup(artifact_root, config['test_id'])
1063 test.run()
1064 test.check_result()
1065 except KeyboardInterrupt:
1066 break
1067 except Exception as e:
1068 test.passed = False
1069 test.failure_reason += str(e)
1070 logging.debug("Test %d exception:\n%s\n", config['test_id'], traceback.format_exc())
df1eaa36
VF
1071 if test.passed:
1072 result = "PASSED"
1073 passed = passed + 1
1074 else:
1075 result = "FAILED: {0}".format(test.failure_reason)
1076 failed = failed + 1
15a73987
VF
1077 contents, _ = FioJobTest.get_file(test.stderr_file)
1078 logging.debug("Test %d: stderr:\n%s", config['test_id'], contents)
1079 contents, _ = FioJobTest.get_file(test.stdout_file)
1080 logging.debug("Test %d: stdout:\n%s", config['test_id'], contents)
9393cdaa 1081 print("Test {0} {1} {2}".format(config['test_id'], result, desc))
df1eaa36
VF
1082
1083 print("{0} test(s) passed, {1} failed, {2} skipped".format(passed, failed, skipped))
1084
1085 sys.exit(failed)
1086
1087
1088if __name__ == '__main__':
1089 main()