t/run-fio-tests: address issues identified by pylint
[fio.git] / t / run-fio-tests.py
CommitLineData
df1eaa36
VF
1#!/usr/bin/env python3
2# SPDX-License-Identifier: GPL-2.0-only
3#
4# Copyright (c) 2019 Western Digital Corporation or its affiliates.
5#
6"""
7# run-fio-tests.py
8#
9# Automate running of fio tests
10#
11# USAGE
12# python3 run-fio-tests.py [-r fio-root] [-f fio-path] [-a artifact-root]
13# [--skip # # #...] [--run-only # # #...]
14#
15#
16# EXAMPLE
b048455f 17# # git clone git://git.kernel.dk/fio.git
df1eaa36
VF
18# # cd fio
19# # make -j
20# # python3 t/run-fio-tests.py
21#
22#
23# REQUIREMENTS
b1bc705e 24# - Python 3.5 (subprocess.run)
df1eaa36
VF
25# - Linux (libaio ioengine, zbd tests, etc)
26# - The artifact directory must be on a file system that accepts 512-byte IO
27# (t0002, t0003, t0004).
28# - The artifact directory needs to be on an SSD. Otherwise tests that carry
29# out file-based IO will trigger a timeout (t0006).
30# - 4 CPUs (t0009)
31# - SciPy (steadystate_tests.py)
32# - libzbc (zbd tests)
33# - root privileges (zbd test)
34# - kernel 4.19 or later for zoned null block devices (zbd tests)
35# - CUnit support (unittests)
36#
37"""
38
39#
40# TODO run multiple tests simultaneously
41# TODO Add sgunmap tests (requires SAS SSD)
df1eaa36
VF
42#
43
44import os
45import sys
46import json
47import time
6d5470c3 48import shutil
df1eaa36
VF
49import logging
50import argparse
b1bc705e 51import platform
aa9f2627 52import traceback
df1eaa36 53import subprocess
b1bc705e 54import multiprocessing
df1eaa36 55from pathlib import Path
888dbd62 56from statsmodels.sandbox.stats.runs import runstest_1samp
df1eaa36
VF
57
58
114eadb2 59class FioTest():
df1eaa36
VF
60 """Base for all fio tests."""
61
62 def __init__(self, exe_path, parameters, success):
63 self.exe_path = exe_path
64 self.parameters = parameters
65 self.success = success
66 self.output = {}
67 self.artifact_root = None
68 self.testnum = None
69 self.test_dir = None
70 self.passed = True
71 self.failure_reason = ''
704cc4df
VF
72 self.command_file = None
73 self.stdout_file = None
74 self.stderr_file = None
75 self.exitcode_file = None
df1eaa36
VF
76
77 def setup(self, artifact_root, testnum):
704cc4df
VF
78 """Setup instance variables for test."""
79
df1eaa36
VF
80 self.artifact_root = artifact_root
81 self.testnum = testnum
1b4ba547 82 self.test_dir = os.path.join(artifact_root, f"{testnum:04d}")
df1eaa36
VF
83 if not os.path.exists(self.test_dir):
84 os.mkdir(self.test_dir)
85
86 self.command_file = os.path.join(
704cc4df 87 self.test_dir,
1b4ba547 88 f"{os.path.basename(self.exe_path)}.command")
df1eaa36 89 self.stdout_file = os.path.join(
704cc4df 90 self.test_dir,
1b4ba547 91 f"{os.path.basename(self.exe_path)}.stdout")
df1eaa36 92 self.stderr_file = os.path.join(
704cc4df 93 self.test_dir,
1b4ba547 94 f"{os.path.basename(self.exe_path)}.stderr")
704cc4df
VF
95 self.exitcode_file = os.path.join(
96 self.test_dir,
1b4ba547 97 f"{os.path.basename(self.exe_path)}.exitcode")
df1eaa36
VF
98
99 def run(self):
704cc4df
VF
100 """Run the test."""
101
df1eaa36
VF
102 raise NotImplementedError()
103
104 def check_result(self):
704cc4df
VF
105 """Check test results."""
106
df1eaa36
VF
107 raise NotImplementedError()
108
109
110class FioExeTest(FioTest):
111 """Test consists of an executable binary or script"""
112
113 def __init__(self, exe_path, parameters, success):
114 """Construct a FioExeTest which is a FioTest consisting of an
115 executable binary or script.
116
117 exe_path: location of executable binary or script
118 parameters: list of parameters for executable
119 success: Definition of test success
120 """
121
122 FioTest.__init__(self, exe_path, parameters, success)
123
df1eaa36 124 def run(self):
704cc4df
VF
125 """Execute the binary or script described by this instance."""
126
58a77d2a 127 command = [self.exe_path] + self.parameters
df1eaa36 128 command_file = open(self.command_file, "w+")
1b4ba547 129 command_file.write(f"{command}\n")
df1eaa36
VF
130 command_file.close()
131
132 stdout_file = open(self.stdout_file, "w+")
133 stderr_file = open(self.stderr_file, "w+")
704cc4df 134 exitcode_file = open(self.exitcode_file, "w+")
df1eaa36 135 try:
b048455f 136 proc = None
df1eaa36
VF
137 # Avoid using subprocess.run() here because when a timeout occurs,
138 # fio will be stopped with SIGKILL. This does not give fio a
139 # chance to clean up and means that child processes may continue
140 # running and submitting IO.
141 proc = subprocess.Popen(command,
142 stdout=stdout_file,
143 stderr=stderr_file,
144 cwd=self.test_dir,
145 universal_newlines=True)
146 proc.communicate(timeout=self.success['timeout'])
1b4ba547 147 exitcode_file.write(f'{proc.returncode}\n')
704cc4df 148 logging.debug("Test %d: return code: %d", self.testnum, proc.returncode)
df1eaa36
VF
149 self.output['proc'] = proc
150 except subprocess.TimeoutExpired:
151 proc.terminate()
152 proc.communicate()
153 assert proc.poll()
154 self.output['failure'] = 'timeout'
155 except Exception:
b048455f
VF
156 if proc:
157 if not proc.poll():
158 proc.terminate()
159 proc.communicate()
df1eaa36
VF
160 self.output['failure'] = 'exception'
161 self.output['exc_info'] = sys.exc_info()
162 finally:
163 stdout_file.close()
164 stderr_file.close()
704cc4df 165 exitcode_file.close()
df1eaa36
VF
166
167 def check_result(self):
704cc4df
VF
168 """Check results of test run."""
169
df1eaa36
VF
170 if 'proc' not in self.output:
171 if self.output['failure'] == 'timeout':
1b4ba547 172 self.failure_reason = f"{self.failure_reason} timeout,"
df1eaa36
VF
173 else:
174 assert self.output['failure'] == 'exception'
175 self.failure_reason = '{0} exception: {1}, {2}'.format(
704cc4df
VF
176 self.failure_reason, self.output['exc_info'][0],
177 self.output['exc_info'][1])
df1eaa36
VF
178
179 self.passed = False
180 return
181
182 if 'zero_return' in self.success:
183 if self.success['zero_return']:
184 if self.output['proc'].returncode != 0:
185 self.passed = False
1b4ba547 186 self.failure_reason = f"{self.failure_reason} non-zero return code,"
df1eaa36
VF
187 else:
188 if self.output['proc'].returncode == 0:
1b4ba547 189 self.failure_reason = f"{self.failure_reason} zero return code,"
df1eaa36
VF
190 self.passed = False
191
6d5470c3 192 stderr_size = os.path.getsize(self.stderr_file)
df1eaa36 193 if 'stderr_empty' in self.success:
df1eaa36
VF
194 if self.success['stderr_empty']:
195 if stderr_size != 0:
1b4ba547 196 self.failure_reason = f"{self.failure_reason} stderr not empty,"
df1eaa36
VF
197 self.passed = False
198 else:
199 if stderr_size == 0:
1b4ba547 200 self.failure_reason = f"{self.failure_reason} stderr empty,"
df1eaa36
VF
201 self.passed = False
202
203
204class FioJobTest(FioExeTest):
205 """Test consists of a fio job"""
206
207 def __init__(self, fio_path, fio_job, success, fio_pre_job=None,
208 fio_pre_success=None, output_format="normal"):
209 """Construct a FioJobTest which is a FioExeTest consisting of a
210 single fio job file with an optional setup step.
211
212 fio_path: location of fio executable
213 fio_job: location of fio job file
214 success: Definition of test success
215 fio_pre_job: fio job for preconditioning
216 fio_pre_success: Definition of test success for fio precon job
217 output_format: normal (default), json, jsonplus, or terse
218 """
219
220 self.fio_job = fio_job
221 self.fio_pre_job = fio_pre_job
222 self.fio_pre_success = fio_pre_success if fio_pre_success else success
223 self.output_format = output_format
224 self.precon_failed = False
225 self.json_data = None
1b4ba547 226 self.fio_output = f"{os.path.basename(self.fio_job)}.output"
df1eaa36 227 self.fio_args = [
771dbb52 228 "--max-jobs=16",
1b4ba547
VF
229 f"--output-format={self.output_format}",
230 f"--output={self.fio_output}",
df1eaa36
VF
231 self.fio_job,
232 ]
233 FioExeTest.__init__(self, fio_path, self.fio_args, success)
234
235 def setup(self, artifact_root, testnum):
704cc4df
VF
236 """Setup instance variables for fio job test."""
237
1b4ba547 238 super().setup(artifact_root, testnum)
df1eaa36
VF
239
240 self.command_file = os.path.join(
704cc4df 241 self.test_dir,
1b4ba547 242 f"{os.path.basename(self.fio_job)}.command")
df1eaa36 243 self.stdout_file = os.path.join(
704cc4df 244 self.test_dir,
1b4ba547 245 f"{os.path.basename(self.fio_job)}.stdout")
df1eaa36 246 self.stderr_file = os.path.join(
704cc4df 247 self.test_dir,
1b4ba547 248 f"{os.path.basename(self.fio_job)}.stderr")
704cc4df
VF
249 self.exitcode_file = os.path.join(
250 self.test_dir,
1b4ba547 251 f"{os.path.basename(self.fio_job)}.exitcode")
df1eaa36
VF
252
253 def run_pre_job(self):
704cc4df
VF
254 """Run fio job precondition step."""
255
df1eaa36
VF
256 precon = FioJobTest(self.exe_path, self.fio_pre_job,
257 self.fio_pre_success,
258 output_format=self.output_format)
259 precon.setup(self.artifact_root, self.testnum)
260 precon.run()
261 precon.check_result()
262 self.precon_failed = not precon.passed
263 self.failure_reason = precon.failure_reason
264
265 def run(self):
704cc4df
VF
266 """Run fio job test."""
267
df1eaa36
VF
268 if self.fio_pre_job:
269 self.run_pre_job()
270
271 if not self.precon_failed:
1b4ba547 272 super().run()
df1eaa36 273 else:
704cc4df 274 logging.debug("Test %d: precondition step failed", self.testnum)
df1eaa36 275
15a73987
VF
276 @classmethod
277 def get_file(cls, filename):
278 """Safely read a file."""
279 file_data = ''
280 success = True
281
282 try:
283 with open(filename, "r") as output_file:
284 file_data = output_file.read()
285 except OSError:
286 success = False
287
288 return file_data, success
289
0f5234b4
VF
290 def get_file_fail(self, filename):
291 """Safely read a file and fail the test upon error."""
292 file_data = None
293
294 try:
295 with open(filename, "r") as output_file:
296 file_data = output_file.read()
297 except OSError:
1b4ba547 298 self.failure_reason += f" unable to read file {filename}"
0f5234b4
VF
299 self.passed = False
300
301 return file_data
302
df1eaa36 303 def check_result(self):
704cc4df
VF
304 """Check fio job results."""
305
df1eaa36
VF
306 if self.precon_failed:
307 self.passed = False
1b4ba547 308 self.failure_reason = f"{self.failure_reason} precondition step failed,"
df1eaa36
VF
309 return
310
1b4ba547 311 super().check_result()
df1eaa36 312
6d5470c3
VF
313 if not self.passed:
314 return
315
704cc4df 316 if 'json' not in self.output_format:
742b8799
VF
317 return
318
0f5234b4
VF
319 file_data = self.get_file_fail(os.path.join(self.test_dir, self.fio_output))
320 if not file_data:
742b8799
VF
321 return
322
323 #
324 # Sometimes fio informational messages are included at the top of the
325 # JSON output, especially under Windows. Try to decode output as JSON
db2637ec 326 # data, skipping everything until the first {
742b8799
VF
327 #
328 lines = file_data.splitlines()
db2637ec
VF
329 file_data = '\n'.join(lines[lines.index("{"):])
330 try:
331 self.json_data = json.loads(file_data)
332 except json.JSONDecodeError:
1b4ba547 333 self.failure_reason = f"{self.failure_reason} unable to decode JSON data,"
db2637ec 334 self.passed = False
df1eaa36
VF
335
336
337class FioJobTest_t0005(FioJobTest):
338 """Test consists of fio test job t0005
339 Confirm that read['io_kbytes'] == write['io_kbytes'] == 102400"""
340
341 def check_result(self):
1b4ba547 342 super().check_result()
df1eaa36
VF
343
344 if not self.passed:
345 return
346
347 if self.json_data['jobs'][0]['read']['io_kbytes'] != 102400:
1b4ba547 348 self.failure_reason = f"{self.failure_reason} bytes read mismatch,"
df1eaa36
VF
349 self.passed = False
350 if self.json_data['jobs'][0]['write']['io_kbytes'] != 102400:
1b4ba547 351 self.failure_reason = f"{self.failure_reason} bytes written mismatch,"
df1eaa36
VF
352 self.passed = False
353
354
355class FioJobTest_t0006(FioJobTest):
356 """Test consists of fio test job t0006
357 Confirm that read['io_kbytes'] ~ 2*write['io_kbytes']"""
358
359 def check_result(self):
1b4ba547 360 super().check_result()
df1eaa36
VF
361
362 if not self.passed:
363 return
364
365 ratio = self.json_data['jobs'][0]['read']['io_kbytes'] \
366 / self.json_data['jobs'][0]['write']['io_kbytes']
704cc4df 367 logging.debug("Test %d: ratio: %f", self.testnum, ratio)
df1eaa36 368 if ratio < 1.99 or ratio > 2.01:
1b4ba547 369 self.failure_reason = f"{self.failure_reason} read/write ratio mismatch,"
df1eaa36
VF
370 self.passed = False
371
372
373class FioJobTest_t0007(FioJobTest):
374 """Test consists of fio test job t0007
375 Confirm that read['io_kbytes'] = 87040"""
376
377 def check_result(self):
1b4ba547 378 super().check_result()
df1eaa36
VF
379
380 if not self.passed:
381 return
382
383 if self.json_data['jobs'][0]['read']['io_kbytes'] != 87040:
1b4ba547 384 self.failure_reason = f"{self.failure_reason} bytes read mismatch,"
df1eaa36
VF
385 self.passed = False
386
387
388class FioJobTest_t0008(FioJobTest):
389 """Test consists of fio test job t0008
390 Confirm that read['io_kbytes'] = 32768 and that
77c758db 391 write['io_kbytes'] ~ 16384
df1eaa36 392
77c758db
VF
393 This is a 50/50 seq read/write workload. Since fio flips a coin to
394 determine whether to issue a read or a write, total bytes written will not
395 be exactly 16384K. But total bytes read will be exactly 32768K because
396 reads will include the initial phase as well as the verify phase where all
397 the blocks originally written will be read."""
df1eaa36
VF
398
399 def check_result(self):
1b4ba547 400 super().check_result()
df1eaa36
VF
401
402 if not self.passed:
403 return
404
77c758db 405 ratio = self.json_data['jobs'][0]['write']['io_kbytes'] / 16384
704cc4df 406 logging.debug("Test %d: ratio: %f", self.testnum, ratio)
df1eaa36 407
77c758db 408 if ratio < 0.97 or ratio > 1.03:
1b4ba547 409 self.failure_reason = f"{self.failure_reason} bytes written mismatch,"
df1eaa36
VF
410 self.passed = False
411 if self.json_data['jobs'][0]['read']['io_kbytes'] != 32768:
1b4ba547 412 self.failure_reason = f"{self.failure_reason} bytes read mismatch,"
df1eaa36
VF
413 self.passed = False
414
415
416class FioJobTest_t0009(FioJobTest):
417 """Test consists of fio test job t0009
418 Confirm that runtime >= 60s"""
419
420 def check_result(self):
1b4ba547 421 super().check_result()
df1eaa36
VF
422
423 if not self.passed:
424 return
425
704cc4df 426 logging.debug('Test %d: elapsed: %d', self.testnum, self.json_data['jobs'][0]['elapsed'])
df1eaa36
VF
427
428 if self.json_data['jobs'][0]['elapsed'] < 60:
1b4ba547 429 self.failure_reason = f"{self.failure_reason} elapsed time mismatch,"
df1eaa36
VF
430 self.passed = False
431
432
d4e74fda
DB
433class FioJobTest_t0012(FioJobTest):
434 """Test consists of fio test job t0012
435 Confirm ratios of job iops are 1:5:10
436 job1,job2,job3 respectively"""
437
438 def check_result(self):
1b4ba547 439 super().check_result()
d4e74fda
DB
440
441 if not self.passed:
442 return
443
444 iops_files = []
114eadb2
VF
445 for i in range(1, 4):
446 filename = os.path.join(self.test_dir, "{0}_iops.{1}.log".format(os.path.basename(
447 self.fio_job), i))
0f5234b4
VF
448 file_data = self.get_file_fail(filename)
449 if not file_data:
d4e74fda
DB
450 return
451
452 iops_files.append(file_data.splitlines())
453
454 # there are 9 samples for job1 and job2, 4 samples for job3
455 iops1 = 0.0
456 iops2 = 0.0
457 iops3 = 0.0
458 for i in range(9):
459 iops1 = iops1 + float(iops_files[0][i].split(',')[1])
460 iops2 = iops2 + float(iops_files[1][i].split(',')[1])
461 iops3 = iops3 + float(iops_files[2][i].split(',')[1])
462
463 ratio1 = iops3/iops2
464 ratio2 = iops3/iops1
114eadb2
VF
465 logging.debug("sample {0}: job1 iops={1} job2 iops={2} job3 iops={3} " \
466 "job3/job2={4:.3f} job3/job1={5:.3f}".format(i, iops1, iops2, iops3, ratio1,
467 ratio2))
d4e74fda
DB
468
469 # test job1 and job2 succeeded to recalibrate
470 if ratio1 < 1 or ratio1 > 3 or ratio2 < 7 or ratio2 > 13:
114eadb2
VF
471 self.failure_reason += " iops ratio mismatch iops1={0} iops2={1} iops3={2} " \
472 "expected r1~2 r2~10 got r1={3:.3f} r2={4:.3f},".format(iops1, iops2, iops3,
473 ratio1, ratio2)
d4e74fda
DB
474 self.passed = False
475 return
476
477
478class FioJobTest_t0014(FioJobTest):
479 """Test consists of fio test job t0014
480 Confirm that job1_iops / job2_iops ~ 1:2 for entire duration
481 and that job1_iops / job3_iops ~ 1:3 for first half of duration.
482
483 The test is about making sure the flow feature can
484 re-calibrate the activity dynamically"""
485
486 def check_result(self):
1b4ba547 487 super().check_result()
d4e74fda
DB
488
489 if not self.passed:
490 return
491
492 iops_files = []
114eadb2
VF
493 for i in range(1, 4):
494 filename = os.path.join(self.test_dir, "{0}_iops.{1}.log".format(os.path.basename(
495 self.fio_job), i))
0f5234b4
VF
496 file_data = self.get_file_fail(filename)
497 if not file_data:
d4e74fda
DB
498 return
499
500 iops_files.append(file_data.splitlines())
501
502 # there are 9 samples for job1 and job2, 4 samples for job3
503 iops1 = 0.0
504 iops2 = 0.0
505 iops3 = 0.0
506 for i in range(9):
507 if i < 4:
508 iops3 = iops3 + float(iops_files[2][i].split(',')[1])
509 elif i == 4:
510 ratio1 = iops1 / iops2
511 ratio2 = iops1 / iops3
512
513
514 if ratio1 < 0.43 or ratio1 > 0.57 or ratio2 < 0.21 or ratio2 > 0.45:
114eadb2
VF
515 self.failure_reason += " iops ratio mismatch iops1={0} iops2={1} iops3={2} " \
516 "expected r1~0.5 r2~0.33 got r1={3:.3f} r2={4:.3f},".format(
517 iops1, iops2, iops3, ratio1, ratio2)
d4e74fda
DB
518 self.passed = False
519
520 iops1 = iops1 + float(iops_files[0][i].split(',')[1])
521 iops2 = iops2 + float(iops_files[1][i].split(',')[1])
522
523 ratio1 = iops1/iops2
524 ratio2 = iops1/iops3
114eadb2
VF
525 logging.debug("sample {0}: job1 iops={1} job2 iops={2} job3 iops={3} " \
526 "job1/job2={4:.3f} job1/job3={5:.3f}".format(i, iops1, iops2, iops3,
527 ratio1, ratio2))
d4e74fda
DB
528
529 # test job1 and job2 succeeded to recalibrate
530 if ratio1 < 0.43 or ratio1 > 0.57:
114eadb2
VF
531 self.failure_reason += " iops ratio mismatch iops1={0} iops2={1} expected ratio~0.5 " \
532 "got ratio={2:.3f},".format(iops1, iops2, ratio1)
d4e74fda
DB
533 self.passed = False
534 return
535
536
60ebb939 537class FioJobTest_t0015(FioJobTest):
de31fe9a 538 """Test consists of fio test jobs t0015 and t0016
60ebb939
VF
539 Confirm that mean(slat) + mean(clat) = mean(tlat)"""
540
541 def check_result(self):
1b4ba547 542 super().check_result()
60ebb939
VF
543
544 if not self.passed:
545 return
546
547 slat = self.json_data['jobs'][0]['read']['slat_ns']['mean']
548 clat = self.json_data['jobs'][0]['read']['clat_ns']['mean']
549 tlat = self.json_data['jobs'][0]['read']['lat_ns']['mean']
550 logging.debug('Test %d: slat %f, clat %f, tlat %f', self.testnum, slat, clat, tlat)
551
552 if abs(slat + clat - tlat) > 1:
553 self.failure_reason = "{0} slat {1} + clat {2} = {3} != tlat {4},".format(
554 self.failure_reason, slat, clat, slat+clat, tlat)
555 self.passed = False
556
557
ef54f290
VF
558class FioJobTest_t0019(FioJobTest):
559 """Test consists of fio test job t0019
560 Confirm that all offsets were touched sequentially"""
561
562 def check_result(self):
1b4ba547 563 super().check_result()
ef54f290
VF
564
565 bw_log_filename = os.path.join(self.test_dir, "test_bw.log")
0f5234b4
VF
566 file_data = self.get_file_fail(bw_log_filename)
567 if not file_data:
114eadb2
VF
568 return
569
ef54f290
VF
570 log_lines = file_data.split('\n')
571
572 prev = -4096
573 for line in log_lines:
574 if len(line.strip()) == 0:
575 continue
576 cur = int(line.split(',')[4])
577 if cur - prev != 4096:
578 self.passed = False
1b4ba547 579 self.failure_reason = f"offsets {prev}, {cur} not sequential"
ef54f290
VF
580 return
581 prev = cur
582
583 if cur/4096 != 255:
584 self.passed = False
1b4ba547 585 self.failure_reason = f"unexpected last offset {cur}"
ef54f290
VF
586
587
588class FioJobTest_t0020(FioJobTest):
eb40b275 589 """Test consists of fio test jobs t0020 and t0021
ef54f290
VF
590 Confirm that almost all offsets were touched non-sequentially"""
591
592 def check_result(self):
1b4ba547 593 super().check_result()
ef54f290
VF
594
595 bw_log_filename = os.path.join(self.test_dir, "test_bw.log")
0f5234b4
VF
596 file_data = self.get_file_fail(bw_log_filename)
597 if not file_data:
114eadb2
VF
598 return
599
ef54f290
VF
600 log_lines = file_data.split('\n')
601
888dbd62 602 offsets = []
ef54f290
VF
603
604 prev = int(log_lines[0].split(',')[4])
605 for line in log_lines[1:]:
888dbd62 606 offsets.append(prev/4096)
ef54f290
VF
607 if len(line.strip()) == 0:
608 continue
609 cur = int(line.split(',')[4])
ef54f290
VF
610 prev = cur
611
ef54f290
VF
612 if len(offsets) != 256:
613 self.passed = False
1b4ba547 614 self.failure_reason += f" number of offsets is {len(offsets)} instead of 256"
ef54f290
VF
615
616 for i in range(256):
617 if not i in offsets:
618 self.passed = False
1b4ba547 619 self.failure_reason += f" missing offset {i * 4096}"
ef54f290 620
1b4ba547 621 (_, p) = runstest_1samp(list(offsets))
888dbd62
VF
622 if p < 0.05:
623 self.passed = False
624 self.failure_reason += f" runs test failed with p = {p}"
625
ef54f290 626
eb40b275
VF
627class FioJobTest_t0022(FioJobTest):
628 """Test consists of fio test job t0022"""
629
630 def check_result(self):
1b4ba547 631 super().check_result()
eb40b275
VF
632
633 bw_log_filename = os.path.join(self.test_dir, "test_bw.log")
0f5234b4
VF
634 file_data = self.get_file_fail(bw_log_filename)
635 if not file_data:
114eadb2
VF
636 return
637
eb40b275
VF
638 log_lines = file_data.split('\n')
639
640 filesize = 1024*1024
641 bs = 4096
642 seq_count = 0
643 offsets = set()
644
645 prev = int(log_lines[0].split(',')[4])
646 for line in log_lines[1:]:
647 offsets.add(prev/bs)
648 if len(line.strip()) == 0:
649 continue
650 cur = int(line.split(',')[4])
651 if cur - prev == bs:
652 seq_count += 1
653 prev = cur
654
655 # 10 is an arbitrary threshold
656 if seq_count > 10:
657 self.passed = False
1b4ba547 658 self.failure_reason = f"too many ({seq_count}) consecutive offsets"
eb40b275
VF
659
660 if len(offsets) == filesize/bs:
661 self.passed = False
114eadb2 662 self.failure_reason += " no duplicate offsets found with norandommap=1"
eb40b275
VF
663
664
c37183f8 665class FioJobTest_t0023(FioJobTest):
21a20229 666 """Test consists of fio test job t0023 randtrimwrite test."""
c37183f8 667
1fb78294
VF
668 def check_trimwrite(self, filename):
669 """Make sure that trims are followed by writes of the same size at the same offset."""
670
c37183f8 671 bw_log_filename = os.path.join(self.test_dir, filename)
0f5234b4
VF
672 file_data = self.get_file_fail(bw_log_filename)
673 if not file_data:
114eadb2
VF
674 return
675
c37183f8
VF
676 log_lines = file_data.split('\n')
677
678 prev_ddir = 1
679 for line in log_lines:
680 if len(line.strip()) == 0:
681 continue
682 vals = line.split(',')
683 ddir = int(vals[2])
684 bs = int(vals[3])
685 offset = int(vals[4])
686 if prev_ddir == 1:
687 if ddir != 2:
688 self.passed = False
21a20229
VF
689 self.failure_reason += " {0}: write not preceeded by trim: {1}".format(
690 bw_log_filename, line)
c37183f8
VF
691 break
692 else:
1b4ba547 693 if ddir != 1: # pylint: disable=no-else-break
c37183f8 694 self.passed = False
21a20229
VF
695 self.failure_reason += " {0}: trim not preceeded by write: {1}".format(
696 bw_log_filename, line)
c37183f8
VF
697 break
698 else:
699 if prev_bs != bs:
700 self.passed = False
21a20229
VF
701 self.failure_reason += " {0}: block size does not match: {1}".format(
702 bw_log_filename, line)
c37183f8 703 break
1b4ba547 704
c37183f8
VF
705 if prev_offset != offset:
706 self.passed = False
21a20229
VF
707 self.failure_reason += " {0}: offset does not match: {1}".format(
708 bw_log_filename, line)
c37183f8 709 break
1b4ba547 710
c37183f8
VF
711 prev_ddir = ddir
712 prev_bs = bs
713 prev_offset = offset
714
715
9e83aa16 716 def check_all_offsets(self, filename, sectorsize, filesize):
21a20229
VF
717 """Make sure all offsets were touched."""
718
0f5234b4
VF
719 file_data = self.get_file_fail(os.path.join(self.test_dir, filename))
720 if not file_data:
9e83aa16
VF
721 return
722
723 log_lines = file_data.split('\n')
724
725 offsets = set()
726
727 for line in log_lines:
728 if len(line.strip()) == 0:
729 continue
730 vals = line.split(',')
731 bs = int(vals[3])
732 offset = int(vals[4])
733 if offset % sectorsize != 0:
734 self.passed = False
21a20229
VF
735 self.failure_reason += " {0}: offset {1} not a multiple of sector size {2}".format(
736 filename, offset, sectorsize)
737 break
9e83aa16
VF
738 if bs % sectorsize != 0:
739 self.passed = False
21a20229
VF
740 self.failure_reason += " {0}: block size {1} not a multiple of sector size " \
741 "{2}".format(filename, bs, sectorsize)
742 break
9e83aa16
VF
743 for i in range(int(bs/sectorsize)):
744 offsets.add(offset/sectorsize + i)
745
746 if len(offsets) != filesize/sectorsize:
747 self.passed = False
21a20229
VF
748 self.failure_reason += " {0}: only {1} offsets touched; expected {2}".format(
749 filename, len(offsets), filesize/sectorsize)
9e83aa16 750 else:
21a20229 751 logging.debug("%s: %d sectors touched", filename, len(offsets))
9e83aa16
VF
752
753
c37183f8 754 def check_result(self):
1b4ba547 755 super().check_result()
c37183f8 756
9e83aa16
VF
757 filesize = 1024*1024
758
1fb78294
VF
759 self.check_trimwrite("basic_bw.log")
760 self.check_trimwrite("bs_bw.log")
761 self.check_trimwrite("bsrange_bw.log")
762 self.check_trimwrite("bssplit_bw.log")
763 self.check_trimwrite("basic_no_rm_bw.log")
764 self.check_trimwrite("bs_no_rm_bw.log")
765 self.check_trimwrite("bsrange_no_rm_bw.log")
766 self.check_trimwrite("bssplit_no_rm_bw.log")
c37183f8 767
9e83aa16
VF
768 self.check_all_offsets("basic_bw.log", 4096, filesize)
769 self.check_all_offsets("bs_bw.log", 8192, filesize)
770 self.check_all_offsets("bsrange_bw.log", 512, filesize)
771 self.check_all_offsets("bssplit_bw.log", 512, filesize)
c37183f8
VF
772
773
2d5c4600
VF
774class FioJobTest_t0024(FioJobTest_t0023):
775 """Test consists of fio test job t0024 trimwrite test."""
776
777 def check_result(self):
778 # call FioJobTest_t0023's parent to skip checks done by t0023
779 super(FioJobTest_t0023, self).check_result()
780
781 filesize = 1024*1024
782
783 self.check_trimwrite("basic_bw.log")
784 self.check_trimwrite("bs_bw.log")
785 self.check_trimwrite("bsrange_bw.log")
786 self.check_trimwrite("bssplit_bw.log")
787
788 self.check_all_offsets("basic_bw.log", 4096, filesize)
789 self.check_all_offsets("bs_bw.log", 8192, filesize)
790 self.check_all_offsets("bsrange_bw.log", 512, filesize)
791 self.check_all_offsets("bssplit_bw.log", 512, filesize)
792
793
d661fb18
SK
794class FioJobTest_t0025(FioJobTest):
795 """Test experimental verify read backs written data pattern."""
796 def check_result(self):
1b4ba547 797 super().check_result()
d661fb18
SK
798
799 if not self.passed:
800 return
801
802 if self.json_data['jobs'][0]['read']['io_kbytes'] != 128:
803 self.passed = False
804
ede04c27
LG
805class FioJobTest_t0027(FioJobTest):
806 def setup(self, *args, **kws):
1b4ba547 807 super().setup(*args, **kws)
ede04c27
LG
808 self.pattern_file = os.path.join(self.test_dir, "t0027.pattern")
809 self.output_file = os.path.join(self.test_dir, "t0027file")
810 self.pattern = os.urandom(16 << 10)
811 with open(self.pattern_file, "wb") as f:
812 f.write(self.pattern)
813
814 def check_result(self):
1b4ba547 815 super().check_result()
ede04c27
LG
816
817 if not self.passed:
818 return
819
820 with open(self.output_file, "rb") as f:
821 data = f.read()
822
823 if data != self.pattern:
824 self.passed = False
d661fb18 825
0a602473 826class FioJobTest_iops_rate(FioJobTest):
12b609b5 827 """Test consists of fio test job t0011
df1eaa36
VF
828 Confirm that job0 iops == 1000
829 and that job1_iops / job0_iops ~ 8
830 With two runs of fio-3.16 I observed a ratio of 8.3"""
831
832 def check_result(self):
1b4ba547 833 super().check_result()
df1eaa36
VF
834
835 if not self.passed:
836 return
837
838 iops1 = self.json_data['jobs'][0]['read']['iops']
7ddc4ed1 839 logging.debug("Test %d: iops1: %f", self.testnum, iops1)
df1eaa36 840 iops2 = self.json_data['jobs'][1]['read']['iops']
7ddc4ed1 841 logging.debug("Test %d: iops2: %f", self.testnum, iops2)
df1eaa36 842 ratio = iops2 / iops1
704cc4df 843 logging.debug("Test %d: ratio: %f", self.testnum, ratio)
df1eaa36 844
0a9b8988 845 if iops1 < 950 or iops1 > 1050:
1b4ba547 846 self.failure_reason = f"{self.failure_reason} iops value mismatch,"
df1eaa36
VF
847 self.passed = False
848
d4e74fda 849 if ratio < 6 or ratio > 10:
1b4ba547 850 self.failure_reason = f"{self.failure_reason} iops ratio mismatch,"
df1eaa36
VF
851 self.passed = False
852
853
114eadb2 854class Requirements():
b1bc705e
VF
855 """Requirements consists of multiple run environment characteristics.
856 These are to determine if a particular test can be run"""
857
858 _linux = False
859 _libaio = False
a2947c33 860 _io_uring = False
b1bc705e
VF
861 _zbd = False
862 _root = False
863 _zoned_nullb = False
864 _not_macos = False
c58b33b4 865 _not_windows = False
b1bc705e
VF
866 _unittests = False
867 _cpucount4 = False
2128f93a 868 _nvmecdev = False
b1bc705e 869
2128f93a 870 def __init__(self, fio_root, args):
b1bc705e 871 Requirements._not_macos = platform.system() != "Darwin"
c58b33b4 872 Requirements._not_windows = platform.system() != "Windows"
b1bc705e
VF
873 Requirements._linux = platform.system() == "Linux"
874
875 if Requirements._linux:
15a73987
VF
876 config_file = os.path.join(fio_root, "config-host.h")
877 contents, success = FioJobTest.get_file(config_file)
878 if not success:
1b4ba547 879 print(f"Unable to open {config_file} to check requirements")
b1bc705e
VF
880 Requirements._zbd = True
881 else:
b7694961 882 Requirements._zbd = "CONFIG_HAS_BLKZONED" in contents
b1bc705e
VF
883 Requirements._libaio = "CONFIG_LIBAIO" in contents
884
a2947c33
VF
885 contents, success = FioJobTest.get_file("/proc/kallsyms")
886 if not success:
887 print("Unable to open '/proc/kallsyms' to probe for io_uring support")
888 else:
889 Requirements._io_uring = "io_uring_setup" in contents
890
1b4ba547 891 Requirements._root = os.geteuid() == 0
b1bc705e 892 if Requirements._zbd and Requirements._root:
8854e368
VF
893 try:
894 subprocess.run(["modprobe", "null_blk"],
895 stdout=subprocess.PIPE,
896 stderr=subprocess.PIPE)
897 if os.path.exists("/sys/module/null_blk/parameters/zoned"):
898 Requirements._zoned_nullb = True
899 except Exception:
900 pass
b1bc705e 901
742b8799
VF
902 if platform.system() == "Windows":
903 utest_exe = "unittest.exe"
904 else:
905 utest_exe = "unittest"
906 unittest_path = os.path.join(fio_root, "unittests", utest_exe)
b1bc705e
VF
907 Requirements._unittests = os.path.exists(unittest_path)
908
909 Requirements._cpucount4 = multiprocessing.cpu_count() >= 4
2128f93a
VF
910 Requirements._nvmecdev = args.nvmecdev
911
912 req_list = [
913 Requirements.linux,
914 Requirements.libaio,
915 Requirements.io_uring,
916 Requirements.zbd,
917 Requirements.root,
918 Requirements.zoned_nullb,
919 Requirements.not_macos,
920 Requirements.not_windows,
921 Requirements.unittests,
922 Requirements.cpucount4,
923 Requirements.nvmecdev,
924 ]
b1bc705e
VF
925 for req in req_list:
926 value, desc = req()
704cc4df 927 logging.debug("Requirements: Requirement '%s' met? %s", desc, value)
b1bc705e 928
704cc4df
VF
929 @classmethod
930 def linux(cls):
931 """Are we running on Linux?"""
b1bc705e
VF
932 return Requirements._linux, "Linux required"
933
704cc4df
VF
934 @classmethod
935 def libaio(cls):
936 """Is libaio available?"""
b1bc705e
VF
937 return Requirements._libaio, "libaio required"
938
a2947c33
VF
939 @classmethod
940 def io_uring(cls):
941 """Is io_uring available?"""
942 return Requirements._io_uring, "io_uring required"
943
704cc4df
VF
944 @classmethod
945 def zbd(cls):
946 """Is ZBD support available?"""
b1bc705e
VF
947 return Requirements._zbd, "Zoned block device support required"
948
704cc4df
VF
949 @classmethod
950 def root(cls):
951 """Are we running as root?"""
b1bc705e
VF
952 return Requirements._root, "root required"
953
704cc4df
VF
954 @classmethod
955 def zoned_nullb(cls):
956 """Are zoned null block devices available?"""
b1bc705e
VF
957 return Requirements._zoned_nullb, "Zoned null block device support required"
958
704cc4df
VF
959 @classmethod
960 def not_macos(cls):
961 """Are we running on a platform other than macOS?"""
b1bc705e
VF
962 return Requirements._not_macos, "platform other than macOS required"
963
704cc4df
VF
964 @classmethod
965 def not_windows(cls):
966 """Are we running on a platform other than Windws?"""
c58b33b4
VF
967 return Requirements._not_windows, "platform other than Windows required"
968
704cc4df
VF
969 @classmethod
970 def unittests(cls):
971 """Were unittests built?"""
b1bc705e
VF
972 return Requirements._unittests, "Unittests support required"
973
704cc4df
VF
974 @classmethod
975 def cpucount4(cls):
976 """Do we have at least 4 CPUs?"""
b1bc705e
VF
977 return Requirements._cpucount4, "4+ CPUs required"
978
2128f93a
VF
979 @classmethod
980 def nvmecdev(cls):
981 """Do we have an NVMe character device to test?"""
982 return Requirements._nvmecdev, "NVMe character device test target required"
983
b1bc705e 984
df1eaa36 985SUCCESS_DEFAULT = {
704cc4df
VF
986 'zero_return': True,
987 'stderr_empty': True,
988 'timeout': 600,
989 }
df1eaa36 990SUCCESS_NONZERO = {
704cc4df
VF
991 'zero_return': False,
992 'stderr_empty': False,
993 'timeout': 600,
994 }
df1eaa36 995SUCCESS_STDERR = {
704cc4df
VF
996 'zero_return': True,
997 'stderr_empty': False,
998 'timeout': 600,
999 }
df1eaa36 1000TEST_LIST = [
704cc4df
VF
1001 {
1002 'test_id': 1,
1003 'test_class': FioJobTest,
1004 'job': 't0001-52c58027.fio',
1005 'success': SUCCESS_DEFAULT,
1006 'pre_job': None,
1007 'pre_success': None,
1008 'requirements': [],
1009 },
1010 {
1011 'test_id': 2,
1012 'test_class': FioJobTest,
1013 'job': 't0002-13af05ae-post.fio',
1014 'success': SUCCESS_DEFAULT,
1015 'pre_job': 't0002-13af05ae-pre.fio',
1016 'pre_success': None,
1017 'requirements': [Requirements.linux, Requirements.libaio],
1018 },
1019 {
1020 'test_id': 3,
1021 'test_class': FioJobTest,
1022 'job': 't0003-0ae2c6e1-post.fio',
1023 'success': SUCCESS_NONZERO,
1024 'pre_job': 't0003-0ae2c6e1-pre.fio',
1025 'pre_success': SUCCESS_DEFAULT,
1026 'requirements': [Requirements.linux, Requirements.libaio],
1027 },
1028 {
1029 'test_id': 4,
1030 'test_class': FioJobTest,
1031 'job': 't0004-8a99fdf6.fio',
1032 'success': SUCCESS_DEFAULT,
1033 'pre_job': None,
1034 'pre_success': None,
1035 'requirements': [Requirements.linux, Requirements.libaio],
1036 },
1037 {
1038 'test_id': 5,
1039 'test_class': FioJobTest_t0005,
1040 'job': 't0005-f7078f7b.fio',
1041 'success': SUCCESS_DEFAULT,
1042 'pre_job': None,
1043 'pre_success': None,
1044 'output_format': 'json',
1045 'requirements': [Requirements.not_windows],
1046 },
1047 {
1048 'test_id': 6,
1049 'test_class': FioJobTest_t0006,
1050 'job': 't0006-82af2a7c.fio',
1051 'success': SUCCESS_DEFAULT,
1052 'pre_job': None,
1053 'pre_success': None,
1054 'output_format': 'json',
1055 'requirements': [Requirements.linux, Requirements.libaio],
1056 },
1057 {
1058 'test_id': 7,
1059 'test_class': FioJobTest_t0007,
1060 'job': 't0007-37cf9e3c.fio',
1061 'success': SUCCESS_DEFAULT,
1062 'pre_job': None,
1063 'pre_success': None,
1064 'output_format': 'json',
1065 'requirements': [],
1066 },
1067 {
1068 'test_id': 8,
1069 'test_class': FioJobTest_t0008,
1070 'job': 't0008-ae2fafc8.fio',
1071 'success': SUCCESS_DEFAULT,
1072 'pre_job': None,
1073 'pre_success': None,
1074 'output_format': 'json',
1075 'requirements': [],
1076 },
1077 {
1078 'test_id': 9,
1079 'test_class': FioJobTest_t0009,
1080 'job': 't0009-f8b0bd10.fio',
1081 'success': SUCCESS_DEFAULT,
1082 'pre_job': None,
1083 'pre_success': None,
1084 'output_format': 'json',
1085 'requirements': [Requirements.not_macos,
1086 Requirements.cpucount4],
1087 # mac os does not support CPU affinity
1088 },
1089 {
1090 'test_id': 10,
1091 'test_class': FioJobTest,
1092 'job': 't0010-b7aae4ba.fio',
1093 'success': SUCCESS_DEFAULT,
1094 'pre_job': None,
1095 'pre_success': None,
1096 'requirements': [],
1097 },
1098 {
1099 'test_id': 11,
0a602473 1100 'test_class': FioJobTest_iops_rate,
704cc4df
VF
1101 'job': 't0011-5d2788d5.fio',
1102 'success': SUCCESS_DEFAULT,
1103 'pre_job': None,
1104 'pre_success': None,
1105 'output_format': 'json',
1106 'requirements': [],
1107 },
0a602473
BVA
1108 {
1109 'test_id': 12,
d4e74fda 1110 'test_class': FioJobTest_t0012,
0a602473
BVA
1111 'job': 't0012.fio',
1112 'success': SUCCESS_DEFAULT,
1113 'pre_job': None,
1114 'pre_success': None,
1115 'output_format': 'json',
d4e74fda 1116 'requirements': [],
0a602473 1117 },
f0c7ae7a
BVA
1118 {
1119 'test_id': 13,
061a0773 1120 'test_class': FioJobTest,
f0c7ae7a
BVA
1121 'job': 't0013.fio',
1122 'success': SUCCESS_DEFAULT,
1123 'pre_job': None,
1124 'pre_success': None,
1125 'output_format': 'json',
1126 'requirements': [],
1127 },
d4e74fda
DB
1128 {
1129 'test_id': 14,
1130 'test_class': FioJobTest_t0014,
1131 'job': 't0014.fio',
1132 'success': SUCCESS_DEFAULT,
1133 'pre_job': None,
1134 'pre_success': None,
1135 'output_format': 'json',
1136 'requirements': [],
1137 },
60ebb939
VF
1138 {
1139 'test_id': 15,
1140 'test_class': FioJobTest_t0015,
1141 'job': 't0015-e78980ff.fio',
1142 'success': SUCCESS_DEFAULT,
1143 'pre_job': None,
1144 'pre_success': None,
1145 'output_format': 'json',
1146 'requirements': [Requirements.linux, Requirements.libaio],
1147 },
de31fe9a
VF
1148 {
1149 'test_id': 16,
1150 'test_class': FioJobTest_t0015,
f045d5f8 1151 'job': 't0016-d54ae22.fio',
de31fe9a
VF
1152 'success': SUCCESS_DEFAULT,
1153 'pre_job': None,
1154 'pre_success': None,
1155 'output_format': 'json',
1156 'requirements': [],
1157 },
31a58cba
VF
1158 {
1159 'test_id': 17,
1160 'test_class': FioJobTest_t0015,
1161 'job': 't0017.fio',
1162 'success': SUCCESS_DEFAULT,
1163 'pre_job': None,
1164 'pre_success': None,
1165 'output_format': 'json',
1166 'requirements': [Requirements.not_windows],
1167 },
a2947c33
VF
1168 {
1169 'test_id': 18,
1170 'test_class': FioJobTest,
1171 'job': 't0018.fio',
1172 'success': SUCCESS_DEFAULT,
1173 'pre_job': None,
1174 'pre_success': None,
1175 'requirements': [Requirements.linux, Requirements.io_uring],
1176 },
ef54f290
VF
1177 {
1178 'test_id': 19,
1179 'test_class': FioJobTest_t0019,
1180 'job': 't0019.fio',
1181 'success': SUCCESS_DEFAULT,
1182 'pre_job': None,
1183 'pre_success': None,
1184 'requirements': [],
1185 },
1186 {
1187 'test_id': 20,
1188 'test_class': FioJobTest_t0020,
1189 'job': 't0020.fio',
1190 'success': SUCCESS_DEFAULT,
1191 'pre_job': None,
1192 'pre_success': None,
1193 'requirements': [],
1194 },
eb40b275
VF
1195 {
1196 'test_id': 21,
1197 'test_class': FioJobTest_t0020,
1198 'job': 't0021.fio',
1199 'success': SUCCESS_DEFAULT,
1200 'pre_job': None,
1201 'pre_success': None,
1202 'requirements': [],
1203 },
1204 {
1205 'test_id': 22,
1206 'test_class': FioJobTest_t0022,
1207 'job': 't0022.fio',
1208 'success': SUCCESS_DEFAULT,
1209 'pre_job': None,
1210 'pre_success': None,
1211 'requirements': [],
1212 },
c37183f8
VF
1213 {
1214 'test_id': 23,
1215 'test_class': FioJobTest_t0023,
1216 'job': 't0023.fio',
1217 'success': SUCCESS_DEFAULT,
1218 'pre_job': None,
1219 'pre_success': None,
1220 'requirements': [],
1221 },
2d5c4600
VF
1222 {
1223 'test_id': 24,
1224 'test_class': FioJobTest_t0024,
1225 'job': 't0024.fio',
1226 'success': SUCCESS_DEFAULT,
1227 'pre_job': None,
1228 'pre_success': None,
1229 'requirements': [],
1230 },
d661fb18
SK
1231 {
1232 'test_id': 25,
1233 'test_class': FioJobTest_t0025,
1234 'job': 't0025.fio',
1235 'success': SUCCESS_DEFAULT,
1236 'pre_job': None,
1237 'pre_success': None,
1238 'output_format': 'json',
1239 'requirements': [],
1240 },
c4704c08
SK
1241 {
1242 'test_id': 26,
1243 'test_class': FioJobTest,
1244 'job': 't0026.fio',
1245 'success': SUCCESS_DEFAULT,
1246 'pre_job': None,
1247 'pre_success': None,
1248 'requirements': [Requirements.not_windows],
1249 },
ede04c27
LG
1250 {
1251 'test_id': 27,
1252 'test_class': FioJobTest_t0027,
1253 'job': 't0027.fio',
1254 'success': SUCCESS_DEFAULT,
1255 'pre_job': None,
1256 'pre_success': None,
1257 'requirements': [],
1258 },
23fb1642
VF
1259 {
1260 'test_id': 28,
1261 'test_class': FioJobTest,
1262 'job': 't0028-c6cade16.fio',
1263 'success': SUCCESS_DEFAULT,
1264 'pre_job': None,
1265 'pre_success': None,
1266 'requirements': [],
1267 },
704cc4df
VF
1268 {
1269 'test_id': 1000,
1270 'test_class': FioExeTest,
1271 'exe': 't/axmap',
1272 'parameters': None,
1273 'success': SUCCESS_DEFAULT,
1274 'requirements': [],
1275 },
1276 {
1277 'test_id': 1001,
1278 'test_class': FioExeTest,
1279 'exe': 't/ieee754',
1280 'parameters': None,
1281 'success': SUCCESS_DEFAULT,
1282 'requirements': [],
1283 },
1284 {
1285 'test_id': 1002,
1286 'test_class': FioExeTest,
1287 'exe': 't/lfsr-test',
1288 'parameters': ['0xFFFFFF', '0', '0', 'verify'],
1289 'success': SUCCESS_STDERR,
1290 'requirements': [],
1291 },
1292 {
1293 'test_id': 1003,
1294 'test_class': FioExeTest,
1295 'exe': 't/readonly.py',
1296 'parameters': ['-f', '{fio_path}'],
1297 'success': SUCCESS_DEFAULT,
1298 'requirements': [],
1299 },
1300 {
1301 'test_id': 1004,
1302 'test_class': FioExeTest,
1303 'exe': 't/steadystate_tests.py',
1304 'parameters': ['{fio_path}'],
1305 'success': SUCCESS_DEFAULT,
1306 'requirements': [],
1307 },
1308 {
1309 'test_id': 1005,
1310 'test_class': FioExeTest,
1311 'exe': 't/stest',
1312 'parameters': None,
1313 'success': SUCCESS_STDERR,
1314 'requirements': [],
1315 },
1316 {
1317 'test_id': 1006,
1318 'test_class': FioExeTest,
1319 'exe': 't/strided.py',
1320 'parameters': ['{fio_path}'],
1321 'success': SUCCESS_DEFAULT,
1322 'requirements': [],
1323 },
1324 {
1325 'test_id': 1007,
1326 'test_class': FioExeTest,
037b2b50
DF
1327 'exe': 't/zbd/run-tests-against-nullb',
1328 'parameters': ['-s', '1'],
704cc4df
VF
1329 'success': SUCCESS_DEFAULT,
1330 'requirements': [Requirements.linux, Requirements.zbd,
1331 Requirements.root],
1332 },
1333 {
1334 'test_id': 1008,
1335 'test_class': FioExeTest,
037b2b50
DF
1336 'exe': 't/zbd/run-tests-against-nullb',
1337 'parameters': ['-s', '2'],
704cc4df
VF
1338 'success': SUCCESS_DEFAULT,
1339 'requirements': [Requirements.linux, Requirements.zbd,
1340 Requirements.root, Requirements.zoned_nullb],
1341 },
1342 {
1343 'test_id': 1009,
1344 'test_class': FioExeTest,
1345 'exe': 'unittests/unittest',
1346 'parameters': None,
1347 'success': SUCCESS_DEFAULT,
1348 'requirements': [Requirements.unittests],
1349 },
1350 {
1351 'test_id': 1010,
1352 'test_class': FioExeTest,
1353 'exe': 't/latency_percentiles.py',
1354 'parameters': ['-f', '{fio_path}'],
1355 'success': SUCCESS_DEFAULT,
1356 'requirements': [],
1357 },
8403eca6
VF
1358 {
1359 'test_id': 1011,
1360 'test_class': FioExeTest,
1361 'exe': 't/jsonplus2csv_test.py',
1362 'parameters': ['-f', '{fio_path}'],
1363 'success': SUCCESS_DEFAULT,
1364 'requirements': [],
1365 },
03900b0b 1366 {
1367 'test_id': 1012,
1368 'test_class': FioExeTest,
1369 'exe': 't/log_compression.py',
1370 'parameters': ['-f', '{fio_path}'],
1371 'success': SUCCESS_DEFAULT,
1372 'requirements': [],
1373 },
c994fa62
VF
1374 {
1375 'test_id': 1013,
1376 'test_class': FioExeTest,
1377 'exe': 't/random_seed.py',
1378 'parameters': ['-f', '{fio_path}'],
1379 'success': SUCCESS_DEFAULT,
1380 'requirements': [],
1381 },
2128f93a
VF
1382 {
1383 'test_id': 1014,
1384 'test_class': FioExeTest,
1385 'exe': 't/nvmept.py',
1386 'parameters': ['-f', '{fio_path}', '--dut', '{nvmecdev}'],
1387 'success': SUCCESS_DEFAULT,
1388 'requirements': [Requirements.linux, Requirements.nvmecdev],
1389 },
df1eaa36
VF
1390]
1391
1392
1393def parse_args():
704cc4df
VF
1394 """Parse command-line arguments."""
1395
df1eaa36
VF
1396 parser = argparse.ArgumentParser()
1397 parser.add_argument('-r', '--fio-root',
1398 help='fio root path')
1399 parser.add_argument('-f', '--fio',
1400 help='path to fio executable (e.g., ./fio)')
1401 parser.add_argument('-a', '--artifact-root',
1402 help='artifact root directory')
1403 parser.add_argument('-s', '--skip', nargs='+', type=int,
1404 help='list of test(s) to skip')
1405 parser.add_argument('-o', '--run-only', nargs='+', type=int,
1406 help='list of test(s) to run, skipping all others')
6d5470c3
VF
1407 parser.add_argument('-d', '--debug', action='store_true',
1408 help='provide debug output')
b1bc705e
VF
1409 parser.add_argument('-k', '--skip-req', action='store_true',
1410 help='skip requirements checking')
58a77d2a
VF
1411 parser.add_argument('-p', '--pass-through', action='append',
1412 help='pass-through an argument to an executable test')
2128f93a
VF
1413 parser.add_argument('--nvmecdev', action='store', default=None,
1414 help='NVMe character device for **DESTRUCTIVE** testing (e.g., /dev/ng0n1)')
df1eaa36
VF
1415 args = parser.parse_args()
1416
1417 return args
1418
1419
1420def main():
704cc4df
VF
1421 """Entry point."""
1422
df1eaa36 1423 args = parse_args()
6d5470c3
VF
1424 if args.debug:
1425 logging.basicConfig(level=logging.DEBUG)
1426 else:
1427 logging.basicConfig(level=logging.INFO)
1428
58a77d2a
VF
1429 pass_through = {}
1430 if args.pass_through:
1431 for arg in args.pass_through:
1432 if not ':' in arg:
1b4ba547 1433 print(f"Invalid --pass-through argument '{arg}'")
58a77d2a
VF
1434 print("Syntax for --pass-through is TESTNUMBER:ARGUMENT")
1435 return
061a0773 1436 split = arg.split(":", 1)
58a77d2a 1437 pass_through[int(split[0])] = split[1]
061a0773 1438 logging.debug("Pass-through arguments: %s", pass_through)
58a77d2a 1439
df1eaa36
VF
1440 if args.fio_root:
1441 fio_root = args.fio_root
1442 else:
6d5470c3 1443 fio_root = str(Path(__file__).absolute().parent.parent)
1b4ba547 1444 print(f"fio root is {fio_root}")
df1eaa36
VF
1445
1446 if args.fio:
1447 fio_path = args.fio
1448 else:
742b8799
VF
1449 if platform.system() == "Windows":
1450 fio_exe = "fio.exe"
1451 else:
1452 fio_exe = "fio"
1453 fio_path = os.path.join(fio_root, fio_exe)
1b4ba547 1454 print(f"fio path is {fio_path}")
6d5470c3
VF
1455 if not shutil.which(fio_path):
1456 print("Warning: fio executable not found")
df1eaa36
VF
1457
1458 artifact_root = args.artifact_root if args.artifact_root else \
1b4ba547 1459 f"fio-test-{time.strftime('%Y%m%d-%H%M%S')}"
df1eaa36 1460 os.mkdir(artifact_root)
1b4ba547 1461 print(f"Artifact directory is {artifact_root}")
df1eaa36 1462
b1bc705e 1463 if not args.skip_req:
2128f93a 1464 req = Requirements(fio_root, args)
b1bc705e 1465
df1eaa36
VF
1466 passed = 0
1467 failed = 0
1468 skipped = 0
1469
1470 for config in TEST_LIST:
1471 if (args.skip and config['test_id'] in args.skip) or \
1472 (args.run_only and config['test_id'] not in args.run_only):
1473 skipped = skipped + 1
1b4ba547 1474 print(f"Test {config['test_id']} SKIPPED (User request)")
df1eaa36
VF
1475 continue
1476
1477 if issubclass(config['test_class'], FioJobTest):
1478 if config['pre_job']:
1479 fio_pre_job = os.path.join(fio_root, 't', 'jobs',
1480 config['pre_job'])
1481 else:
1482 fio_pre_job = None
1483 if config['pre_success']:
1484 fio_pre_success = config['pre_success']
1485 else:
1486 fio_pre_success = None
1487 if 'output_format' in config:
1488 output_format = config['output_format']
1489 else:
1490 output_format = 'normal'
1491 test = config['test_class'](
1492 fio_path,
1493 os.path.join(fio_root, 't', 'jobs', config['job']),
1494 config['success'],
1495 fio_pre_job=fio_pre_job,
1496 fio_pre_success=fio_pre_success,
1497 output_format=output_format)
9393cdaa 1498 desc = config['job']
df1eaa36
VF
1499 elif issubclass(config['test_class'], FioExeTest):
1500 exe_path = os.path.join(fio_root, config['exe'])
1501 if config['parameters']:
2128f93a
VF
1502 parameters = [p.format(fio_path=fio_path, nvmecdev=args.nvmecdev)
1503 for p in config['parameters']]
df1eaa36 1504 else:
58a77d2a 1505 parameters = []
742b8799 1506 if Path(exe_path).suffix == '.py' and platform.system() == "Windows":
58a77d2a 1507 parameters.insert(0, exe_path)
742b8799 1508 exe_path = "python.exe"
58a77d2a
VF
1509 if config['test_id'] in pass_through:
1510 parameters += pass_through[config['test_id']].split()
df1eaa36
VF
1511 test = config['test_class'](exe_path, parameters,
1512 config['success'])
9393cdaa 1513 desc = config['exe']
df1eaa36 1514 else:
1b4ba547 1515 print(f"Test {config['test_id']} FAILED: unable to process test config")
df1eaa36
VF
1516 failed = failed + 1
1517 continue
1518
b1bc705e 1519 if not args.skip_req:
704cc4df 1520 reqs_met = True
b1bc705e 1521 for req in config['requirements']:
704cc4df
VF
1522 reqs_met, reason = req()
1523 logging.debug("Test %d: Requirement '%s' met? %s", config['test_id'], reason,
1524 reqs_met)
1525 if not reqs_met:
b1bc705e 1526 break
704cc4df 1527 if not reqs_met:
1b4ba547 1528 print(f"Test {config['test_id']} SKIPPED ({reason}) {desc}")
b1bc705e
VF
1529 skipped = skipped + 1
1530 continue
1531
aa9f2627
VF
1532 try:
1533 test.setup(artifact_root, config['test_id'])
1534 test.run()
1535 test.check_result()
1536 except KeyboardInterrupt:
1537 break
1538 except Exception as e:
1539 test.passed = False
1540 test.failure_reason += str(e)
1541 logging.debug("Test %d exception:\n%s\n", config['test_id'], traceback.format_exc())
df1eaa36
VF
1542 if test.passed:
1543 result = "PASSED"
1544 passed = passed + 1
1545 else:
1b4ba547 1546 result = f"FAILED: {test.failure_reason}"
df1eaa36 1547 failed = failed + 1
15a73987
VF
1548 contents, _ = FioJobTest.get_file(test.stderr_file)
1549 logging.debug("Test %d: stderr:\n%s", config['test_id'], contents)
1550 contents, _ = FioJobTest.get_file(test.stdout_file)
1551 logging.debug("Test %d: stdout:\n%s", config['test_id'], contents)
1b4ba547 1552 print(f"Test {config['test_id']} {result} {desc}")
df1eaa36 1553
1b4ba547 1554 print(f"{passed} test(s) passed, {failed} failed, {skipped} skipped")
df1eaa36
VF
1555
1556 sys.exit(failed)
1557
1558
1559if __name__ == '__main__':
1560 main()