appveyor: use on_finish section to upload artifacts
[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
df1eaa36
VF
125 if self.parameters:
126 command = [self.exe_path] + self.parameters
127 else:
128 command = [self.exe_path]
129 command_file = open(self.command_file, "w+")
130 command_file.write("%s\n" % command)
131 command_file.close()
132
133 stdout_file = open(self.stdout_file, "w+")
134 stderr_file = open(self.stderr_file, "w+")
704cc4df 135 exitcode_file = open(self.exitcode_file, "w+")
df1eaa36 136 try:
b048455f 137 proc = None
df1eaa36
VF
138 # Avoid using subprocess.run() here because when a timeout occurs,
139 # fio will be stopped with SIGKILL. This does not give fio a
140 # chance to clean up and means that child processes may continue
141 # running and submitting IO.
142 proc = subprocess.Popen(command,
143 stdout=stdout_file,
144 stderr=stderr_file,
145 cwd=self.test_dir,
146 universal_newlines=True)
147 proc.communicate(timeout=self.success['timeout'])
704cc4df
VF
148 exitcode_file.write('{0}\n'.format(proc.returncode))
149 logging.debug("Test %d: return code: %d", self.testnum, proc.returncode)
df1eaa36
VF
150 self.output['proc'] = proc
151 except subprocess.TimeoutExpired:
152 proc.terminate()
153 proc.communicate()
154 assert proc.poll()
155 self.output['failure'] = 'timeout'
156 except Exception:
b048455f
VF
157 if proc:
158 if not proc.poll():
159 proc.terminate()
160 proc.communicate()
df1eaa36
VF
161 self.output['failure'] = 'exception'
162 self.output['exc_info'] = sys.exc_info()
163 finally:
164 stdout_file.close()
165 stderr_file.close()
704cc4df 166 exitcode_file.close()
df1eaa36
VF
167
168 def check_result(self):
704cc4df
VF
169 """Check results of test run."""
170
df1eaa36
VF
171 if 'proc' not in self.output:
172 if self.output['failure'] == 'timeout':
173 self.failure_reason = "{0} timeout,".format(self.failure_reason)
174 else:
175 assert self.output['failure'] == 'exception'
176 self.failure_reason = '{0} exception: {1}, {2}'.format(
704cc4df
VF
177 self.failure_reason, self.output['exc_info'][0],
178 self.output['exc_info'][1])
df1eaa36
VF
179
180 self.passed = False
181 return
182
183 if 'zero_return' in self.success:
184 if self.success['zero_return']:
185 if self.output['proc'].returncode != 0:
186 self.passed = False
187 self.failure_reason = "{0} non-zero return code,".format(self.failure_reason)
188 else:
189 if self.output['proc'].returncode == 0:
190 self.failure_reason = "{0} zero return code,".format(self.failure_reason)
191 self.passed = False
192
6d5470c3 193 stderr_size = os.path.getsize(self.stderr_file)
df1eaa36 194 if 'stderr_empty' in self.success:
df1eaa36
VF
195 if self.success['stderr_empty']:
196 if stderr_size != 0:
197 self.failure_reason = "{0} stderr not empty,".format(self.failure_reason)
198 self.passed = False
199 else:
200 if stderr_size == 0:
201 self.failure_reason = "{0} stderr empty,".format(self.failure_reason)
202 self.passed = False
203
204
205class FioJobTest(FioExeTest):
206 """Test consists of a fio job"""
207
208 def __init__(self, fio_path, fio_job, success, fio_pre_job=None,
209 fio_pre_success=None, output_format="normal"):
210 """Construct a FioJobTest which is a FioExeTest consisting of a
211 single fio job file with an optional setup step.
212
213 fio_path: location of fio executable
214 fio_job: location of fio job file
215 success: Definition of test success
216 fio_pre_job: fio job for preconditioning
217 fio_pre_success: Definition of test success for fio precon job
218 output_format: normal (default), json, jsonplus, or terse
219 """
220
221 self.fio_job = fio_job
222 self.fio_pre_job = fio_pre_job
223 self.fio_pre_success = fio_pre_success if fio_pre_success else success
224 self.output_format = output_format
225 self.precon_failed = False
226 self.json_data = None
227 self.fio_output = "{0}.output".format(os.path.basename(self.fio_job))
228 self.fio_args = [
771dbb52 229 "--max-jobs=16",
df1eaa36
VF
230 "--output-format={0}".format(self.output_format),
231 "--output={0}".format(self.fio_output),
232 self.fio_job,
233 ]
234 FioExeTest.__init__(self, fio_path, self.fio_args, success)
235
236 def setup(self, artifact_root, testnum):
704cc4df
VF
237 """Setup instance variables for fio job test."""
238
df1eaa36
VF
239 super(FioJobTest, self).setup(artifact_root, testnum)
240
241 self.command_file = os.path.join(
704cc4df
VF
242 self.test_dir,
243 "{0}.command".format(os.path.basename(self.fio_job)))
df1eaa36 244 self.stdout_file = os.path.join(
704cc4df
VF
245 self.test_dir,
246 "{0}.stdout".format(os.path.basename(self.fio_job)))
df1eaa36 247 self.stderr_file = os.path.join(
704cc4df
VF
248 self.test_dir,
249 "{0}.stderr".format(os.path.basename(self.fio_job)))
250 self.exitcode_file = os.path.join(
251 self.test_dir,
252 "{0}.exitcode".format(os.path.basename(self.fio_job)))
df1eaa36
VF
253
254 def run_pre_job(self):
704cc4df
VF
255 """Run fio job precondition step."""
256
df1eaa36
VF
257 precon = FioJobTest(self.exe_path, self.fio_pre_job,
258 self.fio_pre_success,
259 output_format=self.output_format)
260 precon.setup(self.artifact_root, self.testnum)
261 precon.run()
262 precon.check_result()
263 self.precon_failed = not precon.passed
264 self.failure_reason = precon.failure_reason
265
266 def run(self):
704cc4df
VF
267 """Run fio job test."""
268
df1eaa36
VF
269 if self.fio_pre_job:
270 self.run_pre_job()
271
272 if not self.precon_failed:
273 super(FioJobTest, self).run()
274 else:
704cc4df 275 logging.debug("Test %d: precondition step failed", self.testnum)
df1eaa36 276
15a73987
VF
277 @classmethod
278 def get_file(cls, filename):
279 """Safely read a file."""
280 file_data = ''
281 success = True
282
283 try:
284 with open(filename, "r") as output_file:
285 file_data = output_file.read()
286 except OSError:
287 success = False
288
289 return file_data, success
290
df1eaa36 291 def check_result(self):
704cc4df
VF
292 """Check fio job results."""
293
df1eaa36
VF
294 if self.precon_failed:
295 self.passed = False
296 self.failure_reason = "{0} precondition step failed,".format(self.failure_reason)
297 return
298
299 super(FioJobTest, self).check_result()
300
6d5470c3
VF
301 if not self.passed:
302 return
303
704cc4df 304 if 'json' not in self.output_format:
742b8799
VF
305 return
306
15a73987
VF
307 file_data, success = self.get_file(os.path.join(self.test_dir, self.fio_output))
308 if not success:
742b8799
VF
309 self.failure_reason = "{0} unable to open output file,".format(self.failure_reason)
310 self.passed = False
311 return
312
313 #
314 # Sometimes fio informational messages are included at the top of the
315 # JSON output, especially under Windows. Try to decode output as JSON
316 # data, lopping off up to the first four lines
317 #
318 lines = file_data.splitlines()
319 for i in range(5):
320 file_data = '\n'.join(lines[i:])
df1eaa36 321 try:
742b8799
VF
322 self.json_data = json.loads(file_data)
323 except json.JSONDecodeError:
324 continue
6d5470c3 325 else:
704cc4df 326 logging.debug("Test %d: skipped %d lines decoding JSON data", self.testnum, i)
742b8799
VF
327 return
328
329 self.failure_reason = "{0} unable to decode JSON data,".format(self.failure_reason)
330 self.passed = False
df1eaa36
VF
331
332
333class FioJobTest_t0005(FioJobTest):
334 """Test consists of fio test job t0005
335 Confirm that read['io_kbytes'] == write['io_kbytes'] == 102400"""
336
337 def check_result(self):
338 super(FioJobTest_t0005, self).check_result()
339
340 if not self.passed:
341 return
342
343 if self.json_data['jobs'][0]['read']['io_kbytes'] != 102400:
344 self.failure_reason = "{0} bytes read mismatch,".format(self.failure_reason)
345 self.passed = False
346 if self.json_data['jobs'][0]['write']['io_kbytes'] != 102400:
347 self.failure_reason = "{0} bytes written mismatch,".format(self.failure_reason)
348 self.passed = False
349
350
351class FioJobTest_t0006(FioJobTest):
352 """Test consists of fio test job t0006
353 Confirm that read['io_kbytes'] ~ 2*write['io_kbytes']"""
354
355 def check_result(self):
356 super(FioJobTest_t0006, self).check_result()
357
358 if not self.passed:
359 return
360
361 ratio = self.json_data['jobs'][0]['read']['io_kbytes'] \
362 / self.json_data['jobs'][0]['write']['io_kbytes']
704cc4df 363 logging.debug("Test %d: ratio: %f", self.testnum, ratio)
df1eaa36
VF
364 if ratio < 1.99 or ratio > 2.01:
365 self.failure_reason = "{0} read/write ratio mismatch,".format(self.failure_reason)
366 self.passed = False
367
368
369class FioJobTest_t0007(FioJobTest):
370 """Test consists of fio test job t0007
371 Confirm that read['io_kbytes'] = 87040"""
372
373 def check_result(self):
374 super(FioJobTest_t0007, self).check_result()
375
376 if not self.passed:
377 return
378
379 if self.json_data['jobs'][0]['read']['io_kbytes'] != 87040:
380 self.failure_reason = "{0} bytes read mismatch,".format(self.failure_reason)
381 self.passed = False
382
383
384class FioJobTest_t0008(FioJobTest):
385 """Test consists of fio test job t0008
386 Confirm that read['io_kbytes'] = 32768 and that
387 write['io_kbytes'] ~ 16568
388
389 I did runs with fio-ae2fafc8 and saw write['io_kbytes'] values of
390 16585, 16588. With two runs of fio-3.16 I obtained 16568"""
391
392 def check_result(self):
393 super(FioJobTest_t0008, self).check_result()
394
395 if not self.passed:
396 return
397
398 ratio = self.json_data['jobs'][0]['write']['io_kbytes'] / 16568
704cc4df 399 logging.debug("Test %d: ratio: %f", self.testnum, ratio)
df1eaa36
VF
400
401 if ratio < 0.99 or ratio > 1.01:
402 self.failure_reason = "{0} bytes written mismatch,".format(self.failure_reason)
403 self.passed = False
404 if self.json_data['jobs'][0]['read']['io_kbytes'] != 32768:
405 self.failure_reason = "{0} bytes read mismatch,".format(self.failure_reason)
406 self.passed = False
407
408
409class FioJobTest_t0009(FioJobTest):
410 """Test consists of fio test job t0009
411 Confirm that runtime >= 60s"""
412
413 def check_result(self):
414 super(FioJobTest_t0009, self).check_result()
415
416 if not self.passed:
417 return
418
704cc4df 419 logging.debug('Test %d: elapsed: %d', self.testnum, self.json_data['jobs'][0]['elapsed'])
df1eaa36
VF
420
421 if self.json_data['jobs'][0]['elapsed'] < 60:
422 self.failure_reason = "{0} elapsed time mismatch,".format(self.failure_reason)
423 self.passed = False
424
425
426class FioJobTest_t0011(FioJobTest):
427 """Test consists of fio test job t0009
428 Confirm that job0 iops == 1000
429 and that job1_iops / job0_iops ~ 8
430 With two runs of fio-3.16 I observed a ratio of 8.3"""
431
432 def check_result(self):
433 super(FioJobTest_t0011, self).check_result()
434
435 if not self.passed:
436 return
437
438 iops1 = self.json_data['jobs'][0]['read']['iops']
439 iops2 = self.json_data['jobs'][1]['read']['iops']
440 ratio = iops2 / iops1
704cc4df
VF
441 logging.debug("Test %d: iops1: %f", self.testnum, iops1)
442 logging.debug("Test %d: ratio: %f", self.testnum, ratio)
df1eaa36 443
41ceb6c7 444 if iops1 < 998 or iops1 > 1002:
df1eaa36
VF
445 self.failure_reason = "{0} iops value mismatch,".format(self.failure_reason)
446 self.passed = False
447
448 if ratio < 7 or ratio > 9:
449 self.failure_reason = "{0} iops ratio mismatch,".format(self.failure_reason)
450 self.passed = False
451
452
b1bc705e
VF
453class Requirements(object):
454 """Requirements consists of multiple run environment characteristics.
455 These are to determine if a particular test can be run"""
456
457 _linux = False
458 _libaio = False
459 _zbd = False
460 _root = False
461 _zoned_nullb = False
462 _not_macos = False
c58b33b4 463 _not_windows = False
b1bc705e
VF
464 _unittests = False
465 _cpucount4 = False
466
467 def __init__(self, fio_root):
468 Requirements._not_macos = platform.system() != "Darwin"
c58b33b4 469 Requirements._not_windows = platform.system() != "Windows"
b1bc705e
VF
470 Requirements._linux = platform.system() == "Linux"
471
472 if Requirements._linux:
15a73987
VF
473 config_file = os.path.join(fio_root, "config-host.h")
474 contents, success = FioJobTest.get_file(config_file)
475 if not success:
b1bc705e
VF
476 print("Unable to open {0} to check requirements".format(config_file))
477 Requirements._zbd = True
478 else:
b7694961 479 Requirements._zbd = "CONFIG_HAS_BLKZONED" in contents
b1bc705e
VF
480 Requirements._libaio = "CONFIG_LIBAIO" in contents
481
482 Requirements._root = (os.geteuid() == 0)
483 if Requirements._zbd and Requirements._root:
704cc4df
VF
484 subprocess.run(["modprobe", "null_blk"],
485 stdout=subprocess.PIPE,
486 stderr=subprocess.PIPE)
487 if os.path.exists("/sys/module/null_blk/parameters/zoned"):
488 Requirements._zoned_nullb = True
b1bc705e 489
742b8799
VF
490 if platform.system() == "Windows":
491 utest_exe = "unittest.exe"
492 else:
493 utest_exe = "unittest"
494 unittest_path = os.path.join(fio_root, "unittests", utest_exe)
b1bc705e
VF
495 Requirements._unittests = os.path.exists(unittest_path)
496
497 Requirements._cpucount4 = multiprocessing.cpu_count() >= 4
498
499 req_list = [Requirements.linux,
500 Requirements.libaio,
501 Requirements.zbd,
502 Requirements.root,
503 Requirements.zoned_nullb,
504 Requirements.not_macos,
c58b33b4 505 Requirements.not_windows,
b1bc705e
VF
506 Requirements.unittests,
507 Requirements.cpucount4]
508 for req in req_list:
509 value, desc = req()
704cc4df 510 logging.debug("Requirements: Requirement '%s' met? %s", desc, value)
b1bc705e 511
704cc4df
VF
512 @classmethod
513 def linux(cls):
514 """Are we running on Linux?"""
b1bc705e
VF
515 return Requirements._linux, "Linux required"
516
704cc4df
VF
517 @classmethod
518 def libaio(cls):
519 """Is libaio available?"""
b1bc705e
VF
520 return Requirements._libaio, "libaio required"
521
704cc4df
VF
522 @classmethod
523 def zbd(cls):
524 """Is ZBD support available?"""
b1bc705e
VF
525 return Requirements._zbd, "Zoned block device support required"
526
704cc4df
VF
527 @classmethod
528 def root(cls):
529 """Are we running as root?"""
b1bc705e
VF
530 return Requirements._root, "root required"
531
704cc4df
VF
532 @classmethod
533 def zoned_nullb(cls):
534 """Are zoned null block devices available?"""
b1bc705e
VF
535 return Requirements._zoned_nullb, "Zoned null block device support required"
536
704cc4df
VF
537 @classmethod
538 def not_macos(cls):
539 """Are we running on a platform other than macOS?"""
b1bc705e
VF
540 return Requirements._not_macos, "platform other than macOS required"
541
704cc4df
VF
542 @classmethod
543 def not_windows(cls):
544 """Are we running on a platform other than Windws?"""
c58b33b4
VF
545 return Requirements._not_windows, "platform other than Windows required"
546
704cc4df
VF
547 @classmethod
548 def unittests(cls):
549 """Were unittests built?"""
b1bc705e
VF
550 return Requirements._unittests, "Unittests support required"
551
704cc4df
VF
552 @classmethod
553 def cpucount4(cls):
554 """Do we have at least 4 CPUs?"""
b1bc705e
VF
555 return Requirements._cpucount4, "4+ CPUs required"
556
557
df1eaa36 558SUCCESS_DEFAULT = {
704cc4df
VF
559 'zero_return': True,
560 'stderr_empty': True,
561 'timeout': 600,
562 }
df1eaa36 563SUCCESS_NONZERO = {
704cc4df
VF
564 'zero_return': False,
565 'stderr_empty': False,
566 'timeout': 600,
567 }
df1eaa36 568SUCCESS_STDERR = {
704cc4df
VF
569 'zero_return': True,
570 'stderr_empty': False,
571 'timeout': 600,
572 }
df1eaa36 573TEST_LIST = [
704cc4df
VF
574 {
575 'test_id': 1,
576 'test_class': FioJobTest,
577 'job': 't0001-52c58027.fio',
578 'success': SUCCESS_DEFAULT,
579 'pre_job': None,
580 'pre_success': None,
581 'requirements': [],
582 },
583 {
584 'test_id': 2,
585 'test_class': FioJobTest,
586 'job': 't0002-13af05ae-post.fio',
587 'success': SUCCESS_DEFAULT,
588 'pre_job': 't0002-13af05ae-pre.fio',
589 'pre_success': None,
590 'requirements': [Requirements.linux, Requirements.libaio],
591 },
592 {
593 'test_id': 3,
594 'test_class': FioJobTest,
595 'job': 't0003-0ae2c6e1-post.fio',
596 'success': SUCCESS_NONZERO,
597 'pre_job': 't0003-0ae2c6e1-pre.fio',
598 'pre_success': SUCCESS_DEFAULT,
599 'requirements': [Requirements.linux, Requirements.libaio],
600 },
601 {
602 'test_id': 4,
603 'test_class': FioJobTest,
604 'job': 't0004-8a99fdf6.fio',
605 'success': SUCCESS_DEFAULT,
606 'pre_job': None,
607 'pre_success': None,
608 'requirements': [Requirements.linux, Requirements.libaio],
609 },
610 {
611 'test_id': 5,
612 'test_class': FioJobTest_t0005,
613 'job': 't0005-f7078f7b.fio',
614 'success': SUCCESS_DEFAULT,
615 'pre_job': None,
616 'pre_success': None,
617 'output_format': 'json',
618 'requirements': [Requirements.not_windows],
619 },
620 {
621 'test_id': 6,
622 'test_class': FioJobTest_t0006,
623 'job': 't0006-82af2a7c.fio',
624 'success': SUCCESS_DEFAULT,
625 'pre_job': None,
626 'pre_success': None,
627 'output_format': 'json',
628 'requirements': [Requirements.linux, Requirements.libaio],
629 },
630 {
631 'test_id': 7,
632 'test_class': FioJobTest_t0007,
633 'job': 't0007-37cf9e3c.fio',
634 'success': SUCCESS_DEFAULT,
635 'pre_job': None,
636 'pre_success': None,
637 'output_format': 'json',
638 'requirements': [],
639 },
640 {
641 'test_id': 8,
642 'test_class': FioJobTest_t0008,
643 'job': 't0008-ae2fafc8.fio',
644 'success': SUCCESS_DEFAULT,
645 'pre_job': None,
646 'pre_success': None,
647 'output_format': 'json',
648 'requirements': [],
649 },
650 {
651 'test_id': 9,
652 'test_class': FioJobTest_t0009,
653 'job': 't0009-f8b0bd10.fio',
654 'success': SUCCESS_DEFAULT,
655 'pre_job': None,
656 'pre_success': None,
657 'output_format': 'json',
658 'requirements': [Requirements.not_macos,
659 Requirements.cpucount4],
660 # mac os does not support CPU affinity
661 },
662 {
663 'test_id': 10,
664 'test_class': FioJobTest,
665 'job': 't0010-b7aae4ba.fio',
666 'success': SUCCESS_DEFAULT,
667 'pre_job': None,
668 'pre_success': None,
669 'requirements': [],
670 },
671 {
672 'test_id': 11,
673 'test_class': FioJobTest_t0011,
674 'job': 't0011-5d2788d5.fio',
675 'success': SUCCESS_DEFAULT,
676 'pre_job': None,
677 'pre_success': None,
678 'output_format': 'json',
679 'requirements': [],
680 },
681 {
682 'test_id': 1000,
683 'test_class': FioExeTest,
684 'exe': 't/axmap',
685 'parameters': None,
686 'success': SUCCESS_DEFAULT,
687 'requirements': [],
688 },
689 {
690 'test_id': 1001,
691 'test_class': FioExeTest,
692 'exe': 't/ieee754',
693 'parameters': None,
694 'success': SUCCESS_DEFAULT,
695 'requirements': [],
696 },
697 {
698 'test_id': 1002,
699 'test_class': FioExeTest,
700 'exe': 't/lfsr-test',
701 'parameters': ['0xFFFFFF', '0', '0', 'verify'],
702 'success': SUCCESS_STDERR,
703 'requirements': [],
704 },
705 {
706 'test_id': 1003,
707 'test_class': FioExeTest,
708 'exe': 't/readonly.py',
709 'parameters': ['-f', '{fio_path}'],
710 'success': SUCCESS_DEFAULT,
711 'requirements': [],
712 },
713 {
714 'test_id': 1004,
715 'test_class': FioExeTest,
716 'exe': 't/steadystate_tests.py',
717 'parameters': ['{fio_path}'],
718 'success': SUCCESS_DEFAULT,
719 'requirements': [],
720 },
721 {
722 'test_id': 1005,
723 'test_class': FioExeTest,
724 'exe': 't/stest',
725 'parameters': None,
726 'success': SUCCESS_STDERR,
727 'requirements': [],
728 },
729 {
730 'test_id': 1006,
731 'test_class': FioExeTest,
732 'exe': 't/strided.py',
733 'parameters': ['{fio_path}'],
734 'success': SUCCESS_DEFAULT,
735 'requirements': [],
736 },
737 {
738 'test_id': 1007,
739 'test_class': FioExeTest,
740 'exe': 't/zbd/run-tests-against-regular-nullb',
741 'parameters': None,
742 'success': SUCCESS_DEFAULT,
743 'requirements': [Requirements.linux, Requirements.zbd,
744 Requirements.root],
745 },
746 {
747 'test_id': 1008,
748 'test_class': FioExeTest,
749 'exe': 't/zbd/run-tests-against-zoned-nullb',
750 'parameters': None,
751 'success': SUCCESS_DEFAULT,
752 'requirements': [Requirements.linux, Requirements.zbd,
753 Requirements.root, Requirements.zoned_nullb],
754 },
755 {
756 'test_id': 1009,
757 'test_class': FioExeTest,
758 'exe': 'unittests/unittest',
759 'parameters': None,
760 'success': SUCCESS_DEFAULT,
761 'requirements': [Requirements.unittests],
762 },
763 {
764 'test_id': 1010,
765 'test_class': FioExeTest,
766 'exe': 't/latency_percentiles.py',
767 'parameters': ['-f', '{fio_path}'],
768 'success': SUCCESS_DEFAULT,
769 'requirements': [],
770 },
8403eca6
VF
771 {
772 'test_id': 1011,
773 'test_class': FioExeTest,
774 'exe': 't/jsonplus2csv_test.py',
775 'parameters': ['-f', '{fio_path}'],
776 'success': SUCCESS_DEFAULT,
777 'requirements': [],
778 },
df1eaa36
VF
779]
780
781
782def parse_args():
704cc4df
VF
783 """Parse command-line arguments."""
784
df1eaa36
VF
785 parser = argparse.ArgumentParser()
786 parser.add_argument('-r', '--fio-root',
787 help='fio root path')
788 parser.add_argument('-f', '--fio',
789 help='path to fio executable (e.g., ./fio)')
790 parser.add_argument('-a', '--artifact-root',
791 help='artifact root directory')
792 parser.add_argument('-s', '--skip', nargs='+', type=int,
793 help='list of test(s) to skip')
794 parser.add_argument('-o', '--run-only', nargs='+', type=int,
795 help='list of test(s) to run, skipping all others')
6d5470c3
VF
796 parser.add_argument('-d', '--debug', action='store_true',
797 help='provide debug output')
b1bc705e
VF
798 parser.add_argument('-k', '--skip-req', action='store_true',
799 help='skip requirements checking')
df1eaa36
VF
800 args = parser.parse_args()
801
802 return args
803
804
805def main():
704cc4df
VF
806 """Entry point."""
807
df1eaa36 808 args = parse_args()
6d5470c3
VF
809 if args.debug:
810 logging.basicConfig(level=logging.DEBUG)
811 else:
812 logging.basicConfig(level=logging.INFO)
813
df1eaa36
VF
814 if args.fio_root:
815 fio_root = args.fio_root
816 else:
6d5470c3
VF
817 fio_root = str(Path(__file__).absolute().parent.parent)
818 print("fio root is %s" % fio_root)
df1eaa36
VF
819
820 if args.fio:
821 fio_path = args.fio
822 else:
742b8799
VF
823 if platform.system() == "Windows":
824 fio_exe = "fio.exe"
825 else:
826 fio_exe = "fio"
827 fio_path = os.path.join(fio_root, fio_exe)
6d5470c3
VF
828 print("fio path is %s" % fio_path)
829 if not shutil.which(fio_path):
830 print("Warning: fio executable not found")
df1eaa36
VF
831
832 artifact_root = args.artifact_root if args.artifact_root else \
833 "fio-test-{0}".format(time.strftime("%Y%m%d-%H%M%S"))
834 os.mkdir(artifact_root)
835 print("Artifact directory is %s" % artifact_root)
836
b1bc705e
VF
837 if not args.skip_req:
838 req = Requirements(fio_root)
839
df1eaa36
VF
840 passed = 0
841 failed = 0
842 skipped = 0
843
844 for config in TEST_LIST:
845 if (args.skip and config['test_id'] in args.skip) or \
846 (args.run_only and config['test_id'] not in args.run_only):
847 skipped = skipped + 1
b1bc705e 848 print("Test {0} SKIPPED (User request)".format(config['test_id']))
df1eaa36
VF
849 continue
850
851 if issubclass(config['test_class'], FioJobTest):
852 if config['pre_job']:
853 fio_pre_job = os.path.join(fio_root, 't', 'jobs',
854 config['pre_job'])
855 else:
856 fio_pre_job = None
857 if config['pre_success']:
858 fio_pre_success = config['pre_success']
859 else:
860 fio_pre_success = None
861 if 'output_format' in config:
862 output_format = config['output_format']
863 else:
864 output_format = 'normal'
865 test = config['test_class'](
866 fio_path,
867 os.path.join(fio_root, 't', 'jobs', config['job']),
868 config['success'],
869 fio_pre_job=fio_pre_job,
870 fio_pre_success=fio_pre_success,
871 output_format=output_format)
872 elif issubclass(config['test_class'], FioExeTest):
873 exe_path = os.path.join(fio_root, config['exe'])
874 if config['parameters']:
875 parameters = [p.format(fio_path=fio_path) for p in config['parameters']]
876 else:
877 parameters = None
742b8799
VF
878 if Path(exe_path).suffix == '.py' and platform.system() == "Windows":
879 if parameters:
880 parameters.insert(0, exe_path)
881 else:
882 parameters = [exe_path]
883 exe_path = "python.exe"
df1eaa36
VF
884 test = config['test_class'](exe_path, parameters,
885 config['success'])
886 else:
887 print("Test {0} FAILED: unable to process test config".format(config['test_id']))
888 failed = failed + 1
889 continue
890
b1bc705e 891 if not args.skip_req:
704cc4df 892 reqs_met = True
b1bc705e 893 for req in config['requirements']:
704cc4df
VF
894 reqs_met, reason = req()
895 logging.debug("Test %d: Requirement '%s' met? %s", config['test_id'], reason,
896 reqs_met)
897 if not reqs_met:
b1bc705e 898 break
704cc4df 899 if not reqs_met:
b1bc705e
VF
900 print("Test {0} SKIPPED ({1})".format(config['test_id'], reason))
901 skipped = skipped + 1
902 continue
903
df1eaa36
VF
904 test.setup(artifact_root, config['test_id'])
905 test.run()
906 test.check_result()
907 if test.passed:
908 result = "PASSED"
909 passed = passed + 1
910 else:
911 result = "FAILED: {0}".format(test.failure_reason)
912 failed = failed + 1
15a73987
VF
913 contents, _ = FioJobTest.get_file(test.stderr_file)
914 logging.debug("Test %d: stderr:\n%s", config['test_id'], contents)
915 contents, _ = FioJobTest.get_file(test.stdout_file)
916 logging.debug("Test %d: stdout:\n%s", config['test_id'], contents)
df1eaa36
VF
917 print("Test {0} {1}".format(config['test_id'], result))
918
919 print("{0} test(s) passed, {1} failed, {2} skipped".format(passed, failed, skipped))
920
921 sys.exit(failed)
922
923
924if __name__ == '__main__':
925 main()