t/run-fio-tests: integrate t/nvmept.py
[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
82 self.test_dir = os.path.join(artifact_root, "{:04d}".format(testnum))
83 if not os.path.exists(self.test_dir):
84 os.mkdir(self.test_dir)
85
86 self.command_file = os.path.join(
704cc4df
VF
87 self.test_dir,
88 "{0}.command".format(os.path.basename(self.exe_path)))
df1eaa36 89 self.stdout_file = os.path.join(
704cc4df
VF
90 self.test_dir,
91 "{0}.stdout".format(os.path.basename(self.exe_path)))
df1eaa36 92 self.stderr_file = os.path.join(
704cc4df
VF
93 self.test_dir,
94 "{0}.stderr".format(os.path.basename(self.exe_path)))
95 self.exitcode_file = os.path.join(
96 self.test_dir,
97 "{0}.exitcode".format(os.path.basename(self.exe_path)))
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
VF
128 command_file = open(self.command_file, "w+")
129 command_file.write("%s\n" % command)
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'])
704cc4df
VF
147 exitcode_file.write('{0}\n'.format(proc.returncode))
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':
172 self.failure_reason = "{0} timeout,".format(self.failure_reason)
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
186 self.failure_reason = "{0} non-zero return code,".format(self.failure_reason)
187 else:
188 if self.output['proc'].returncode == 0:
189 self.failure_reason = "{0} zero return code,".format(self.failure_reason)
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:
196 self.failure_reason = "{0} stderr not empty,".format(self.failure_reason)
197 self.passed = False
198 else:
199 if stderr_size == 0:
200 self.failure_reason = "{0} stderr empty,".format(self.failure_reason)
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
226 self.fio_output = "{0}.output".format(os.path.basename(self.fio_job))
227 self.fio_args = [
771dbb52 228 "--max-jobs=16",
df1eaa36
VF
229 "--output-format={0}".format(self.output_format),
230 "--output={0}".format(self.fio_output),
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
df1eaa36
VF
238 super(FioJobTest, self).setup(artifact_root, testnum)
239
240 self.command_file = os.path.join(
704cc4df
VF
241 self.test_dir,
242 "{0}.command".format(os.path.basename(self.fio_job)))
df1eaa36 243 self.stdout_file = os.path.join(
704cc4df
VF
244 self.test_dir,
245 "{0}.stdout".format(os.path.basename(self.fio_job)))
df1eaa36 246 self.stderr_file = os.path.join(
704cc4df
VF
247 self.test_dir,
248 "{0}.stderr".format(os.path.basename(self.fio_job)))
249 self.exitcode_file = os.path.join(
250 self.test_dir,
251 "{0}.exitcode".format(os.path.basename(self.fio_job)))
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:
272 super(FioJobTest, self).run()
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:
298 self.failure_reason += " unable to read file {0}".format(filename)
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
308 self.failure_reason = "{0} precondition step failed,".format(self.failure_reason)
309 return
310
311 super(FioJobTest, self).check_result()
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:
333 self.failure_reason = "{0} unable to decode JSON data,".format(self.failure_reason)
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):
342 super(FioJobTest_t0005, self).check_result()
343
344 if not self.passed:
345 return
346
347 if self.json_data['jobs'][0]['read']['io_kbytes'] != 102400:
348 self.failure_reason = "{0} bytes read mismatch,".format(self.failure_reason)
349 self.passed = False
350 if self.json_data['jobs'][0]['write']['io_kbytes'] != 102400:
351 self.failure_reason = "{0} bytes written mismatch,".format(self.failure_reason)
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):
360 super(FioJobTest_t0006, self).check_result()
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
VF
368 if ratio < 1.99 or ratio > 2.01:
369 self.failure_reason = "{0} read/write ratio mismatch,".format(self.failure_reason)
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):
378 super(FioJobTest_t0007, self).check_result()
379
380 if not self.passed:
381 return
382
383 if self.json_data['jobs'][0]['read']['io_kbytes'] != 87040:
384 self.failure_reason = "{0} bytes read mismatch,".format(self.failure_reason)
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):
400 super(FioJobTest_t0008, self).check_result()
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:
df1eaa36
VF
409 self.failure_reason = "{0} bytes written mismatch,".format(self.failure_reason)
410 self.passed = False
411 if self.json_data['jobs'][0]['read']['io_kbytes'] != 32768:
412 self.failure_reason = "{0} bytes read mismatch,".format(self.failure_reason)
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):
421 super(FioJobTest_t0009, self).check_result()
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:
429 self.failure_reason = "{0} elapsed time mismatch,".format(self.failure_reason)
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):
439 super(FioJobTest_t0012, self).check_result()
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):
487 super(FioJobTest_t0014, self).check_result()
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):
542 super(FioJobTest_t0015, self).check_result()
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):
563 super(FioJobTest_t0019, self).check_result()
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
579 self.failure_reason = "offsets {0}, {1} not sequential".format(prev, cur)
580 return
581 prev = cur
582
583 if cur/4096 != 255:
584 self.passed = False
585 self.failure_reason = "unexpected last offset {0}".format(cur)
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):
593 super(FioJobTest_t0020, self).check_result()
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
614 self.failure_reason += " number of offsets is {0} instead of 256".format(len(offsets))
615
616 for i in range(256):
617 if not i in offsets:
618 self.passed = False
619 self.failure_reason += " missing offset {0}".format(i*4096)
620
888dbd62
VF
621 (z, p) = runstest_1samp(list(offsets))
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):
631 super(FioJobTest_t0022, self).check_result()
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
658 self.failure_reason = "too many ({0}) consecutive offsets".format(seq_count)
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:
693 if ddir != 1:
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
VF
703 break
704 if prev_offset != offset:
705 self.passed = False
21a20229
VF
706 self.failure_reason += " {0}: offset does not match: {1}".format(
707 bw_log_filename, line)
c37183f8
VF
708 break
709 prev_ddir = ddir
710 prev_bs = bs
711 prev_offset = offset
712
713
9e83aa16 714 def check_all_offsets(self, filename, sectorsize, filesize):
21a20229
VF
715 """Make sure all offsets were touched."""
716
0f5234b4
VF
717 file_data = self.get_file_fail(os.path.join(self.test_dir, filename))
718 if not file_data:
9e83aa16
VF
719 return
720
721 log_lines = file_data.split('\n')
722
723 offsets = set()
724
725 for line in log_lines:
726 if len(line.strip()) == 0:
727 continue
728 vals = line.split(',')
729 bs = int(vals[3])
730 offset = int(vals[4])
731 if offset % sectorsize != 0:
732 self.passed = False
21a20229
VF
733 self.failure_reason += " {0}: offset {1} not a multiple of sector size {2}".format(
734 filename, offset, sectorsize)
735 break
9e83aa16
VF
736 if bs % sectorsize != 0:
737 self.passed = False
21a20229
VF
738 self.failure_reason += " {0}: block size {1} not a multiple of sector size " \
739 "{2}".format(filename, bs, sectorsize)
740 break
9e83aa16
VF
741 for i in range(int(bs/sectorsize)):
742 offsets.add(offset/sectorsize + i)
743
744 if len(offsets) != filesize/sectorsize:
745 self.passed = False
21a20229
VF
746 self.failure_reason += " {0}: only {1} offsets touched; expected {2}".format(
747 filename, len(offsets), filesize/sectorsize)
9e83aa16 748 else:
21a20229 749 logging.debug("%s: %d sectors touched", filename, len(offsets))
9e83aa16
VF
750
751
c37183f8
VF
752 def check_result(self):
753 super(FioJobTest_t0023, self).check_result()
754
9e83aa16
VF
755 filesize = 1024*1024
756
1fb78294
VF
757 self.check_trimwrite("basic_bw.log")
758 self.check_trimwrite("bs_bw.log")
759 self.check_trimwrite("bsrange_bw.log")
760 self.check_trimwrite("bssplit_bw.log")
761 self.check_trimwrite("basic_no_rm_bw.log")
762 self.check_trimwrite("bs_no_rm_bw.log")
763 self.check_trimwrite("bsrange_no_rm_bw.log")
764 self.check_trimwrite("bssplit_no_rm_bw.log")
c37183f8 765
9e83aa16
VF
766 self.check_all_offsets("basic_bw.log", 4096, filesize)
767 self.check_all_offsets("bs_bw.log", 8192, filesize)
768 self.check_all_offsets("bsrange_bw.log", 512, filesize)
769 self.check_all_offsets("bssplit_bw.log", 512, filesize)
c37183f8
VF
770
771
2d5c4600
VF
772class FioJobTest_t0024(FioJobTest_t0023):
773 """Test consists of fio test job t0024 trimwrite test."""
774
775 def check_result(self):
776 # call FioJobTest_t0023's parent to skip checks done by t0023
777 super(FioJobTest_t0023, self).check_result()
778
779 filesize = 1024*1024
780
781 self.check_trimwrite("basic_bw.log")
782 self.check_trimwrite("bs_bw.log")
783 self.check_trimwrite("bsrange_bw.log")
784 self.check_trimwrite("bssplit_bw.log")
785
786 self.check_all_offsets("basic_bw.log", 4096, filesize)
787 self.check_all_offsets("bs_bw.log", 8192, filesize)
788 self.check_all_offsets("bsrange_bw.log", 512, filesize)
789 self.check_all_offsets("bssplit_bw.log", 512, filesize)
790
791
d661fb18
SK
792class FioJobTest_t0025(FioJobTest):
793 """Test experimental verify read backs written data pattern."""
794 def check_result(self):
795 super(FioJobTest_t0025, self).check_result()
796
797 if not self.passed:
798 return
799
800 if self.json_data['jobs'][0]['read']['io_kbytes'] != 128:
801 self.passed = False
802
ede04c27
LG
803class FioJobTest_t0027(FioJobTest):
804 def setup(self, *args, **kws):
805 super(FioJobTest_t0027, self).setup(*args, **kws)
806 self.pattern_file = os.path.join(self.test_dir, "t0027.pattern")
807 self.output_file = os.path.join(self.test_dir, "t0027file")
808 self.pattern = os.urandom(16 << 10)
809 with open(self.pattern_file, "wb") as f:
810 f.write(self.pattern)
811
812 def check_result(self):
813 super(FioJobTest_t0027, self).check_result()
814
815 if not self.passed:
816 return
817
818 with open(self.output_file, "rb") as f:
819 data = f.read()
820
821 if data != self.pattern:
822 self.passed = False
d661fb18 823
0a602473 824class FioJobTest_iops_rate(FioJobTest):
12b609b5 825 """Test consists of fio test job t0011
df1eaa36
VF
826 Confirm that job0 iops == 1000
827 and that job1_iops / job0_iops ~ 8
828 With two runs of fio-3.16 I observed a ratio of 8.3"""
829
830 def check_result(self):
0a602473 831 super(FioJobTest_iops_rate, self).check_result()
df1eaa36
VF
832
833 if not self.passed:
834 return
835
836 iops1 = self.json_data['jobs'][0]['read']['iops']
7ddc4ed1 837 logging.debug("Test %d: iops1: %f", self.testnum, iops1)
df1eaa36 838 iops2 = self.json_data['jobs'][1]['read']['iops']
7ddc4ed1 839 logging.debug("Test %d: iops2: %f", self.testnum, iops2)
df1eaa36 840 ratio = iops2 / iops1
704cc4df 841 logging.debug("Test %d: ratio: %f", self.testnum, ratio)
df1eaa36 842
0a9b8988 843 if iops1 < 950 or iops1 > 1050:
df1eaa36
VF
844 self.failure_reason = "{0} iops value mismatch,".format(self.failure_reason)
845 self.passed = False
846
d4e74fda 847 if ratio < 6 or ratio > 10:
df1eaa36
VF
848 self.failure_reason = "{0} iops ratio mismatch,".format(self.failure_reason)
849 self.passed = False
850
851
114eadb2 852class Requirements():
b1bc705e
VF
853 """Requirements consists of multiple run environment characteristics.
854 These are to determine if a particular test can be run"""
855
856 _linux = False
857 _libaio = False
a2947c33 858 _io_uring = False
b1bc705e
VF
859 _zbd = False
860 _root = False
861 _zoned_nullb = False
862 _not_macos = False
c58b33b4 863 _not_windows = False
b1bc705e
VF
864 _unittests = False
865 _cpucount4 = False
2128f93a 866 _nvmecdev = False
b1bc705e 867
2128f93a 868 def __init__(self, fio_root, args):
b1bc705e 869 Requirements._not_macos = platform.system() != "Darwin"
c58b33b4 870 Requirements._not_windows = platform.system() != "Windows"
b1bc705e
VF
871 Requirements._linux = platform.system() == "Linux"
872
873 if Requirements._linux:
15a73987
VF
874 config_file = os.path.join(fio_root, "config-host.h")
875 contents, success = FioJobTest.get_file(config_file)
876 if not success:
b1bc705e
VF
877 print("Unable to open {0} to check requirements".format(config_file))
878 Requirements._zbd = True
879 else:
b7694961 880 Requirements._zbd = "CONFIG_HAS_BLKZONED" in contents
b1bc705e
VF
881 Requirements._libaio = "CONFIG_LIBAIO" in contents
882
a2947c33
VF
883 contents, success = FioJobTest.get_file("/proc/kallsyms")
884 if not success:
885 print("Unable to open '/proc/kallsyms' to probe for io_uring support")
886 else:
887 Requirements._io_uring = "io_uring_setup" in contents
888
b1bc705e
VF
889 Requirements._root = (os.geteuid() == 0)
890 if Requirements._zbd and Requirements._root:
8854e368
VF
891 try:
892 subprocess.run(["modprobe", "null_blk"],
893 stdout=subprocess.PIPE,
894 stderr=subprocess.PIPE)
895 if os.path.exists("/sys/module/null_blk/parameters/zoned"):
896 Requirements._zoned_nullb = True
897 except Exception:
898 pass
b1bc705e 899
742b8799
VF
900 if platform.system() == "Windows":
901 utest_exe = "unittest.exe"
902 else:
903 utest_exe = "unittest"
904 unittest_path = os.path.join(fio_root, "unittests", utest_exe)
b1bc705e
VF
905 Requirements._unittests = os.path.exists(unittest_path)
906
907 Requirements._cpucount4 = multiprocessing.cpu_count() >= 4
2128f93a
VF
908 Requirements._nvmecdev = args.nvmecdev
909
910 req_list = [
911 Requirements.linux,
912 Requirements.libaio,
913 Requirements.io_uring,
914 Requirements.zbd,
915 Requirements.root,
916 Requirements.zoned_nullb,
917 Requirements.not_macos,
918 Requirements.not_windows,
919 Requirements.unittests,
920 Requirements.cpucount4,
921 Requirements.nvmecdev,
922 ]
b1bc705e
VF
923 for req in req_list:
924 value, desc = req()
704cc4df 925 logging.debug("Requirements: Requirement '%s' met? %s", desc, value)
b1bc705e 926
704cc4df
VF
927 @classmethod
928 def linux(cls):
929 """Are we running on Linux?"""
b1bc705e
VF
930 return Requirements._linux, "Linux required"
931
704cc4df
VF
932 @classmethod
933 def libaio(cls):
934 """Is libaio available?"""
b1bc705e
VF
935 return Requirements._libaio, "libaio required"
936
a2947c33
VF
937 @classmethod
938 def io_uring(cls):
939 """Is io_uring available?"""
940 return Requirements._io_uring, "io_uring required"
941
704cc4df
VF
942 @classmethod
943 def zbd(cls):
944 """Is ZBD support available?"""
b1bc705e
VF
945 return Requirements._zbd, "Zoned block device support required"
946
704cc4df
VF
947 @classmethod
948 def root(cls):
949 """Are we running as root?"""
b1bc705e
VF
950 return Requirements._root, "root required"
951
704cc4df
VF
952 @classmethod
953 def zoned_nullb(cls):
954 """Are zoned null block devices available?"""
b1bc705e
VF
955 return Requirements._zoned_nullb, "Zoned null block device support required"
956
704cc4df
VF
957 @classmethod
958 def not_macos(cls):
959 """Are we running on a platform other than macOS?"""
b1bc705e
VF
960 return Requirements._not_macos, "platform other than macOS required"
961
704cc4df
VF
962 @classmethod
963 def not_windows(cls):
964 """Are we running on a platform other than Windws?"""
c58b33b4
VF
965 return Requirements._not_windows, "platform other than Windows required"
966
704cc4df
VF
967 @classmethod
968 def unittests(cls):
969 """Were unittests built?"""
b1bc705e
VF
970 return Requirements._unittests, "Unittests support required"
971
704cc4df
VF
972 @classmethod
973 def cpucount4(cls):
974 """Do we have at least 4 CPUs?"""
b1bc705e
VF
975 return Requirements._cpucount4, "4+ CPUs required"
976
2128f93a
VF
977 @classmethod
978 def nvmecdev(cls):
979 """Do we have an NVMe character device to test?"""
980 return Requirements._nvmecdev, "NVMe character device test target required"
981
b1bc705e 982
df1eaa36 983SUCCESS_DEFAULT = {
704cc4df
VF
984 'zero_return': True,
985 'stderr_empty': True,
986 'timeout': 600,
987 }
df1eaa36 988SUCCESS_NONZERO = {
704cc4df
VF
989 'zero_return': False,
990 'stderr_empty': False,
991 'timeout': 600,
992 }
df1eaa36 993SUCCESS_STDERR = {
704cc4df
VF
994 'zero_return': True,
995 'stderr_empty': False,
996 'timeout': 600,
997 }
df1eaa36 998TEST_LIST = [
704cc4df
VF
999 {
1000 'test_id': 1,
1001 'test_class': FioJobTest,
1002 'job': 't0001-52c58027.fio',
1003 'success': SUCCESS_DEFAULT,
1004 'pre_job': None,
1005 'pre_success': None,
1006 'requirements': [],
1007 },
1008 {
1009 'test_id': 2,
1010 'test_class': FioJobTest,
1011 'job': 't0002-13af05ae-post.fio',
1012 'success': SUCCESS_DEFAULT,
1013 'pre_job': 't0002-13af05ae-pre.fio',
1014 'pre_success': None,
1015 'requirements': [Requirements.linux, Requirements.libaio],
1016 },
1017 {
1018 'test_id': 3,
1019 'test_class': FioJobTest,
1020 'job': 't0003-0ae2c6e1-post.fio',
1021 'success': SUCCESS_NONZERO,
1022 'pre_job': 't0003-0ae2c6e1-pre.fio',
1023 'pre_success': SUCCESS_DEFAULT,
1024 'requirements': [Requirements.linux, Requirements.libaio],
1025 },
1026 {
1027 'test_id': 4,
1028 'test_class': FioJobTest,
1029 'job': 't0004-8a99fdf6.fio',
1030 'success': SUCCESS_DEFAULT,
1031 'pre_job': None,
1032 'pre_success': None,
1033 'requirements': [Requirements.linux, Requirements.libaio],
1034 },
1035 {
1036 'test_id': 5,
1037 'test_class': FioJobTest_t0005,
1038 'job': 't0005-f7078f7b.fio',
1039 'success': SUCCESS_DEFAULT,
1040 'pre_job': None,
1041 'pre_success': None,
1042 'output_format': 'json',
1043 'requirements': [Requirements.not_windows],
1044 },
1045 {
1046 'test_id': 6,
1047 'test_class': FioJobTest_t0006,
1048 'job': 't0006-82af2a7c.fio',
1049 'success': SUCCESS_DEFAULT,
1050 'pre_job': None,
1051 'pre_success': None,
1052 'output_format': 'json',
1053 'requirements': [Requirements.linux, Requirements.libaio],
1054 },
1055 {
1056 'test_id': 7,
1057 'test_class': FioJobTest_t0007,
1058 'job': 't0007-37cf9e3c.fio',
1059 'success': SUCCESS_DEFAULT,
1060 'pre_job': None,
1061 'pre_success': None,
1062 'output_format': 'json',
1063 'requirements': [],
1064 },
1065 {
1066 'test_id': 8,
1067 'test_class': FioJobTest_t0008,
1068 'job': 't0008-ae2fafc8.fio',
1069 'success': SUCCESS_DEFAULT,
1070 'pre_job': None,
1071 'pre_success': None,
1072 'output_format': 'json',
1073 'requirements': [],
1074 },
1075 {
1076 'test_id': 9,
1077 'test_class': FioJobTest_t0009,
1078 'job': 't0009-f8b0bd10.fio',
1079 'success': SUCCESS_DEFAULT,
1080 'pre_job': None,
1081 'pre_success': None,
1082 'output_format': 'json',
1083 'requirements': [Requirements.not_macos,
1084 Requirements.cpucount4],
1085 # mac os does not support CPU affinity
1086 },
1087 {
1088 'test_id': 10,
1089 'test_class': FioJobTest,
1090 'job': 't0010-b7aae4ba.fio',
1091 'success': SUCCESS_DEFAULT,
1092 'pre_job': None,
1093 'pre_success': None,
1094 'requirements': [],
1095 },
1096 {
1097 'test_id': 11,
0a602473 1098 'test_class': FioJobTest_iops_rate,
704cc4df
VF
1099 'job': 't0011-5d2788d5.fio',
1100 'success': SUCCESS_DEFAULT,
1101 'pre_job': None,
1102 'pre_success': None,
1103 'output_format': 'json',
1104 'requirements': [],
1105 },
0a602473
BVA
1106 {
1107 'test_id': 12,
d4e74fda 1108 'test_class': FioJobTest_t0012,
0a602473
BVA
1109 'job': 't0012.fio',
1110 'success': SUCCESS_DEFAULT,
1111 'pre_job': None,
1112 'pre_success': None,
1113 'output_format': 'json',
d4e74fda 1114 'requirements': [],
0a602473 1115 },
f0c7ae7a
BVA
1116 {
1117 'test_id': 13,
061a0773 1118 'test_class': FioJobTest,
f0c7ae7a
BVA
1119 'job': 't0013.fio',
1120 'success': SUCCESS_DEFAULT,
1121 'pre_job': None,
1122 'pre_success': None,
1123 'output_format': 'json',
1124 'requirements': [],
1125 },
d4e74fda
DB
1126 {
1127 'test_id': 14,
1128 'test_class': FioJobTest_t0014,
1129 'job': 't0014.fio',
1130 'success': SUCCESS_DEFAULT,
1131 'pre_job': None,
1132 'pre_success': None,
1133 'output_format': 'json',
1134 'requirements': [],
1135 },
60ebb939
VF
1136 {
1137 'test_id': 15,
1138 'test_class': FioJobTest_t0015,
1139 'job': 't0015-e78980ff.fio',
1140 'success': SUCCESS_DEFAULT,
1141 'pre_job': None,
1142 'pre_success': None,
1143 'output_format': 'json',
1144 'requirements': [Requirements.linux, Requirements.libaio],
1145 },
de31fe9a
VF
1146 {
1147 'test_id': 16,
1148 'test_class': FioJobTest_t0015,
f045d5f8 1149 'job': 't0016-d54ae22.fio',
de31fe9a
VF
1150 'success': SUCCESS_DEFAULT,
1151 'pre_job': None,
1152 'pre_success': None,
1153 'output_format': 'json',
1154 'requirements': [],
1155 },
31a58cba
VF
1156 {
1157 'test_id': 17,
1158 'test_class': FioJobTest_t0015,
1159 'job': 't0017.fio',
1160 'success': SUCCESS_DEFAULT,
1161 'pre_job': None,
1162 'pre_success': None,
1163 'output_format': 'json',
1164 'requirements': [Requirements.not_windows],
1165 },
a2947c33
VF
1166 {
1167 'test_id': 18,
1168 'test_class': FioJobTest,
1169 'job': 't0018.fio',
1170 'success': SUCCESS_DEFAULT,
1171 'pre_job': None,
1172 'pre_success': None,
1173 'requirements': [Requirements.linux, Requirements.io_uring],
1174 },
ef54f290
VF
1175 {
1176 'test_id': 19,
1177 'test_class': FioJobTest_t0019,
1178 'job': 't0019.fio',
1179 'success': SUCCESS_DEFAULT,
1180 'pre_job': None,
1181 'pre_success': None,
1182 'requirements': [],
1183 },
1184 {
1185 'test_id': 20,
1186 'test_class': FioJobTest_t0020,
1187 'job': 't0020.fio',
1188 'success': SUCCESS_DEFAULT,
1189 'pre_job': None,
1190 'pre_success': None,
1191 'requirements': [],
1192 },
eb40b275
VF
1193 {
1194 'test_id': 21,
1195 'test_class': FioJobTest_t0020,
1196 'job': 't0021.fio',
1197 'success': SUCCESS_DEFAULT,
1198 'pre_job': None,
1199 'pre_success': None,
1200 'requirements': [],
1201 },
1202 {
1203 'test_id': 22,
1204 'test_class': FioJobTest_t0022,
1205 'job': 't0022.fio',
1206 'success': SUCCESS_DEFAULT,
1207 'pre_job': None,
1208 'pre_success': None,
1209 'requirements': [],
1210 },
c37183f8
VF
1211 {
1212 'test_id': 23,
1213 'test_class': FioJobTest_t0023,
1214 'job': 't0023.fio',
1215 'success': SUCCESS_DEFAULT,
1216 'pre_job': None,
1217 'pre_success': None,
1218 'requirements': [],
1219 },
2d5c4600
VF
1220 {
1221 'test_id': 24,
1222 'test_class': FioJobTest_t0024,
1223 'job': 't0024.fio',
1224 'success': SUCCESS_DEFAULT,
1225 'pre_job': None,
1226 'pre_success': None,
1227 'requirements': [],
1228 },
d661fb18
SK
1229 {
1230 'test_id': 25,
1231 'test_class': FioJobTest_t0025,
1232 'job': 't0025.fio',
1233 'success': SUCCESS_DEFAULT,
1234 'pre_job': None,
1235 'pre_success': None,
1236 'output_format': 'json',
1237 'requirements': [],
1238 },
c4704c08
SK
1239 {
1240 'test_id': 26,
1241 'test_class': FioJobTest,
1242 'job': 't0026.fio',
1243 'success': SUCCESS_DEFAULT,
1244 'pre_job': None,
1245 'pre_success': None,
1246 'requirements': [Requirements.not_windows],
1247 },
ede04c27
LG
1248 {
1249 'test_id': 27,
1250 'test_class': FioJobTest_t0027,
1251 'job': 't0027.fio',
1252 'success': SUCCESS_DEFAULT,
1253 'pre_job': None,
1254 'pre_success': None,
1255 'requirements': [],
1256 },
23fb1642
VF
1257 {
1258 'test_id': 28,
1259 'test_class': FioJobTest,
1260 'job': 't0028-c6cade16.fio',
1261 'success': SUCCESS_DEFAULT,
1262 'pre_job': None,
1263 'pre_success': None,
1264 'requirements': [],
1265 },
704cc4df
VF
1266 {
1267 'test_id': 1000,
1268 'test_class': FioExeTest,
1269 'exe': 't/axmap',
1270 'parameters': None,
1271 'success': SUCCESS_DEFAULT,
1272 'requirements': [],
1273 },
1274 {
1275 'test_id': 1001,
1276 'test_class': FioExeTest,
1277 'exe': 't/ieee754',
1278 'parameters': None,
1279 'success': SUCCESS_DEFAULT,
1280 'requirements': [],
1281 },
1282 {
1283 'test_id': 1002,
1284 'test_class': FioExeTest,
1285 'exe': 't/lfsr-test',
1286 'parameters': ['0xFFFFFF', '0', '0', 'verify'],
1287 'success': SUCCESS_STDERR,
1288 'requirements': [],
1289 },
1290 {
1291 'test_id': 1003,
1292 'test_class': FioExeTest,
1293 'exe': 't/readonly.py',
1294 'parameters': ['-f', '{fio_path}'],
1295 'success': SUCCESS_DEFAULT,
1296 'requirements': [],
1297 },
1298 {
1299 'test_id': 1004,
1300 'test_class': FioExeTest,
1301 'exe': 't/steadystate_tests.py',
1302 'parameters': ['{fio_path}'],
1303 'success': SUCCESS_DEFAULT,
1304 'requirements': [],
1305 },
1306 {
1307 'test_id': 1005,
1308 'test_class': FioExeTest,
1309 'exe': 't/stest',
1310 'parameters': None,
1311 'success': SUCCESS_STDERR,
1312 'requirements': [],
1313 },
1314 {
1315 'test_id': 1006,
1316 'test_class': FioExeTest,
1317 'exe': 't/strided.py',
1318 'parameters': ['{fio_path}'],
1319 'success': SUCCESS_DEFAULT,
1320 'requirements': [],
1321 },
1322 {
1323 'test_id': 1007,
1324 'test_class': FioExeTest,
037b2b50
DF
1325 'exe': 't/zbd/run-tests-against-nullb',
1326 'parameters': ['-s', '1'],
704cc4df
VF
1327 'success': SUCCESS_DEFAULT,
1328 'requirements': [Requirements.linux, Requirements.zbd,
1329 Requirements.root],
1330 },
1331 {
1332 'test_id': 1008,
1333 'test_class': FioExeTest,
037b2b50
DF
1334 'exe': 't/zbd/run-tests-against-nullb',
1335 'parameters': ['-s', '2'],
704cc4df
VF
1336 'success': SUCCESS_DEFAULT,
1337 'requirements': [Requirements.linux, Requirements.zbd,
1338 Requirements.root, Requirements.zoned_nullb],
1339 },
1340 {
1341 'test_id': 1009,
1342 'test_class': FioExeTest,
1343 'exe': 'unittests/unittest',
1344 'parameters': None,
1345 'success': SUCCESS_DEFAULT,
1346 'requirements': [Requirements.unittests],
1347 },
1348 {
1349 'test_id': 1010,
1350 'test_class': FioExeTest,
1351 'exe': 't/latency_percentiles.py',
1352 'parameters': ['-f', '{fio_path}'],
1353 'success': SUCCESS_DEFAULT,
1354 'requirements': [],
1355 },
8403eca6
VF
1356 {
1357 'test_id': 1011,
1358 'test_class': FioExeTest,
1359 'exe': 't/jsonplus2csv_test.py',
1360 'parameters': ['-f', '{fio_path}'],
1361 'success': SUCCESS_DEFAULT,
1362 'requirements': [],
1363 },
03900b0b 1364 {
1365 'test_id': 1012,
1366 'test_class': FioExeTest,
1367 'exe': 't/log_compression.py',
1368 'parameters': ['-f', '{fio_path}'],
1369 'success': SUCCESS_DEFAULT,
1370 'requirements': [],
1371 },
c994fa62
VF
1372 {
1373 'test_id': 1013,
1374 'test_class': FioExeTest,
1375 'exe': 't/random_seed.py',
1376 'parameters': ['-f', '{fio_path}'],
1377 'success': SUCCESS_DEFAULT,
1378 'requirements': [],
1379 },
2128f93a
VF
1380 {
1381 'test_id': 1014,
1382 'test_class': FioExeTest,
1383 'exe': 't/nvmept.py',
1384 'parameters': ['-f', '{fio_path}', '--dut', '{nvmecdev}'],
1385 'success': SUCCESS_DEFAULT,
1386 'requirements': [Requirements.linux, Requirements.nvmecdev],
1387 },
df1eaa36
VF
1388]
1389
1390
1391def parse_args():
704cc4df
VF
1392 """Parse command-line arguments."""
1393
df1eaa36
VF
1394 parser = argparse.ArgumentParser()
1395 parser.add_argument('-r', '--fio-root',
1396 help='fio root path')
1397 parser.add_argument('-f', '--fio',
1398 help='path to fio executable (e.g., ./fio)')
1399 parser.add_argument('-a', '--artifact-root',
1400 help='artifact root directory')
1401 parser.add_argument('-s', '--skip', nargs='+', type=int,
1402 help='list of test(s) to skip')
1403 parser.add_argument('-o', '--run-only', nargs='+', type=int,
1404 help='list of test(s) to run, skipping all others')
6d5470c3
VF
1405 parser.add_argument('-d', '--debug', action='store_true',
1406 help='provide debug output')
b1bc705e
VF
1407 parser.add_argument('-k', '--skip-req', action='store_true',
1408 help='skip requirements checking')
58a77d2a
VF
1409 parser.add_argument('-p', '--pass-through', action='append',
1410 help='pass-through an argument to an executable test')
2128f93a
VF
1411 parser.add_argument('--nvmecdev', action='store', default=None,
1412 help='NVMe character device for **DESTRUCTIVE** testing (e.g., /dev/ng0n1)')
df1eaa36
VF
1413 args = parser.parse_args()
1414
1415 return args
1416
1417
1418def main():
704cc4df
VF
1419 """Entry point."""
1420
df1eaa36 1421 args = parse_args()
6d5470c3
VF
1422 if args.debug:
1423 logging.basicConfig(level=logging.DEBUG)
1424 else:
1425 logging.basicConfig(level=logging.INFO)
1426
58a77d2a
VF
1427 pass_through = {}
1428 if args.pass_through:
1429 for arg in args.pass_through:
1430 if not ':' in arg:
1431 print("Invalid --pass-through argument '%s'" % arg)
1432 print("Syntax for --pass-through is TESTNUMBER:ARGUMENT")
1433 return
061a0773 1434 split = arg.split(":", 1)
58a77d2a 1435 pass_through[int(split[0])] = split[1]
061a0773 1436 logging.debug("Pass-through arguments: %s", pass_through)
58a77d2a 1437
df1eaa36
VF
1438 if args.fio_root:
1439 fio_root = args.fio_root
1440 else:
6d5470c3
VF
1441 fio_root = str(Path(__file__).absolute().parent.parent)
1442 print("fio root is %s" % fio_root)
df1eaa36
VF
1443
1444 if args.fio:
1445 fio_path = args.fio
1446 else:
742b8799
VF
1447 if platform.system() == "Windows":
1448 fio_exe = "fio.exe"
1449 else:
1450 fio_exe = "fio"
1451 fio_path = os.path.join(fio_root, fio_exe)
6d5470c3
VF
1452 print("fio path is %s" % fio_path)
1453 if not shutil.which(fio_path):
1454 print("Warning: fio executable not found")
df1eaa36
VF
1455
1456 artifact_root = args.artifact_root if args.artifact_root else \
1457 "fio-test-{0}".format(time.strftime("%Y%m%d-%H%M%S"))
1458 os.mkdir(artifact_root)
1459 print("Artifact directory is %s" % artifact_root)
1460
b1bc705e 1461 if not args.skip_req:
2128f93a 1462 req = Requirements(fio_root, args)
b1bc705e 1463
df1eaa36
VF
1464 passed = 0
1465 failed = 0
1466 skipped = 0
1467
1468 for config in TEST_LIST:
1469 if (args.skip and config['test_id'] in args.skip) or \
1470 (args.run_only and config['test_id'] not in args.run_only):
1471 skipped = skipped + 1
b1bc705e 1472 print("Test {0} SKIPPED (User request)".format(config['test_id']))
df1eaa36
VF
1473 continue
1474
1475 if issubclass(config['test_class'], FioJobTest):
1476 if config['pre_job']:
1477 fio_pre_job = os.path.join(fio_root, 't', 'jobs',
1478 config['pre_job'])
1479 else:
1480 fio_pre_job = None
1481 if config['pre_success']:
1482 fio_pre_success = config['pre_success']
1483 else:
1484 fio_pre_success = None
1485 if 'output_format' in config:
1486 output_format = config['output_format']
1487 else:
1488 output_format = 'normal'
1489 test = config['test_class'](
1490 fio_path,
1491 os.path.join(fio_root, 't', 'jobs', config['job']),
1492 config['success'],
1493 fio_pre_job=fio_pre_job,
1494 fio_pre_success=fio_pre_success,
1495 output_format=output_format)
9393cdaa 1496 desc = config['job']
df1eaa36
VF
1497 elif issubclass(config['test_class'], FioExeTest):
1498 exe_path = os.path.join(fio_root, config['exe'])
1499 if config['parameters']:
2128f93a
VF
1500 parameters = [p.format(fio_path=fio_path, nvmecdev=args.nvmecdev)
1501 for p in config['parameters']]
df1eaa36 1502 else:
58a77d2a 1503 parameters = []
742b8799 1504 if Path(exe_path).suffix == '.py' and platform.system() == "Windows":
58a77d2a 1505 parameters.insert(0, exe_path)
742b8799 1506 exe_path = "python.exe"
58a77d2a
VF
1507 if config['test_id'] in pass_through:
1508 parameters += pass_through[config['test_id']].split()
df1eaa36
VF
1509 test = config['test_class'](exe_path, parameters,
1510 config['success'])
9393cdaa 1511 desc = config['exe']
df1eaa36
VF
1512 else:
1513 print("Test {0} FAILED: unable to process test config".format(config['test_id']))
1514 failed = failed + 1
1515 continue
1516
b1bc705e 1517 if not args.skip_req:
704cc4df 1518 reqs_met = True
b1bc705e 1519 for req in config['requirements']:
704cc4df
VF
1520 reqs_met, reason = req()
1521 logging.debug("Test %d: Requirement '%s' met? %s", config['test_id'], reason,
1522 reqs_met)
1523 if not reqs_met:
b1bc705e 1524 break
704cc4df 1525 if not reqs_met:
9393cdaa 1526 print("Test {0} SKIPPED ({1}) {2}".format(config['test_id'], reason, desc))
b1bc705e
VF
1527 skipped = skipped + 1
1528 continue
1529
aa9f2627
VF
1530 try:
1531 test.setup(artifact_root, config['test_id'])
1532 test.run()
1533 test.check_result()
1534 except KeyboardInterrupt:
1535 break
1536 except Exception as e:
1537 test.passed = False
1538 test.failure_reason += str(e)
1539 logging.debug("Test %d exception:\n%s\n", config['test_id'], traceback.format_exc())
df1eaa36
VF
1540 if test.passed:
1541 result = "PASSED"
1542 passed = passed + 1
1543 else:
1544 result = "FAILED: {0}".format(test.failure_reason)
1545 failed = failed + 1
15a73987
VF
1546 contents, _ = FioJobTest.get_file(test.stderr_file)
1547 logging.debug("Test %d: stderr:\n%s", config['test_id'], contents)
1548 contents, _ = FioJobTest.get_file(test.stdout_file)
1549 logging.debug("Test %d: stdout:\n%s", config['test_id'], contents)
9393cdaa 1550 print("Test {0} {1} {2}".format(config['test_id'], result, desc))
df1eaa36
VF
1551
1552 print("{0} test(s) passed, {1} failed, {2} skipped".format(passed, failed, skipped))
1553
1554 sys.exit(failed)
1555
1556
1557if __name__ == '__main__':
1558 main()