test: improve evaluation of t0020.fio and t0021.fio
[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):
df1eaa36
VF
825 """Test consists of fio test job t0009
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
866
867 def __init__(self, fio_root):
868 Requirements._not_macos = platform.system() != "Darwin"
c58b33b4 869 Requirements._not_windows = platform.system() != "Windows"
b1bc705e
VF
870 Requirements._linux = platform.system() == "Linux"
871
872 if Requirements._linux:
15a73987
VF
873 config_file = os.path.join(fio_root, "config-host.h")
874 contents, success = FioJobTest.get_file(config_file)
875 if not success:
b1bc705e
VF
876 print("Unable to open {0} to check requirements".format(config_file))
877 Requirements._zbd = True
878 else:
b7694961 879 Requirements._zbd = "CONFIG_HAS_BLKZONED" in contents
b1bc705e
VF
880 Requirements._libaio = "CONFIG_LIBAIO" in contents
881
a2947c33
VF
882 contents, success = FioJobTest.get_file("/proc/kallsyms")
883 if not success:
884 print("Unable to open '/proc/kallsyms' to probe for io_uring support")
885 else:
886 Requirements._io_uring = "io_uring_setup" in contents
887
b1bc705e
VF
888 Requirements._root = (os.geteuid() == 0)
889 if Requirements._zbd and Requirements._root:
8854e368
VF
890 try:
891 subprocess.run(["modprobe", "null_blk"],
892 stdout=subprocess.PIPE,
893 stderr=subprocess.PIPE)
894 if os.path.exists("/sys/module/null_blk/parameters/zoned"):
895 Requirements._zoned_nullb = True
896 except Exception:
897 pass
b1bc705e 898
742b8799
VF
899 if platform.system() == "Windows":
900 utest_exe = "unittest.exe"
901 else:
902 utest_exe = "unittest"
903 unittest_path = os.path.join(fio_root, "unittests", utest_exe)
b1bc705e
VF
904 Requirements._unittests = os.path.exists(unittest_path)
905
906 Requirements._cpucount4 = multiprocessing.cpu_count() >= 4
907
908 req_list = [Requirements.linux,
909 Requirements.libaio,
a2947c33 910 Requirements.io_uring,
b1bc705e
VF
911 Requirements.zbd,
912 Requirements.root,
913 Requirements.zoned_nullb,
914 Requirements.not_macos,
c58b33b4 915 Requirements.not_windows,
b1bc705e
VF
916 Requirements.unittests,
917 Requirements.cpucount4]
918 for req in req_list:
919 value, desc = req()
704cc4df 920 logging.debug("Requirements: Requirement '%s' met? %s", desc, value)
b1bc705e 921
704cc4df
VF
922 @classmethod
923 def linux(cls):
924 """Are we running on Linux?"""
b1bc705e
VF
925 return Requirements._linux, "Linux required"
926
704cc4df
VF
927 @classmethod
928 def libaio(cls):
929 """Is libaio available?"""
b1bc705e
VF
930 return Requirements._libaio, "libaio required"
931
a2947c33
VF
932 @classmethod
933 def io_uring(cls):
934 """Is io_uring available?"""
935 return Requirements._io_uring, "io_uring required"
936
704cc4df
VF
937 @classmethod
938 def zbd(cls):
939 """Is ZBD support available?"""
b1bc705e
VF
940 return Requirements._zbd, "Zoned block device support required"
941
704cc4df
VF
942 @classmethod
943 def root(cls):
944 """Are we running as root?"""
b1bc705e
VF
945 return Requirements._root, "root required"
946
704cc4df
VF
947 @classmethod
948 def zoned_nullb(cls):
949 """Are zoned null block devices available?"""
b1bc705e
VF
950 return Requirements._zoned_nullb, "Zoned null block device support required"
951
704cc4df
VF
952 @classmethod
953 def not_macos(cls):
954 """Are we running on a platform other than macOS?"""
b1bc705e
VF
955 return Requirements._not_macos, "platform other than macOS required"
956
704cc4df
VF
957 @classmethod
958 def not_windows(cls):
959 """Are we running on a platform other than Windws?"""
c58b33b4
VF
960 return Requirements._not_windows, "platform other than Windows required"
961
704cc4df
VF
962 @classmethod
963 def unittests(cls):
964 """Were unittests built?"""
b1bc705e
VF
965 return Requirements._unittests, "Unittests support required"
966
704cc4df
VF
967 @classmethod
968 def cpucount4(cls):
969 """Do we have at least 4 CPUs?"""
b1bc705e
VF
970 return Requirements._cpucount4, "4+ CPUs required"
971
972
df1eaa36 973SUCCESS_DEFAULT = {
704cc4df
VF
974 'zero_return': True,
975 'stderr_empty': True,
976 'timeout': 600,
977 }
df1eaa36 978SUCCESS_NONZERO = {
704cc4df
VF
979 'zero_return': False,
980 'stderr_empty': False,
981 'timeout': 600,
982 }
df1eaa36 983SUCCESS_STDERR = {
704cc4df
VF
984 'zero_return': True,
985 'stderr_empty': False,
986 'timeout': 600,
987 }
df1eaa36 988TEST_LIST = [
704cc4df
VF
989 {
990 'test_id': 1,
991 'test_class': FioJobTest,
992 'job': 't0001-52c58027.fio',
993 'success': SUCCESS_DEFAULT,
994 'pre_job': None,
995 'pre_success': None,
996 'requirements': [],
997 },
998 {
999 'test_id': 2,
1000 'test_class': FioJobTest,
1001 'job': 't0002-13af05ae-post.fio',
1002 'success': SUCCESS_DEFAULT,
1003 'pre_job': 't0002-13af05ae-pre.fio',
1004 'pre_success': None,
1005 'requirements': [Requirements.linux, Requirements.libaio],
1006 },
1007 {
1008 'test_id': 3,
1009 'test_class': FioJobTest,
1010 'job': 't0003-0ae2c6e1-post.fio',
1011 'success': SUCCESS_NONZERO,
1012 'pre_job': 't0003-0ae2c6e1-pre.fio',
1013 'pre_success': SUCCESS_DEFAULT,
1014 'requirements': [Requirements.linux, Requirements.libaio],
1015 },
1016 {
1017 'test_id': 4,
1018 'test_class': FioJobTest,
1019 'job': 't0004-8a99fdf6.fio',
1020 'success': SUCCESS_DEFAULT,
1021 'pre_job': None,
1022 'pre_success': None,
1023 'requirements': [Requirements.linux, Requirements.libaio],
1024 },
1025 {
1026 'test_id': 5,
1027 'test_class': FioJobTest_t0005,
1028 'job': 't0005-f7078f7b.fio',
1029 'success': SUCCESS_DEFAULT,
1030 'pre_job': None,
1031 'pre_success': None,
1032 'output_format': 'json',
1033 'requirements': [Requirements.not_windows],
1034 },
1035 {
1036 'test_id': 6,
1037 'test_class': FioJobTest_t0006,
1038 'job': 't0006-82af2a7c.fio',
1039 'success': SUCCESS_DEFAULT,
1040 'pre_job': None,
1041 'pre_success': None,
1042 'output_format': 'json',
1043 'requirements': [Requirements.linux, Requirements.libaio],
1044 },
1045 {
1046 'test_id': 7,
1047 'test_class': FioJobTest_t0007,
1048 'job': 't0007-37cf9e3c.fio',
1049 'success': SUCCESS_DEFAULT,
1050 'pre_job': None,
1051 'pre_success': None,
1052 'output_format': 'json',
1053 'requirements': [],
1054 },
1055 {
1056 'test_id': 8,
1057 'test_class': FioJobTest_t0008,
1058 'job': 't0008-ae2fafc8.fio',
1059 'success': SUCCESS_DEFAULT,
1060 'pre_job': None,
1061 'pre_success': None,
1062 'output_format': 'json',
1063 'requirements': [],
1064 },
1065 {
1066 'test_id': 9,
1067 'test_class': FioJobTest_t0009,
1068 'job': 't0009-f8b0bd10.fio',
1069 'success': SUCCESS_DEFAULT,
1070 'pre_job': None,
1071 'pre_success': None,
1072 'output_format': 'json',
1073 'requirements': [Requirements.not_macos,
1074 Requirements.cpucount4],
1075 # mac os does not support CPU affinity
1076 },
1077 {
1078 'test_id': 10,
1079 'test_class': FioJobTest,
1080 'job': 't0010-b7aae4ba.fio',
1081 'success': SUCCESS_DEFAULT,
1082 'pre_job': None,
1083 'pre_success': None,
1084 'requirements': [],
1085 },
1086 {
1087 'test_id': 11,
0a602473 1088 'test_class': FioJobTest_iops_rate,
704cc4df
VF
1089 'job': 't0011-5d2788d5.fio',
1090 'success': SUCCESS_DEFAULT,
1091 'pre_job': None,
1092 'pre_success': None,
1093 'output_format': 'json',
1094 'requirements': [],
1095 },
0a602473
BVA
1096 {
1097 'test_id': 12,
d4e74fda 1098 'test_class': FioJobTest_t0012,
0a602473
BVA
1099 'job': 't0012.fio',
1100 'success': SUCCESS_DEFAULT,
1101 'pre_job': None,
1102 'pre_success': None,
1103 'output_format': 'json',
d4e74fda 1104 'requirements': [],
0a602473 1105 },
f0c7ae7a
BVA
1106 {
1107 'test_id': 13,
061a0773 1108 'test_class': FioJobTest,
f0c7ae7a
BVA
1109 'job': 't0013.fio',
1110 'success': SUCCESS_DEFAULT,
1111 'pre_job': None,
1112 'pre_success': None,
1113 'output_format': 'json',
1114 'requirements': [],
1115 },
d4e74fda
DB
1116 {
1117 'test_id': 14,
1118 'test_class': FioJobTest_t0014,
1119 'job': 't0014.fio',
1120 'success': SUCCESS_DEFAULT,
1121 'pre_job': None,
1122 'pre_success': None,
1123 'output_format': 'json',
1124 'requirements': [],
1125 },
60ebb939
VF
1126 {
1127 'test_id': 15,
1128 'test_class': FioJobTest_t0015,
1129 'job': 't0015-e78980ff.fio',
1130 'success': SUCCESS_DEFAULT,
1131 'pre_job': None,
1132 'pre_success': None,
1133 'output_format': 'json',
1134 'requirements': [Requirements.linux, Requirements.libaio],
1135 },
de31fe9a
VF
1136 {
1137 'test_id': 16,
1138 'test_class': FioJobTest_t0015,
f045d5f8 1139 'job': 't0016-d54ae22.fio',
de31fe9a
VF
1140 'success': SUCCESS_DEFAULT,
1141 'pre_job': None,
1142 'pre_success': None,
1143 'output_format': 'json',
1144 'requirements': [],
1145 },
31a58cba
VF
1146 {
1147 'test_id': 17,
1148 'test_class': FioJobTest_t0015,
1149 'job': 't0017.fio',
1150 'success': SUCCESS_DEFAULT,
1151 'pre_job': None,
1152 'pre_success': None,
1153 'output_format': 'json',
1154 'requirements': [Requirements.not_windows],
1155 },
a2947c33
VF
1156 {
1157 'test_id': 18,
1158 'test_class': FioJobTest,
1159 'job': 't0018.fio',
1160 'success': SUCCESS_DEFAULT,
1161 'pre_job': None,
1162 'pre_success': None,
1163 'requirements': [Requirements.linux, Requirements.io_uring],
1164 },
ef54f290
VF
1165 {
1166 'test_id': 19,
1167 'test_class': FioJobTest_t0019,
1168 'job': 't0019.fio',
1169 'success': SUCCESS_DEFAULT,
1170 'pre_job': None,
1171 'pre_success': None,
1172 'requirements': [],
1173 },
1174 {
1175 'test_id': 20,
1176 'test_class': FioJobTest_t0020,
1177 'job': 't0020.fio',
1178 'success': SUCCESS_DEFAULT,
1179 'pre_job': None,
1180 'pre_success': None,
1181 'requirements': [],
1182 },
eb40b275
VF
1183 {
1184 'test_id': 21,
1185 'test_class': FioJobTest_t0020,
1186 'job': 't0021.fio',
1187 'success': SUCCESS_DEFAULT,
1188 'pre_job': None,
1189 'pre_success': None,
1190 'requirements': [],
1191 },
1192 {
1193 'test_id': 22,
1194 'test_class': FioJobTest_t0022,
1195 'job': 't0022.fio',
1196 'success': SUCCESS_DEFAULT,
1197 'pre_job': None,
1198 'pre_success': None,
1199 'requirements': [],
1200 },
c37183f8
VF
1201 {
1202 'test_id': 23,
1203 'test_class': FioJobTest_t0023,
1204 'job': 't0023.fio',
1205 'success': SUCCESS_DEFAULT,
1206 'pre_job': None,
1207 'pre_success': None,
1208 'requirements': [],
1209 },
2d5c4600
VF
1210 {
1211 'test_id': 24,
1212 'test_class': FioJobTest_t0024,
1213 'job': 't0024.fio',
1214 'success': SUCCESS_DEFAULT,
1215 'pre_job': None,
1216 'pre_success': None,
1217 'requirements': [],
1218 },
d661fb18
SK
1219 {
1220 'test_id': 25,
1221 'test_class': FioJobTest_t0025,
1222 'job': 't0025.fio',
1223 'success': SUCCESS_DEFAULT,
1224 'pre_job': None,
1225 'pre_success': None,
1226 'output_format': 'json',
1227 'requirements': [],
1228 },
c4704c08
SK
1229 {
1230 'test_id': 26,
1231 'test_class': FioJobTest,
1232 'job': 't0026.fio',
1233 'success': SUCCESS_DEFAULT,
1234 'pre_job': None,
1235 'pre_success': None,
1236 'requirements': [Requirements.not_windows],
1237 },
ede04c27
LG
1238 {
1239 'test_id': 27,
1240 'test_class': FioJobTest_t0027,
1241 'job': 't0027.fio',
1242 'success': SUCCESS_DEFAULT,
1243 'pre_job': None,
1244 'pre_success': None,
1245 'requirements': [],
1246 },
23fb1642
VF
1247 {
1248 'test_id': 28,
1249 'test_class': FioJobTest,
1250 'job': 't0028-c6cade16.fio',
1251 'success': SUCCESS_DEFAULT,
1252 'pre_job': None,
1253 'pre_success': None,
1254 'requirements': [],
1255 },
704cc4df
VF
1256 {
1257 'test_id': 1000,
1258 'test_class': FioExeTest,
1259 'exe': 't/axmap',
1260 'parameters': None,
1261 'success': SUCCESS_DEFAULT,
1262 'requirements': [],
1263 },
1264 {
1265 'test_id': 1001,
1266 'test_class': FioExeTest,
1267 'exe': 't/ieee754',
1268 'parameters': None,
1269 'success': SUCCESS_DEFAULT,
1270 'requirements': [],
1271 },
1272 {
1273 'test_id': 1002,
1274 'test_class': FioExeTest,
1275 'exe': 't/lfsr-test',
1276 'parameters': ['0xFFFFFF', '0', '0', 'verify'],
1277 'success': SUCCESS_STDERR,
1278 'requirements': [],
1279 },
1280 {
1281 'test_id': 1003,
1282 'test_class': FioExeTest,
1283 'exe': 't/readonly.py',
1284 'parameters': ['-f', '{fio_path}'],
1285 'success': SUCCESS_DEFAULT,
1286 'requirements': [],
1287 },
1288 {
1289 'test_id': 1004,
1290 'test_class': FioExeTest,
1291 'exe': 't/steadystate_tests.py',
1292 'parameters': ['{fio_path}'],
1293 'success': SUCCESS_DEFAULT,
1294 'requirements': [],
1295 },
1296 {
1297 'test_id': 1005,
1298 'test_class': FioExeTest,
1299 'exe': 't/stest',
1300 'parameters': None,
1301 'success': SUCCESS_STDERR,
1302 'requirements': [],
1303 },
1304 {
1305 'test_id': 1006,
1306 'test_class': FioExeTest,
1307 'exe': 't/strided.py',
1308 'parameters': ['{fio_path}'],
1309 'success': SUCCESS_DEFAULT,
1310 'requirements': [],
1311 },
1312 {
1313 'test_id': 1007,
1314 'test_class': FioExeTest,
037b2b50
DF
1315 'exe': 't/zbd/run-tests-against-nullb',
1316 'parameters': ['-s', '1'],
704cc4df
VF
1317 'success': SUCCESS_DEFAULT,
1318 'requirements': [Requirements.linux, Requirements.zbd,
1319 Requirements.root],
1320 },
1321 {
1322 'test_id': 1008,
1323 'test_class': FioExeTest,
037b2b50
DF
1324 'exe': 't/zbd/run-tests-against-nullb',
1325 'parameters': ['-s', '2'],
704cc4df
VF
1326 'success': SUCCESS_DEFAULT,
1327 'requirements': [Requirements.linux, Requirements.zbd,
1328 Requirements.root, Requirements.zoned_nullb],
1329 },
1330 {
1331 'test_id': 1009,
1332 'test_class': FioExeTest,
1333 'exe': 'unittests/unittest',
1334 'parameters': None,
1335 'success': SUCCESS_DEFAULT,
1336 'requirements': [Requirements.unittests],
1337 },
1338 {
1339 'test_id': 1010,
1340 'test_class': FioExeTest,
1341 'exe': 't/latency_percentiles.py',
1342 'parameters': ['-f', '{fio_path}'],
1343 'success': SUCCESS_DEFAULT,
1344 'requirements': [],
1345 },
8403eca6
VF
1346 {
1347 'test_id': 1011,
1348 'test_class': FioExeTest,
1349 'exe': 't/jsonplus2csv_test.py',
1350 'parameters': ['-f', '{fio_path}'],
1351 'success': SUCCESS_DEFAULT,
1352 'requirements': [],
1353 },
03900b0b 1354 {
1355 'test_id': 1012,
1356 'test_class': FioExeTest,
1357 'exe': 't/log_compression.py',
1358 'parameters': ['-f', '{fio_path}'],
1359 'success': SUCCESS_DEFAULT,
1360 'requirements': [],
1361 },
c994fa62
VF
1362 {
1363 'test_id': 1013,
1364 'test_class': FioExeTest,
1365 'exe': 't/random_seed.py',
1366 'parameters': ['-f', '{fio_path}'],
1367 'success': SUCCESS_DEFAULT,
1368 'requirements': [],
1369 },
df1eaa36
VF
1370]
1371
1372
1373def parse_args():
704cc4df
VF
1374 """Parse command-line arguments."""
1375
df1eaa36
VF
1376 parser = argparse.ArgumentParser()
1377 parser.add_argument('-r', '--fio-root',
1378 help='fio root path')
1379 parser.add_argument('-f', '--fio',
1380 help='path to fio executable (e.g., ./fio)')
1381 parser.add_argument('-a', '--artifact-root',
1382 help='artifact root directory')
1383 parser.add_argument('-s', '--skip', nargs='+', type=int,
1384 help='list of test(s) to skip')
1385 parser.add_argument('-o', '--run-only', nargs='+', type=int,
1386 help='list of test(s) to run, skipping all others')
6d5470c3
VF
1387 parser.add_argument('-d', '--debug', action='store_true',
1388 help='provide debug output')
b1bc705e
VF
1389 parser.add_argument('-k', '--skip-req', action='store_true',
1390 help='skip requirements checking')
58a77d2a
VF
1391 parser.add_argument('-p', '--pass-through', action='append',
1392 help='pass-through an argument to an executable test')
df1eaa36
VF
1393 args = parser.parse_args()
1394
1395 return args
1396
1397
1398def main():
704cc4df
VF
1399 """Entry point."""
1400
df1eaa36 1401 args = parse_args()
6d5470c3
VF
1402 if args.debug:
1403 logging.basicConfig(level=logging.DEBUG)
1404 else:
1405 logging.basicConfig(level=logging.INFO)
1406
58a77d2a
VF
1407 pass_through = {}
1408 if args.pass_through:
1409 for arg in args.pass_through:
1410 if not ':' in arg:
1411 print("Invalid --pass-through argument '%s'" % arg)
1412 print("Syntax for --pass-through is TESTNUMBER:ARGUMENT")
1413 return
061a0773 1414 split = arg.split(":", 1)
58a77d2a 1415 pass_through[int(split[0])] = split[1]
061a0773 1416 logging.debug("Pass-through arguments: %s", pass_through)
58a77d2a 1417
df1eaa36
VF
1418 if args.fio_root:
1419 fio_root = args.fio_root
1420 else:
6d5470c3
VF
1421 fio_root = str(Path(__file__).absolute().parent.parent)
1422 print("fio root is %s" % fio_root)
df1eaa36
VF
1423
1424 if args.fio:
1425 fio_path = args.fio
1426 else:
742b8799
VF
1427 if platform.system() == "Windows":
1428 fio_exe = "fio.exe"
1429 else:
1430 fio_exe = "fio"
1431 fio_path = os.path.join(fio_root, fio_exe)
6d5470c3
VF
1432 print("fio path is %s" % fio_path)
1433 if not shutil.which(fio_path):
1434 print("Warning: fio executable not found")
df1eaa36
VF
1435
1436 artifact_root = args.artifact_root if args.artifact_root else \
1437 "fio-test-{0}".format(time.strftime("%Y%m%d-%H%M%S"))
1438 os.mkdir(artifact_root)
1439 print("Artifact directory is %s" % artifact_root)
1440
b1bc705e
VF
1441 if not args.skip_req:
1442 req = Requirements(fio_root)
1443
df1eaa36
VF
1444 passed = 0
1445 failed = 0
1446 skipped = 0
1447
1448 for config in TEST_LIST:
1449 if (args.skip and config['test_id'] in args.skip) or \
1450 (args.run_only and config['test_id'] not in args.run_only):
1451 skipped = skipped + 1
b1bc705e 1452 print("Test {0} SKIPPED (User request)".format(config['test_id']))
df1eaa36
VF
1453 continue
1454
1455 if issubclass(config['test_class'], FioJobTest):
1456 if config['pre_job']:
1457 fio_pre_job = os.path.join(fio_root, 't', 'jobs',
1458 config['pre_job'])
1459 else:
1460 fio_pre_job = None
1461 if config['pre_success']:
1462 fio_pre_success = config['pre_success']
1463 else:
1464 fio_pre_success = None
1465 if 'output_format' in config:
1466 output_format = config['output_format']
1467 else:
1468 output_format = 'normal'
1469 test = config['test_class'](
1470 fio_path,
1471 os.path.join(fio_root, 't', 'jobs', config['job']),
1472 config['success'],
1473 fio_pre_job=fio_pre_job,
1474 fio_pre_success=fio_pre_success,
1475 output_format=output_format)
9393cdaa 1476 desc = config['job']
df1eaa36
VF
1477 elif issubclass(config['test_class'], FioExeTest):
1478 exe_path = os.path.join(fio_root, config['exe'])
1479 if config['parameters']:
1480 parameters = [p.format(fio_path=fio_path) for p in config['parameters']]
1481 else:
58a77d2a 1482 parameters = []
742b8799 1483 if Path(exe_path).suffix == '.py' and platform.system() == "Windows":
58a77d2a 1484 parameters.insert(0, exe_path)
742b8799 1485 exe_path = "python.exe"
58a77d2a
VF
1486 if config['test_id'] in pass_through:
1487 parameters += pass_through[config['test_id']].split()
df1eaa36
VF
1488 test = config['test_class'](exe_path, parameters,
1489 config['success'])
9393cdaa 1490 desc = config['exe']
df1eaa36
VF
1491 else:
1492 print("Test {0} FAILED: unable to process test config".format(config['test_id']))
1493 failed = failed + 1
1494 continue
1495
b1bc705e 1496 if not args.skip_req:
704cc4df 1497 reqs_met = True
b1bc705e 1498 for req in config['requirements']:
704cc4df
VF
1499 reqs_met, reason = req()
1500 logging.debug("Test %d: Requirement '%s' met? %s", config['test_id'], reason,
1501 reqs_met)
1502 if not reqs_met:
b1bc705e 1503 break
704cc4df 1504 if not reqs_met:
9393cdaa 1505 print("Test {0} SKIPPED ({1}) {2}".format(config['test_id'], reason, desc))
b1bc705e
VF
1506 skipped = skipped + 1
1507 continue
1508
aa9f2627
VF
1509 try:
1510 test.setup(artifact_root, config['test_id'])
1511 test.run()
1512 test.check_result()
1513 except KeyboardInterrupt:
1514 break
1515 except Exception as e:
1516 test.passed = False
1517 test.failure_reason += str(e)
1518 logging.debug("Test %d exception:\n%s\n", config['test_id'], traceback.format_exc())
df1eaa36
VF
1519 if test.passed:
1520 result = "PASSED"
1521 passed = passed + 1
1522 else:
1523 result = "FAILED: {0}".format(test.failure_reason)
1524 failed = failed + 1
15a73987
VF
1525 contents, _ = FioJobTest.get_file(test.stderr_file)
1526 logging.debug("Test %d: stderr:\n%s", config['test_id'], contents)
1527 contents, _ = FioJobTest.get_file(test.stdout_file)
1528 logging.debug("Test %d: stdout:\n%s", config['test_id'], contents)
9393cdaa 1529 print("Test {0} {1} {2}".format(config['test_id'], result, desc))
df1eaa36
VF
1530
1531 print("{0} test(s) passed, {1} failed, {2} skipped".format(passed, failed, skipped))
1532
1533 sys.exit(failed)
1534
1535
1536if __name__ == '__main__':
1537 main()