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