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