Merge branch 'security-token' of https://github.com/sfc-gh-rnarubin/fio
[fio.git] / t / run-fio-tests.py
... / ...
CommitLineData
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
17# # git clone git://git.kernel.dk/fio.git
18# # cd fio
19# # make -j
20# # python3 t/run-fio-tests.py
21#
22#
23# REQUIREMENTS
24# - Python 3.5 (subprocess.run)
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)
42#
43
44import os
45import sys
46import time
47import shutil
48import logging
49import argparse
50import re
51from pathlib import Path
52from statsmodels.sandbox.stats.runs import runstest_1samp
53from fiotestlib import FioExeTest, FioJobFileTest, run_fio_tests
54from fiotestcommon import *
55
56
57class FioJobFileTest_t0005(FioJobFileTest):
58 """Test consists of fio test job t0005
59 Confirm that read['io_kbytes'] == write['io_kbytes'] == 102400"""
60
61 def check_result(self):
62 super().check_result()
63
64 if not self.passed:
65 return
66
67 if self.json_data['jobs'][0]['read']['io_kbytes'] != 102400:
68 self.failure_reason = f"{self.failure_reason} bytes read mismatch,"
69 self.passed = False
70 if self.json_data['jobs'][0]['write']['io_kbytes'] != 102400:
71 self.failure_reason = f"{self.failure_reason} bytes written mismatch,"
72 self.passed = False
73
74
75class FioJobFileTest_t0006(FioJobFileTest):
76 """Test consists of fio test job t0006
77 Confirm that read['io_kbytes'] ~ 2*write['io_kbytes']"""
78
79 def check_result(self):
80 super().check_result()
81
82 if not self.passed:
83 return
84
85 ratio = self.json_data['jobs'][0]['read']['io_kbytes'] \
86 / self.json_data['jobs'][0]['write']['io_kbytes']
87 logging.debug("Test %d: ratio: %f", self.testnum, ratio)
88 if ratio < 1.99 or ratio > 2.01:
89 self.failure_reason = f"{self.failure_reason} read/write ratio mismatch,"
90 self.passed = False
91
92
93class FioJobFileTest_t0007(FioJobFileTest):
94 """Test consists of fio test job t0007
95 Confirm that read['io_kbytes'] = 87040"""
96
97 def check_result(self):
98 super().check_result()
99
100 if not self.passed:
101 return
102
103 if self.json_data['jobs'][0]['read']['io_kbytes'] != 87040:
104 self.failure_reason = f"{self.failure_reason} bytes read mismatch,"
105 self.passed = False
106
107
108class FioJobFileTest_t0008(FioJobFileTest):
109 """Test consists of fio test job t0008
110 Confirm that read['io_kbytes'] = 32768 and that
111 write['io_kbytes'] ~ 16384
112
113 This is a 50/50 seq read/write workload. Since fio flips a coin to
114 determine whether to issue a read or a write, total bytes written will not
115 be exactly 16384K. But total bytes read will be exactly 32768K because
116 reads will include the initial phase as well as the verify phase where all
117 the blocks originally written will be read."""
118
119 def check_result(self):
120 super().check_result()
121
122 if not self.passed:
123 return
124
125 ratio = self.json_data['jobs'][0]['write']['io_kbytes'] / 16384
126 logging.debug("Test %d: ratio: %f", self.testnum, ratio)
127
128 if ratio < 0.97 or ratio > 1.03:
129 self.failure_reason = f"{self.failure_reason} bytes written mismatch,"
130 self.passed = False
131 if self.json_data['jobs'][0]['read']['io_kbytes'] != 32768:
132 self.failure_reason = f"{self.failure_reason} bytes read mismatch,"
133 self.passed = False
134
135
136class FioJobFileTest_t0009(FioJobFileTest):
137 """Test consists of fio test job t0009
138 Confirm that runtime >= 60s"""
139
140 def check_result(self):
141 super().check_result()
142
143 if not self.passed:
144 return
145
146 logging.debug('Test %d: elapsed: %d', self.testnum, self.json_data['jobs'][0]['elapsed'])
147
148 if self.json_data['jobs'][0]['elapsed'] < 60:
149 self.failure_reason = f"{self.failure_reason} elapsed time mismatch,"
150 self.passed = False
151
152
153class FioJobFileTest_t0012(FioJobFileTest):
154 """Test consists of fio test job t0012
155 Confirm ratios of job iops are 1:5:10
156 job1,job2,job3 respectively"""
157
158 def check_result(self):
159 super().check_result()
160
161 if not self.passed:
162 return
163
164 iops_files = []
165 for i in range(1, 4):
166 filename = os.path.join(self.paths['test_dir'], "{0}_iops.{1}.log".format(os.path.basename(
167 self.fio_job), i))
168 file_data = self.get_file_fail(filename)
169 if not file_data:
170 return
171
172 iops_files.append(file_data.splitlines())
173
174 # there are 9 samples for job1 and job2, 4 samples for job3
175 iops1 = 0.0
176 iops2 = 0.0
177 iops3 = 0.0
178 for i in range(9):
179 iops1 = iops1 + float(iops_files[0][i].split(',')[1])
180 iops2 = iops2 + float(iops_files[1][i].split(',')[1])
181 iops3 = iops3 + float(iops_files[2][i].split(',')[1])
182
183 ratio1 = iops3/iops2
184 ratio2 = iops3/iops1
185 logging.debug("sample {0}: job1 iops={1} job2 iops={2} job3 iops={3} " \
186 "job3/job2={4:.3f} job3/job1={5:.3f}".format(i, iops1, iops2, iops3, ratio1,
187 ratio2))
188
189 # test job1 and job2 succeeded to recalibrate
190 if ratio1 < 1 or ratio1 > 3 or ratio2 < 7 or ratio2 > 13:
191 self.failure_reason += " iops ratio mismatch iops1={0} iops2={1} iops3={2} " \
192 "expected r1~2 r2~10 got r1={3:.3f} r2={4:.3f},".format(iops1, iops2, iops3,
193 ratio1, ratio2)
194 self.passed = False
195 return
196
197
198class FioJobFileTest_t0014(FioJobFileTest):
199 """Test consists of fio test job t0014
200 Confirm that job1_iops / job2_iops ~ 1:2 for entire duration
201 and that job1_iops / job3_iops ~ 1:3 for first half of duration.
202
203 The test is about making sure the flow feature can
204 re-calibrate the activity dynamically"""
205
206 def check_result(self):
207 super().check_result()
208
209 if not self.passed:
210 return
211
212 iops_files = []
213 for i in range(1, 4):
214 filename = os.path.join(self.paths['test_dir'], "{0}_iops.{1}.log".format(os.path.basename(
215 self.fio_job), i))
216 file_data = self.get_file_fail(filename)
217 if not file_data:
218 return
219
220 iops_files.append(file_data.splitlines())
221
222 # there are 9 samples for job1 and job2, 4 samples for job3
223 iops1 = 0.0
224 iops2 = 0.0
225 iops3 = 0.0
226 for i in range(9):
227 if i < 4:
228 iops3 = iops3 + float(iops_files[2][i].split(',')[1])
229 elif i == 4:
230 ratio1 = iops1 / iops2
231 ratio2 = iops1 / iops3
232
233
234 if ratio1 < 0.43 or ratio1 > 0.57 or ratio2 < 0.21 or ratio2 > 0.45:
235 self.failure_reason += " iops ratio mismatch iops1={0} iops2={1} iops3={2} " \
236 "expected r1~0.5 r2~0.33 got r1={3:.3f} r2={4:.3f},".format(
237 iops1, iops2, iops3, ratio1, ratio2)
238 self.passed = False
239
240 iops1 = iops1 + float(iops_files[0][i].split(',')[1])
241 iops2 = iops2 + float(iops_files[1][i].split(',')[1])
242
243 ratio1 = iops1/iops2
244 ratio2 = iops1/iops3
245 logging.debug("sample {0}: job1 iops={1} job2 iops={2} job3 iops={3} " \
246 "job1/job2={4:.3f} job1/job3={5:.3f}".format(i, iops1, iops2, iops3,
247 ratio1, ratio2))
248
249 # test job1 and job2 succeeded to recalibrate
250 if ratio1 < 0.43 or ratio1 > 0.57:
251 self.failure_reason += " iops ratio mismatch iops1={0} iops2={1} expected ratio~0.5 " \
252 "got ratio={2:.3f},".format(iops1, iops2, ratio1)
253 self.passed = False
254 return
255
256
257class FioJobFileTest_t0015(FioJobFileTest):
258 """Test consists of fio test jobs t0015 and t0016
259 Confirm that mean(slat) + mean(clat) = mean(tlat)"""
260
261 def check_result(self):
262 super().check_result()
263
264 if not self.passed:
265 return
266
267 slat = self.json_data['jobs'][0]['read']['slat_ns']['mean']
268 clat = self.json_data['jobs'][0]['read']['clat_ns']['mean']
269 tlat = self.json_data['jobs'][0]['read']['lat_ns']['mean']
270 logging.debug('Test %d: slat %f, clat %f, tlat %f', self.testnum, slat, clat, tlat)
271
272 if abs(slat + clat - tlat) > 1:
273 self.failure_reason = "{0} slat {1} + clat {2} = {3} != tlat {4},".format(
274 self.failure_reason, slat, clat, slat+clat, tlat)
275 self.passed = False
276
277
278class FioJobFileTest_t0019(FioJobFileTest):
279 """Test consists of fio test job t0019
280 Confirm that all offsets were touched sequentially"""
281
282 def check_result(self):
283 super().check_result()
284
285 bw_log_filename = os.path.join(self.paths['test_dir'], "test_bw.log")
286 file_data = self.get_file_fail(bw_log_filename)
287 if not file_data:
288 return
289
290 log_lines = file_data.split('\n')
291
292 prev = -4096
293 for line in log_lines:
294 if len(line.strip()) == 0:
295 continue
296 cur = int(line.split(',')[4])
297 if cur - prev != 4096:
298 self.passed = False
299 self.failure_reason = f"offsets {prev}, {cur} not sequential"
300 return
301 prev = cur
302
303 if cur/4096 != 255:
304 self.passed = False
305 self.failure_reason = f"unexpected last offset {cur}"
306
307
308class FioJobFileTest_t0020(FioJobFileTest):
309 """Test consists of fio test jobs t0020 and t0021
310 Confirm that almost all offsets were touched non-sequentially"""
311
312 def check_result(self):
313 super().check_result()
314
315 bw_log_filename = os.path.join(self.paths['test_dir'], "test_bw.log")
316 file_data = self.get_file_fail(bw_log_filename)
317 if not file_data:
318 return
319
320 log_lines = file_data.split('\n')
321
322 offsets = []
323
324 prev = int(log_lines[0].split(',')[4])
325 for line in log_lines[1:]:
326 offsets.append(prev/4096)
327 if len(line.strip()) == 0:
328 continue
329 cur = int(line.split(',')[4])
330 prev = cur
331
332 if len(offsets) != 256:
333 self.passed = False
334 self.failure_reason += f" number of offsets is {len(offsets)} instead of 256"
335
336 for i in range(256):
337 if not i in offsets:
338 self.passed = False
339 self.failure_reason += f" missing offset {i * 4096}"
340
341 (_, p) = runstest_1samp(list(offsets))
342 if p < 0.05:
343 self.passed = False
344 self.failure_reason += f" runs test failed with p = {p}"
345
346
347class FioJobFileTest_t0022(FioJobFileTest):
348 """Test consists of fio test job t0022"""
349
350 def check_result(self):
351 super().check_result()
352
353 bw_log_filename = os.path.join(self.paths['test_dir'], "test_bw.log")
354 file_data = self.get_file_fail(bw_log_filename)
355 if not file_data:
356 return
357
358 log_lines = file_data.split('\n')
359
360 filesize = 1024*1024
361 bs = 4096
362 seq_count = 0
363 offsets = set()
364
365 prev = int(log_lines[0].split(',')[4])
366 for line in log_lines[1:]:
367 offsets.add(prev/bs)
368 if len(line.strip()) == 0:
369 continue
370 cur = int(line.split(',')[4])
371 if cur - prev == bs:
372 seq_count += 1
373 prev = cur
374
375 # 10 is an arbitrary threshold
376 if seq_count > 10:
377 self.passed = False
378 self.failure_reason = f"too many ({seq_count}) consecutive offsets"
379
380 if len(offsets) == filesize/bs:
381 self.passed = False
382 self.failure_reason += " no duplicate offsets found with norandommap=1"
383
384
385class FioJobFileTest_t0023(FioJobFileTest):
386 """Test consists of fio test job t0023 randtrimwrite test."""
387
388 def check_trimwrite(self, filename):
389 """Make sure that trims are followed by writes of the same size at the same offset."""
390
391 bw_log_filename = os.path.join(self.paths['test_dir'], filename)
392 file_data = self.get_file_fail(bw_log_filename)
393 if not file_data:
394 return
395
396 log_lines = file_data.split('\n')
397
398 prev_ddir = 1
399 for line in log_lines:
400 if len(line.strip()) == 0:
401 continue
402 vals = line.split(',')
403 ddir = int(vals[2])
404 bs = int(vals[3])
405 offset = int(vals[4])
406 if prev_ddir == 1:
407 if ddir != 2:
408 self.passed = False
409 self.failure_reason += " {0}: write not preceeded by trim: {1}".format(
410 bw_log_filename, line)
411 break
412 else:
413 if ddir != 1: # pylint: disable=no-else-break
414 self.passed = False
415 self.failure_reason += " {0}: trim not preceeded by write: {1}".format(
416 bw_log_filename, line)
417 break
418 else:
419 if prev_bs != bs:
420 self.passed = False
421 self.failure_reason += " {0}: block size does not match: {1}".format(
422 bw_log_filename, line)
423 break
424
425 if prev_offset != offset:
426 self.passed = False
427 self.failure_reason += " {0}: offset does not match: {1}".format(
428 bw_log_filename, line)
429 break
430
431 prev_ddir = ddir
432 prev_bs = bs
433 prev_offset = offset
434
435
436 def check_all_offsets(self, filename, sectorsize, filesize):
437 """Make sure all offsets were touched."""
438
439 file_data = self.get_file_fail(os.path.join(self.paths['test_dir'], filename))
440 if not file_data:
441 return
442
443 log_lines = file_data.split('\n')
444
445 offsets = set()
446
447 for line in log_lines:
448 if len(line.strip()) == 0:
449 continue
450 vals = line.split(',')
451 bs = int(vals[3])
452 offset = int(vals[4])
453 if offset % sectorsize != 0:
454 self.passed = False
455 self.failure_reason += " {0}: offset {1} not a multiple of sector size {2}".format(
456 filename, offset, sectorsize)
457 break
458 if bs % sectorsize != 0:
459 self.passed = False
460 self.failure_reason += " {0}: block size {1} not a multiple of sector size " \
461 "{2}".format(filename, bs, sectorsize)
462 break
463 for i in range(int(bs/sectorsize)):
464 offsets.add(offset/sectorsize + i)
465
466 if len(offsets) != filesize/sectorsize:
467 self.passed = False
468 self.failure_reason += " {0}: only {1} offsets touched; expected {2}".format(
469 filename, len(offsets), filesize/sectorsize)
470 else:
471 logging.debug("%s: %d sectors touched", filename, len(offsets))
472
473
474 def check_result(self):
475 super().check_result()
476
477 filesize = 1024*1024
478
479 self.check_trimwrite("basic_bw.log")
480 self.check_trimwrite("bs_bw.log")
481 self.check_trimwrite("bsrange_bw.log")
482 self.check_trimwrite("bssplit_bw.log")
483 self.check_trimwrite("basic_no_rm_bw.log")
484 self.check_trimwrite("bs_no_rm_bw.log")
485 self.check_trimwrite("bsrange_no_rm_bw.log")
486 self.check_trimwrite("bssplit_no_rm_bw.log")
487
488 self.check_all_offsets("basic_bw.log", 4096, filesize)
489 self.check_all_offsets("bs_bw.log", 8192, filesize)
490 self.check_all_offsets("bsrange_bw.log", 512, filesize)
491 self.check_all_offsets("bssplit_bw.log", 512, filesize)
492
493
494class FioJobFileTest_t0024(FioJobFileTest_t0023):
495 """Test consists of fio test job t0024 trimwrite test."""
496
497 def check_result(self):
498 # call FioJobFileTest_t0023's parent to skip checks done by t0023
499 super(FioJobFileTest_t0023, self).check_result()
500
501 filesize = 1024*1024
502
503 self.check_trimwrite("basic_bw.log")
504 self.check_trimwrite("bs_bw.log")
505 self.check_trimwrite("bsrange_bw.log")
506 self.check_trimwrite("bssplit_bw.log")
507
508 self.check_all_offsets("basic_bw.log", 4096, filesize)
509 self.check_all_offsets("bs_bw.log", 8192, filesize)
510 self.check_all_offsets("bsrange_bw.log", 512, filesize)
511 self.check_all_offsets("bssplit_bw.log", 512, filesize)
512
513
514class FioJobFileTest_t0025(FioJobFileTest):
515 """Test experimental verify read backs written data pattern."""
516 def check_result(self):
517 super().check_result()
518
519 if not self.passed:
520 return
521
522 if self.json_data['jobs'][0]['read']['io_kbytes'] != 128:
523 self.passed = False
524
525class FioJobFileTest_t0027(FioJobFileTest):
526 def setup(self, *args, **kws):
527 super().setup(*args, **kws)
528 self.pattern_file = os.path.join(self.paths['test_dir'], "t0027.pattern")
529 self.output_file = os.path.join(self.paths['test_dir'], "t0027file")
530 self.pattern = os.urandom(16 << 10)
531 with open(self.pattern_file, "wb") as f:
532 f.write(self.pattern)
533
534 def check_result(self):
535 super().check_result()
536
537 if not self.passed:
538 return
539
540 with open(self.output_file, "rb") as f:
541 data = f.read()
542
543 if data != self.pattern:
544 self.passed = False
545
546class FioJobFileTest_t0029(FioJobFileTest):
547 """Test loops option works with read-verify workload."""
548 def check_result(self):
549 super().check_result()
550
551 if not self.passed:
552 return
553
554 if self.json_data['jobs'][1]['read']['io_kbytes'] != 8:
555 self.passed = False
556
557class FioJobFileTest_LogFileFormat(FioJobFileTest):
558 """Test log file format"""
559 def setup(self, *args, **kws):
560 super().setup(*args, **kws)
561 self.patterns = {}
562
563 def check_result(self):
564 super().check_result()
565
566 if not self.passed:
567 return
568
569 for logfile in self.patterns.keys():
570 file_path = os.path.join(self.paths['test_dir'], logfile)
571 with open(file_path, "r") as f:
572 line = f.readline()
573 if not re.match(self.patterns[logfile], line):
574 self.passed = False
575 self.failure_reason = "wrong log file format: " + logfile
576 return
577
578class FioJobFileTest_t0033(FioJobFileTest_LogFileFormat):
579 """Test log file format"""
580 def setup(self, *args, **kws):
581 super().setup(*args, **kws)
582 self.patterns = {
583 'log_bw.1.log': '\\d+, \\d+, \\d+, \\d+, 0x[\\da-f]+\\n',
584 'log_clat.2.log': '\\d+, \\d+, \\d+, \\d+, 0, \\d+\\n',
585 'log_iops.3.log': '\\d+, \\d+, \\d+, \\d+, \\d+, 0x[\\da-f]+\\n',
586 'log_iops.4.log': '\\d+, \\d+, \\d+, \\d+, 0, 0, \\d+\\n',
587 }
588
589class FioJobFileTest_t0034(FioJobFileTest_LogFileFormat):
590 """Test log file format"""
591 def setup(self, *args, **kws):
592 super().setup(*args, **kws)
593 self.patterns = {
594 'log_clat.1.log': '\\d+, \\d+, \\d+, \\d+, \\d+, \\d+, \\d+\\n',
595 'log_slat.1.log': '\\d+, \\d+, \\d+, \\d+, \\d+, \\d+, \\d+\\n',
596 'log_lat.1.log': '\\d+, \\d+, \\d+, \\d+, \\d+, \\d+, 0\\n',
597 'log_clat.2.log': '\\d+, \\d+, \\d+, \\d+, 0, 0, \\d+, 0\\n',
598 'log_bw.3.log': '\\d+, \\d+, \\d+, \\d+, \\d+, \\d+, 0\\n',
599 'log_iops.3.log': '\\d+, \\d+, \\d+, \\d+, \\d+, \\d+, 0\\n',
600 }
601
602class FioJobFileTest_iops_rate(FioJobFileTest):
603 """Test consists of fio test job t0011
604 Confirm that job0 iops == 1000
605 and that job1_iops / job0_iops ~ 8
606 With two runs of fio-3.16 I observed a ratio of 8.3"""
607
608 def check_result(self):
609 super().check_result()
610
611 if not self.passed:
612 return
613
614 iops1 = self.json_data['jobs'][0]['read']['iops']
615 logging.debug("Test %d: iops1: %f", self.testnum, iops1)
616 iops2 = self.json_data['jobs'][1]['read']['iops']
617 logging.debug("Test %d: iops2: %f", self.testnum, iops2)
618 ratio = iops2 / iops1
619 logging.debug("Test %d: ratio: %f", self.testnum, ratio)
620
621 if iops1 < 950 or iops1 > 1050:
622 self.failure_reason = f"{self.failure_reason} iops value mismatch,"
623 self.passed = False
624
625 if ratio < 6 or ratio > 10:
626 self.failure_reason = f"{self.failure_reason} iops ratio mismatch,"
627 self.passed = False
628
629
630TEST_LIST = [
631 {
632 'test_id': 1,
633 'test_class': FioJobFileTest,
634 'job': 't0001-52c58027.fio',
635 'success': SUCCESS_DEFAULT,
636 'pre_job': None,
637 'pre_success': None,
638 'requirements': [],
639 },
640 {
641 'test_id': 2,
642 'test_class': FioJobFileTest,
643 'job': 't0002-13af05ae-post.fio',
644 'success': SUCCESS_DEFAULT,
645 'pre_job': 't0002-13af05ae-pre.fio',
646 'pre_success': None,
647 'requirements': [Requirements.linux, Requirements.libaio],
648 },
649 {
650 'test_id': 3,
651 'test_class': FioJobFileTest,
652 'job': 't0003-0ae2c6e1-post.fio',
653 'success': SUCCESS_NONZERO,
654 'pre_job': 't0003-0ae2c6e1-pre.fio',
655 'pre_success': SUCCESS_DEFAULT,
656 'requirements': [Requirements.linux, Requirements.libaio],
657 },
658 {
659 'test_id': 4,
660 'test_class': FioJobFileTest,
661 'job': 't0004-8a99fdf6.fio',
662 'success': SUCCESS_DEFAULT,
663 'pre_job': None,
664 'pre_success': None,
665 'requirements': [Requirements.linux, Requirements.libaio],
666 },
667 {
668 'test_id': 5,
669 'test_class': FioJobFileTest_t0005,
670 'job': 't0005-f7078f7b.fio',
671 'success': SUCCESS_DEFAULT,
672 'pre_job': None,
673 'pre_success': None,
674 'output_format': 'json',
675 'requirements': [Requirements.not_windows],
676 },
677 {
678 'test_id': 6,
679 'test_class': FioJobFileTest_t0006,
680 'job': 't0006-82af2a7c.fio',
681 'success': SUCCESS_DEFAULT,
682 'pre_job': None,
683 'pre_success': None,
684 'output_format': 'json',
685 'requirements': [Requirements.linux, Requirements.libaio],
686 },
687 {
688 'test_id': 7,
689 'test_class': FioJobFileTest_t0007,
690 'job': 't0007-37cf9e3c.fio',
691 'success': SUCCESS_DEFAULT,
692 'pre_job': None,
693 'pre_success': None,
694 'output_format': 'json',
695 'requirements': [],
696 },
697 {
698 'test_id': 8,
699 'test_class': FioJobFileTest_t0008,
700 'job': 't0008-ae2fafc8.fio',
701 'success': SUCCESS_DEFAULT,
702 'pre_job': None,
703 'pre_success': None,
704 'output_format': 'json',
705 'requirements': [],
706 },
707 {
708 'test_id': 9,
709 'test_class': FioJobFileTest_t0009,
710 'job': 't0009-f8b0bd10.fio',
711 'success': SUCCESS_DEFAULT,
712 'pre_job': None,
713 'pre_success': None,
714 'output_format': 'json',
715 'requirements': [Requirements.not_macos,
716 Requirements.cpucount4],
717 # mac os does not support CPU affinity
718 },
719 {
720 'test_id': 10,
721 'test_class': FioJobFileTest,
722 'job': 't0010-b7aae4ba.fio',
723 'success': SUCCESS_DEFAULT,
724 'pre_job': None,
725 'pre_success': None,
726 'requirements': [],
727 },
728 {
729 'test_id': 11,
730 'test_class': FioJobFileTest_iops_rate,
731 'job': 't0011-5d2788d5.fio',
732 'success': SUCCESS_DEFAULT,
733 'pre_job': None,
734 'pre_success': None,
735 'output_format': 'json',
736 'requirements': [],
737 },
738 {
739 'test_id': 12,
740 'test_class': FioJobFileTest_t0012,
741 'job': 't0012.fio',
742 'success': SUCCESS_DEFAULT,
743 'pre_job': None,
744 'pre_success': None,
745 'output_format': 'json',
746 'requirements': [],
747 },
748 {
749 'test_id': 13,
750 'test_class': FioJobFileTest,
751 'job': 't0013.fio',
752 'success': SUCCESS_DEFAULT,
753 'pre_job': None,
754 'pre_success': None,
755 'output_format': 'json',
756 'requirements': [],
757 },
758 {
759 'test_id': 14,
760 'test_class': FioJobFileTest_t0014,
761 'job': 't0014.fio',
762 'success': SUCCESS_DEFAULT,
763 'pre_job': None,
764 'pre_success': None,
765 'output_format': 'json',
766 'requirements': [],
767 },
768 {
769 'test_id': 15,
770 'test_class': FioJobFileTest_t0015,
771 'job': 't0015-4e7e7898.fio',
772 'success': SUCCESS_DEFAULT,
773 'pre_job': None,
774 'pre_success': None,
775 'output_format': 'json',
776 'requirements': [Requirements.linux, Requirements.libaio],
777 },
778 {
779 'test_id': 16,
780 'test_class': FioJobFileTest_t0015,
781 'job': 't0016-d54ae22.fio',
782 'success': SUCCESS_DEFAULT,
783 'pre_job': None,
784 'pre_success': None,
785 'output_format': 'json',
786 'requirements': [],
787 },
788 {
789 'test_id': 17,
790 'test_class': FioJobFileTest_t0015,
791 'job': 't0017.fio',
792 'success': SUCCESS_DEFAULT,
793 'pre_job': None,
794 'pre_success': None,
795 'output_format': 'json',
796 'requirements': [Requirements.not_windows],
797 },
798 {
799 'test_id': 18,
800 'test_class': FioJobFileTest,
801 'job': 't0018.fio',
802 'success': SUCCESS_DEFAULT,
803 'pre_job': None,
804 'pre_success': None,
805 'requirements': [Requirements.linux, Requirements.io_uring],
806 },
807 {
808 'test_id': 19,
809 'test_class': FioJobFileTest_t0019,
810 'job': 't0019.fio',
811 'success': SUCCESS_DEFAULT,
812 'pre_job': None,
813 'pre_success': None,
814 'requirements': [],
815 },
816 {
817 'test_id': 20,
818 'test_class': FioJobFileTest_t0020,
819 'job': 't0020.fio',
820 'success': SUCCESS_DEFAULT,
821 'pre_job': None,
822 'pre_success': None,
823 'requirements': [],
824 },
825 {
826 'test_id': 21,
827 'test_class': FioJobFileTest_t0020,
828 'job': 't0021.fio',
829 'success': SUCCESS_DEFAULT,
830 'pre_job': None,
831 'pre_success': None,
832 'requirements': [],
833 },
834 {
835 'test_id': 22,
836 'test_class': FioJobFileTest_t0022,
837 'job': 't0022.fio',
838 'success': SUCCESS_DEFAULT,
839 'pre_job': None,
840 'pre_success': None,
841 'requirements': [],
842 },
843 {
844 'test_id': 23,
845 'test_class': FioJobFileTest_t0023,
846 'job': 't0023.fio',
847 'success': SUCCESS_DEFAULT,
848 'pre_job': None,
849 'pre_success': None,
850 'requirements': [],
851 },
852 {
853 'test_id': 24,
854 'test_class': FioJobFileTest_t0024,
855 'job': 't0024.fio',
856 'success': SUCCESS_DEFAULT,
857 'pre_job': None,
858 'pre_success': None,
859 'requirements': [],
860 },
861 {
862 'test_id': 25,
863 'test_class': FioJobFileTest_t0025,
864 'job': 't0025.fio',
865 'success': SUCCESS_DEFAULT,
866 'pre_job': None,
867 'pre_success': None,
868 'output_format': 'json',
869 'requirements': [],
870 },
871 {
872 'test_id': 26,
873 'test_class': FioJobFileTest,
874 'job': 't0026.fio',
875 'success': SUCCESS_DEFAULT,
876 'pre_job': None,
877 'pre_success': None,
878 'requirements': [Requirements.not_windows],
879 },
880 {
881 'test_id': 27,
882 'test_class': FioJobFileTest_t0027,
883 'job': 't0027.fio',
884 'success': SUCCESS_DEFAULT,
885 'pre_job': None,
886 'pre_success': None,
887 'requirements': [],
888 },
889 {
890 'test_id': 28,
891 'test_class': FioJobFileTest,
892 'job': 't0028-c6cade16.fio',
893 'success': SUCCESS_DEFAULT,
894 'pre_job': None,
895 'pre_success': None,
896 'requirements': [],
897 },
898 {
899 'test_id': 29,
900 'test_class': FioJobFileTest_t0029,
901 'job': 't0029.fio',
902 'success': SUCCESS_DEFAULT,
903 'pre_job': None,
904 'pre_success': None,
905 'output_format': 'json',
906 'requirements': [],
907 },
908 {
909 'test_id': 30,
910 'test_class': FioJobFileTest,
911 'job': 't0030.fio',
912 'success': SUCCESS_DEFAULT,
913 'pre_job': None,
914 'pre_success': None,
915 'parameters': ['--bandwidth-log'],
916 'requirements': [],
917 },
918 {
919 'test_id': 31,
920 'test_class': FioJobFileTest,
921 'job': 't0031.fio',
922 'success': SUCCESS_DEFAULT,
923 'pre_job': 't0031-pre.fio',
924 'pre_success': SUCCESS_DEFAULT,
925 'requirements': [Requirements.linux, Requirements.libaio],
926 },
927 {
928 'test_id': 33,
929 'test_class': FioJobFileTest_t0033,
930 'job': 't0033.fio',
931 'success': SUCCESS_DEFAULT,
932 'pre_job': None,
933 'pre_success': None,
934 'requirements': [Requirements.linux, Requirements.libaio],
935 },
936 {
937 'test_id': 34,
938 'test_class': FioJobFileTest_t0034,
939 'job': 't0034.fio',
940 'success': SUCCESS_DEFAULT,
941 'pre_job': None,
942 'pre_success': None,
943 'requirements': [Requirements.linux, Requirements.libaio],
944 },
945 {
946 'test_id': 35,
947 'test_class': FioJobFileTest,
948 'job': 't0035.fio',
949 'success': SUCCESS_DEFAULT,
950 'pre_job': None,
951 'pre_success': None,
952 'requirements': [],
953 },
954 {
955 'test_id': 36,
956 'test_class': FioJobFileTest,
957 'job': 't0036-post.fio',
958 'success': SUCCESS_DEFAULT,
959 'pre_job': 't0036-pre.fio',
960 'pre_success': SUCCESS_DEFAULT,
961 'requirements': [],
962 },
963 {
964 'test_id': 37,
965 'test_class': FioJobFileTest,
966 'job': 't0037-post.fio',
967 'success': SUCCESS_DEFAULT,
968 'pre_job': 't0037-pre.fio',
969 'pre_success': SUCCESS_DEFAULT,
970 'requirements': [Requirements.linux, Requirements.libaio],
971 },
972 {
973 'test_id': 1000,
974 'test_class': FioExeTest,
975 'exe': 't/axmap',
976 'parameters': None,
977 'success': SUCCESS_DEFAULT,
978 'requirements': [],
979 },
980 {
981 'test_id': 1001,
982 'test_class': FioExeTest,
983 'exe': 't/ieee754',
984 'parameters': None,
985 'success': SUCCESS_DEFAULT,
986 'requirements': [],
987 },
988 {
989 'test_id': 1002,
990 'test_class': FioExeTest,
991 'exe': 't/lfsr-test',
992 'parameters': ['0xFFFFFF', '0', '0', 'verify'],
993 'success': SUCCESS_STDERR,
994 'requirements': [],
995 },
996 {
997 'test_id': 1003,
998 'test_class': FioExeTest,
999 'exe': 't/readonly.py',
1000 'parameters': ['-f', '{fio_path}'],
1001 'success': SUCCESS_DEFAULT,
1002 'requirements': [],
1003 },
1004 {
1005 'test_id': 1004,
1006 'test_class': FioExeTest,
1007 'exe': 't/steadystate_tests.py',
1008 'parameters': ['{fio_path}'],
1009 'success': SUCCESS_DEFAULT,
1010 'requirements': [],
1011 },
1012 {
1013 'test_id': 1005,
1014 'test_class': FioExeTest,
1015 'exe': 't/stest',
1016 'parameters': None,
1017 'success': SUCCESS_STDERR,
1018 'requirements': [],
1019 },
1020 {
1021 'test_id': 1006,
1022 'test_class': FioExeTest,
1023 'exe': 't/strided.py',
1024 'parameters': ['--fio', '{fio_path}'],
1025 'success': SUCCESS_DEFAULT,
1026 'requirements': [],
1027 },
1028 {
1029 'test_id': 1007,
1030 'test_class': FioExeTest,
1031 'exe': 't/zbd/run-tests-against-nullb',
1032 'parameters': ['-s', '1'],
1033 'success': SUCCESS_DEFAULT,
1034 'requirements': [Requirements.linux, Requirements.zbd,
1035 Requirements.root],
1036 },
1037 {
1038 'test_id': 1008,
1039 'test_class': FioExeTest,
1040 'exe': 't/zbd/run-tests-against-nullb',
1041 'parameters': ['-s', '2'],
1042 'success': SUCCESS_DEFAULT,
1043 'requirements': [Requirements.linux, Requirements.zbd,
1044 Requirements.root, Requirements.zoned_nullb],
1045 },
1046 {
1047 'test_id': 1009,
1048 'test_class': FioExeTest,
1049 'exe': 'unittests/unittest',
1050 'parameters': None,
1051 'success': SUCCESS_DEFAULT,
1052 'requirements': [Requirements.unittests],
1053 },
1054 {
1055 'test_id': 1010,
1056 'test_class': FioExeTest,
1057 'exe': 't/latency_percentiles.py',
1058 'parameters': ['-f', '{fio_path}'],
1059 'success': SUCCESS_DEFAULT,
1060 'requirements': [],
1061 },
1062 {
1063 'test_id': 1011,
1064 'test_class': FioExeTest,
1065 'exe': 't/jsonplus2csv_test.py',
1066 'parameters': ['-f', '{fio_path}'],
1067 'success': SUCCESS_DEFAULT,
1068 'requirements': [],
1069 },
1070 {
1071 'test_id': 1012,
1072 'test_class': FioExeTest,
1073 'exe': 't/log_compression.py',
1074 'parameters': ['-f', '{fio_path}'],
1075 'success': SUCCESS_DEFAULT,
1076 'requirements': [],
1077 },
1078 {
1079 'test_id': 1013,
1080 'test_class': FioExeTest,
1081 'exe': 't/random_seed.py',
1082 'parameters': ['-f', '{fio_path}'],
1083 'success': SUCCESS_DEFAULT,
1084 'requirements': [],
1085 },
1086 {
1087 'test_id': 1014,
1088 'test_class': FioExeTest,
1089 'exe': 't/nvmept.py',
1090 'parameters': ['-f', '{fio_path}', '--dut', '{nvmecdev}'],
1091 'success': SUCCESS_DEFAULT,
1092 'requirements': [Requirements.linux, Requirements.nvmecdev],
1093 },
1094 {
1095 'test_id': 1015,
1096 'test_class': FioExeTest,
1097 'exe': 't/nvmept_trim.py',
1098 'parameters': ['-f', '{fio_path}', '--dut', '{nvmecdev}'],
1099 'success': SUCCESS_DEFAULT,
1100 'requirements': [Requirements.linux, Requirements.nvmecdev],
1101 },
1102 {
1103 'test_id': 1016,
1104 'test_class': FioExeTest,
1105 'exe': 't/client_server.py',
1106 'parameters': ['-f', '{fio_path}'],
1107 'success': SUCCESS_DEFAULT,
1108 'requirements': [Requirements.linux],
1109 },
1110 {
1111 'test_id': 1017,
1112 'test_class': FioExeTest,
1113 'exe': 't/verify.py',
1114 'parameters': ['-f', '{fio_path}'],
1115 'success': SUCCESS_LONG,
1116 'requirements': [],
1117 },
1118 {
1119 'test_id': 1018,
1120 'test_class': FioExeTest,
1121 'exe': 't/verify-trim.py',
1122 'parameters': ['-f', '{fio_path}'],
1123 'success': SUCCESS_DEFAULT,
1124 'requirements': [Requirements.linux],
1125 },
1126]
1127
1128
1129def parse_args():
1130 """Parse command-line arguments."""
1131
1132 parser = argparse.ArgumentParser()
1133 parser.add_argument('-r', '--fio-root',
1134 help='fio root path')
1135 parser.add_argument('-f', '--fio',
1136 help='path to fio executable (e.g., ./fio)')
1137 parser.add_argument('-a', '--artifact-root',
1138 help='artifact root directory')
1139 parser.add_argument('-s', '--skip', nargs='+', type=int,
1140 help='list of test(s) to skip')
1141 parser.add_argument('-o', '--run-only', nargs='+', type=int,
1142 help='list of test(s) to run, skipping all others')
1143 parser.add_argument('-d', '--debug', action='store_true',
1144 help='provide debug output')
1145 parser.add_argument('-k', '--skip-req', action='store_true',
1146 help='skip requirements checking')
1147 parser.add_argument('-p', '--pass-through', action='append',
1148 help='pass-through an argument to an executable test')
1149 parser.add_argument('--nvmecdev', action='store', default=None,
1150 help='NVMe character device for **DESTRUCTIVE** testing (e.g., /dev/ng0n1)')
1151 args = parser.parse_args()
1152
1153 return args
1154
1155
1156def main():
1157 """Entry point."""
1158
1159 args = parse_args()
1160 if args.debug:
1161 logging.basicConfig(level=logging.DEBUG)
1162 else:
1163 logging.basicConfig(level=logging.INFO)
1164
1165 pass_through = {}
1166 if args.pass_through:
1167 for arg in args.pass_through:
1168 if not ':' in arg:
1169 print(f"Invalid --pass-through argument '{arg}'")
1170 print("Syntax for --pass-through is TESTNUMBER:ARGUMENT")
1171 return
1172 split = arg.split(":", 1)
1173 pass_through[int(split[0])] = split[1]
1174 logging.debug("Pass-through arguments: %s", pass_through)
1175
1176 if args.fio_root:
1177 fio_root = args.fio_root
1178 else:
1179 fio_root = str(Path(__file__).absolute().parent.parent)
1180 print(f"fio root is {fio_root}")
1181
1182 if args.fio:
1183 fio_path = args.fio
1184 else:
1185 if platform.system() == "Windows":
1186 fio_exe = "fio.exe"
1187 else:
1188 fio_exe = "fio"
1189 fio_path = os.path.join(fio_root, fio_exe)
1190 print(f"fio path is {fio_path}")
1191 if not shutil.which(fio_path):
1192 print("Warning: fio executable not found")
1193
1194 artifact_root = args.artifact_root if args.artifact_root else \
1195 f"fio-test-{time.strftime('%Y%m%d-%H%M%S')}"
1196 os.mkdir(artifact_root)
1197 print(f"Artifact directory is {artifact_root}")
1198
1199 if not args.skip_req:
1200 Requirements(fio_root, args)
1201
1202 test_env = {
1203 'fio_path': fio_path,
1204 'fio_root': fio_root,
1205 'artifact_root': artifact_root,
1206 'pass_through': pass_through,
1207 }
1208 _, failed, _ = run_fio_tests(TEST_LIST, test_env, args)
1209 sys.exit(failed)
1210
1211
1212if __name__ == '__main__':
1213 main()