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