test: fix style issues in run-fio-tests.py
[fio.git] / t / run-fio-tests.py
1 #!/usr/bin/env python3
2 # SPDX-License-Identifier: GPL-2.0-only
3 #
4 # Copyright (c) 2019 Western Digital Corporation or its affiliates.
5 #
6 """
7 # run-fio-tests.py
8 #
9 # Automate running of fio tests
10 #
11 # USAGE
12 # python3 run-fio-tests.py [-r fio-root] [-f fio-path] [-a artifact-root]
13 #                           [--skip # # #...] [--run-only # # #...]
14 #
15 #
16 # EXAMPLE
17 # # git clone git://git.kernel.dk/fio.git
18 # # cd fio
19 # # make -j
20 # # python3 t/run-fio-tests.py
21 #
22 #
23 # REQUIREMENTS
24 # - Python 3.5 (subprocess.run)
25 # - Linux (libaio ioengine, zbd tests, etc)
26 # - The artifact directory must be on a file system that accepts 512-byte IO
27 #   (t0002, t0003, t0004).
28 # - The artifact directory needs to be on an SSD. Otherwise tests that carry
29 #   out file-based IO will trigger a timeout (t0006).
30 # - 4 CPUs (t0009)
31 # - SciPy (steadystate_tests.py)
32 # - libzbc (zbd tests)
33 # - root privileges (zbd test)
34 # - kernel 4.19 or later for zoned null block devices (zbd tests)
35 # - CUnit support (unittests)
36 #
37 """
38
39 #
40 # TODO  run multiple tests simultaneously
41 # TODO  Add sgunmap tests (requires SAS SSD)
42 #
43
44 import os
45 import sys
46 import json
47 import time
48 import shutil
49 import logging
50 import argparse
51 import platform
52 import traceback
53 import subprocess
54 import multiprocessing
55 from pathlib import Path
56
57
58 class FioTest(object):
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 = ''
71         self.command_file = None
72         self.stdout_file = None
73         self.stderr_file = None
74         self.exitcode_file = None
75
76     def setup(self, artifact_root, testnum):
77         """Setup instance variables for test."""
78
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(
86             self.test_dir,
87             "{0}.command".format(os.path.basename(self.exe_path)))
88         self.stdout_file = os.path.join(
89             self.test_dir,
90             "{0}.stdout".format(os.path.basename(self.exe_path)))
91         self.stderr_file = os.path.join(
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)))
97
98     def run(self):
99         """Run the test."""
100
101         raise NotImplementedError()
102
103     def check_result(self):
104         """Check test results."""
105
106         raise NotImplementedError()
107
108
109 class 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
123     def run(self):
124         """Execute the binary or script described by this instance."""
125
126         command = [self.exe_path] + self.parameters
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+")
133         exitcode_file = open(self.exitcode_file, "w+")
134         try:
135             proc = None
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'])
146             exitcode_file.write('{0}\n'.format(proc.returncode))
147             logging.debug("Test %d: return code: %d", self.testnum, proc.returncode)
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:
155             if proc:
156                 if not proc.poll():
157                     proc.terminate()
158                     proc.communicate()
159             self.output['failure'] = 'exception'
160             self.output['exc_info'] = sys.exc_info()
161         finally:
162             stdout_file.close()
163             stderr_file.close()
164             exitcode_file.close()
165
166     def check_result(self):
167         """Check results of test run."""
168
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(
175                     self.failure_reason, self.output['exc_info'][0],
176                     self.output['exc_info'][1])
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
191         stderr_size = os.path.getsize(self.stderr_file)
192         if 'stderr_empty' in self.success:
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
203 class 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 = [
227             "--max-jobs=16",
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):
235         """Setup instance variables for fio job test."""
236
237         super(FioJobTest, self).setup(artifact_root, testnum)
238
239         self.command_file = os.path.join(
240             self.test_dir,
241             "{0}.command".format(os.path.basename(self.fio_job)))
242         self.stdout_file = os.path.join(
243             self.test_dir,
244             "{0}.stdout".format(os.path.basename(self.fio_job)))
245         self.stderr_file = os.path.join(
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)))
251
252     def run_pre_job(self):
253         """Run fio job precondition step."""
254
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):
265         """Run fio job test."""
266
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:
273             logging.debug("Test %d: precondition step failed", self.testnum)
274
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
289     def check_result(self):
290         """Check fio job results."""
291
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
299         if not self.passed:
300             return
301
302         if 'json' not in self.output_format:
303             return
304
305         file_data, success = self.get_file(os.path.join(self.test_dir, self.fio_output))
306         if not success:
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
314         # data, skipping everything until the first {
315         #
316         lines = file_data.splitlines()
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
323
324
325 class 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
343 class 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']
355         logging.debug("Test %d: ratio: %f", self.testnum, ratio)
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
361 class 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
376 class 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
391         logging.debug("Test %d: ratio: %f", self.testnum, ratio)
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
401 class 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
411         logging.debug('Test %d: elapsed: %d', self.testnum, self.json_data['jobs'][0]['elapsed'])
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
418 class 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 = []
430         for i in range(1,4):
431             file_data, success = self.get_file(os.path.join(self.test_dir, "{0}_iops.{1}.log".format(os.path.basename(self.fio_job), i)))
432
433             if not success:
434                 self.failure_reason = "{0} unable to open output file,".format(self.failure_reason)
435                 self.passed = False
436                 return
437
438             iops_files.append(file_data.splitlines())
439
440         # there are 9 samples for job1 and job2, 4 samples for job3
441         iops1 = 0.0
442         iops2 = 0.0
443         iops3 = 0.0
444         for i in range(9):
445             iops1 = iops1 + float(iops_files[0][i].split(',')[1])
446             iops2 = iops2 + float(iops_files[1][i].split(',')[1])
447             iops3 = iops3 + float(iops_files[2][i].split(',')[1])
448
449             ratio1 = iops3/iops2
450             ratio2 = iops3/iops1
451             logging.debug(
452                 "sample {0}: job1 iops={1} job2 iops={2} job3 iops={3} job3/job2={4:.3f} job3/job1={5:.3f}".format(
453                     i, iops1, iops2, iops3, ratio1, ratio2
454                 )
455             )
456
457         # test job1 and job2 succeeded to recalibrate
458         if ratio1 < 1 or ratio1 > 3 or ratio2 < 7 or ratio2 > 13:
459             self.failure_reason = "{0} iops ratio mismatch iops1={1} iops2={2} iops3={3} expected r1~2 r2~10 got r1={4:.3f} r2={5:.3f},".format(
460                 self.failure_reason, iops1, iops2, iops3, ratio1, ratio2
461             )
462             self.passed = False
463             return
464
465
466 class 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 = []
481         for i in range(1,4):
482             file_data, success = self.get_file(os.path.join(self.test_dir, "{0}_iops.{1}.log".format(os.path.basename(self.fio_job), i)))
483
484             if not success:
485                 self.failure_reason = "{0} unable to open output file,".format(self.failure_reason)
486                 self.passed = False
487                 return
488
489             iops_files.append(file_data.splitlines())
490
491         # there are 9 samples for job1 and job2, 4 samples for job3
492         iops1 = 0.0
493         iops2 = 0.0
494         iops3 = 0.0
495         for i in range(9):
496             if i < 4:
497                 iops3 = iops3 + float(iops_files[2][i].split(',')[1])
498             elif i == 4:
499                 ratio1 = iops1 / iops2
500                 ratio2 = iops1 / iops3
501
502
503                 if ratio1 < 0.43 or ratio1 > 0.57 or ratio2 < 0.21 or ratio2 > 0.45:
504                     self.failure_reason = "{0} iops ratio mismatch iops1={1} iops2={2} iops3={3}\
505                                                 expected r1~0.5 r2~0.33 got r1={4:.3f} r2={5:.3f},".format(
506                         self.failure_reason, iops1, iops2, iops3, ratio1, ratio2
507                     )
508                     self.passed = False
509
510             iops1 = iops1 + float(iops_files[0][i].split(',')[1])
511             iops2 = iops2 + float(iops_files[1][i].split(',')[1])
512
513             ratio1 = iops1/iops2
514             ratio2 = iops1/iops3
515             logging.debug(
516                 "sample {0}: job1 iops={1} job2 iops={2} job3 iops={3} job1/job2={4:.3f} job1/job3={5:.3f}".format(
517                     i, iops1, iops2, iops3, ratio1, ratio2
518                 )
519             )
520
521         # test job1 and job2 succeeded to recalibrate
522         if ratio1 < 0.43 or ratio1 > 0.57:
523             self.failure_reason = "{0} iops ratio mismatch iops1={1} iops2={2} expected ratio~0.5 got ratio={3:.3f},".format(
524                 self.failure_reason, iops1, iops2, ratio1
525             )
526             self.passed = False
527             return
528
529
530 class FioJobTest_t0015(FioJobTest):
531     """Test consists of fio test jobs t0015 and t0016
532     Confirm that mean(slat) + mean(clat) = mean(tlat)"""
533
534     def check_result(self):
535         super(FioJobTest_t0015, self).check_result()
536
537         if not self.passed:
538             return
539
540         slat = self.json_data['jobs'][0]['read']['slat_ns']['mean']
541         clat = self.json_data['jobs'][0]['read']['clat_ns']['mean']
542         tlat = self.json_data['jobs'][0]['read']['lat_ns']['mean']
543         logging.debug('Test %d: slat %f, clat %f, tlat %f', self.testnum, slat, clat, tlat)
544
545         if abs(slat + clat - tlat) > 1:
546             self.failure_reason = "{0} slat {1} + clat {2} = {3} != tlat {4},".format(
547                 self.failure_reason, slat, clat, slat+clat, tlat)
548             self.passed = False
549
550
551 class FioJobTest_t0019(FioJobTest):
552     """Test consists of fio test job t0019
553     Confirm that all offsets were touched sequentially"""
554
555     def check_result(self):
556         super(FioJobTest_t0019, self).check_result()
557
558         bw_log_filename = os.path.join(self.test_dir, "test_bw.log")
559         file_data, success = self.get_file(bw_log_filename)
560         log_lines = file_data.split('\n')
561
562         prev = -4096
563         for line in log_lines:
564             if len(line.strip()) == 0:
565                 continue
566             cur = int(line.split(',')[4])
567             if cur - prev != 4096:
568                 self.passed = False
569                 self.failure_reason = "offsets {0}, {1} not sequential".format(prev, cur)
570                 return
571             prev = cur
572
573         if cur/4096 != 255:
574             self.passed = False
575             self.failure_reason = "unexpected last offset {0}".format(cur)
576
577
578 class FioJobTest_t0020(FioJobTest):
579     """Test consists of fio test jobs t0020 and t0021
580     Confirm that almost all offsets were touched non-sequentially"""
581
582     def check_result(self):
583         super(FioJobTest_t0020, self).check_result()
584
585         bw_log_filename = os.path.join(self.test_dir, "test_bw.log")
586         file_data, success = self.get_file(bw_log_filename)
587         log_lines = file_data.split('\n')
588
589         seq_count = 0
590         offsets = set()
591
592         prev = int(log_lines[0].split(',')[4])
593         for line in log_lines[1:]:
594             offsets.add(prev/4096)
595             if len(line.strip()) == 0:
596                 continue
597             cur = int(line.split(',')[4])
598             if cur - prev == 4096:
599                 seq_count += 1
600             prev = cur
601
602         # 10 is an arbitrary threshold
603         if seq_count > 10:
604             self.passed = False
605             self.failure_reason = "too many ({0}) consecutive offsets".format(seq_count)
606
607         if len(offsets) != 256:
608             self.passed = False
609             self.failure_reason += " number of offsets is {0} instead of 256".format(len(offsets))
610
611         for i in range(256):
612             if not i in offsets:
613                 self.passed = False
614                 self.failure_reason += " missing offset {0}".format(i*4096)
615
616
617 class FioJobTest_t0022(FioJobTest):
618     """Test consists of fio test job t0022"""
619
620     def check_result(self):
621         super(FioJobTest_t0022, self).check_result()
622
623         bw_log_filename = os.path.join(self.test_dir, "test_bw.log")
624         file_data, success = self.get_file(bw_log_filename)
625         log_lines = file_data.split('\n')
626
627         filesize = 1024*1024
628         bs = 4096
629         seq_count = 0
630         offsets = set()
631
632         prev = int(log_lines[0].split(',')[4])
633         for line in log_lines[1:]:
634             offsets.add(prev/bs)
635             if len(line.strip()) == 0:
636                 continue
637             cur = int(line.split(',')[4])
638             if cur - prev == bs:
639                 seq_count += 1
640             prev = cur
641
642         # 10 is an arbitrary threshold
643         if seq_count > 10:
644             self.passed = False
645             self.failure_reason = "too many ({0}) consecutive offsets".format(seq_count)
646
647         if len(offsets) == filesize/bs:
648             self.passed = False
649             self.failure_reason += " no duplicate offsets found with norandommap=1".format(len(offsets))
650
651
652 class FioJobTest_t0023(FioJobTest):
653     """Test consists of fio test job t0023 randtrimwrite test."""
654
655     def check_trimwrite(self, filename):
656         """Make sure that trims are followed by writes of the same size at the same offset."""
657
658         bw_log_filename = os.path.join(self.test_dir, filename)
659         file_data, success = self.get_file(bw_log_filename)
660         log_lines = file_data.split('\n')
661
662         prev_ddir = 1
663         for line in log_lines:
664             if len(line.strip()) == 0:
665                 continue
666             vals = line.split(',')
667             ddir = int(vals[2])
668             bs = int(vals[3])
669             offset = int(vals[4])
670             if prev_ddir == 1:
671                 if ddir != 2:
672                     self.passed = False
673                     self.failure_reason += " {0}: write not preceeded by trim: {1}".format(
674                         bw_log_filename, line)
675                     break
676             else:
677                 if ddir != 1:
678                     self.passed = False
679                     self.failure_reason += " {0}: trim not preceeded by write: {1}".format(
680                         bw_log_filename, line)
681                     break
682                 else:
683                     if prev_bs != bs:
684                         self.passed = False
685                         self.failure_reason += " {0}: block size does not match: {1}".format(
686                             bw_log_filename, line)
687                         break
688                     if prev_offset != offset:
689                         self.passed = False
690                         self.failure_reason += " {0}: offset does not match: {1}".format(
691                             bw_log_filename, line)
692                         break
693             prev_ddir = ddir
694             prev_bs = bs
695             prev_offset = offset
696
697
698     def check_all_offsets(self, filename, sectorsize, filesize):
699         """Make sure all offsets were touched."""
700
701         file_data, success = self.get_file(os.path.join(self.test_dir, filename))
702         if not success:
703             self.passed = False
704             self.failure_reason = " could not open {0}".format(filename)
705             return
706
707         log_lines = file_data.split('\n')
708
709         offsets = set()
710
711         for line in log_lines:
712             if len(line.strip()) == 0:
713                 continue
714             vals = line.split(',')
715             bs = int(vals[3])
716             offset = int(vals[4])
717             if offset % sectorsize != 0:
718                 self.passed = False
719                 self.failure_reason += " {0}: offset {1} not a multiple of sector size {2}".format(
720                     filename, offset, sectorsize)
721                 break
722             if bs % sectorsize != 0:
723                 self.passed = False
724                 self.failure_reason += " {0}: block size {1} not a multiple of sector size " \
725                     "{2}".format(filename, bs, sectorsize)
726                 break
727             for i in range(int(bs/sectorsize)):
728                 offsets.add(offset/sectorsize + i)
729
730         if len(offsets) != filesize/sectorsize:
731             self.passed = False
732             self.failure_reason += " {0}: only {1} offsets touched; expected {2}".format(
733                 filename, len(offsets), filesize/sectorsize)
734         else:
735             logging.debug("%s: %d sectors touched", filename, len(offsets))
736
737
738     def check_result(self):
739         super(FioJobTest_t0023, self).check_result()
740
741         filesize = 1024*1024
742
743         self.check_trimwrite("basic_bw.log")
744         self.check_trimwrite("bs_bw.log")
745         self.check_trimwrite("bsrange_bw.log")
746         self.check_trimwrite("bssplit_bw.log")
747         self.check_trimwrite("basic_no_rm_bw.log")
748         self.check_trimwrite("bs_no_rm_bw.log")
749         self.check_trimwrite("bsrange_no_rm_bw.log")
750         self.check_trimwrite("bssplit_no_rm_bw.log")
751
752         self.check_all_offsets("basic_bw.log", 4096, filesize)
753         self.check_all_offsets("bs_bw.log", 8192, filesize)
754         self.check_all_offsets("bsrange_bw.log", 512, filesize)
755         self.check_all_offsets("bssplit_bw.log", 512, filesize)
756
757
758 class FioJobTest_iops_rate(FioJobTest):
759     """Test consists of fio test job t0009
760     Confirm that job0 iops == 1000
761     and that job1_iops / job0_iops ~ 8
762     With two runs of fio-3.16 I observed a ratio of 8.3"""
763
764     def check_result(self):
765         super(FioJobTest_iops_rate, self).check_result()
766
767         if not self.passed:
768             return
769
770         iops1 = self.json_data['jobs'][0]['read']['iops']
771         logging.debug("Test %d: iops1: %f", self.testnum, iops1)
772         iops2 = self.json_data['jobs'][1]['read']['iops']
773         logging.debug("Test %d: iops2: %f", self.testnum, iops2)
774         ratio = iops2 / iops1
775         logging.debug("Test %d: ratio: %f", self.testnum, ratio)
776
777         if iops1 < 950 or iops1 > 1050:
778             self.failure_reason = "{0} iops value mismatch,".format(self.failure_reason)
779             self.passed = False
780
781         if ratio < 6 or ratio > 10:
782             self.failure_reason = "{0} iops ratio mismatch,".format(self.failure_reason)
783             self.passed = False
784
785
786 class Requirements(object):
787     """Requirements consists of multiple run environment characteristics.
788     These are to determine if a particular test can be run"""
789
790     _linux = False
791     _libaio = False
792     _io_uring = False
793     _zbd = False
794     _root = False
795     _zoned_nullb = False
796     _not_macos = False
797     _not_windows = False
798     _unittests = False
799     _cpucount4 = False
800
801     def __init__(self, fio_root):
802         Requirements._not_macos = platform.system() != "Darwin"
803         Requirements._not_windows = platform.system() != "Windows"
804         Requirements._linux = platform.system() == "Linux"
805
806         if Requirements._linux:
807             config_file = os.path.join(fio_root, "config-host.h")
808             contents, success = FioJobTest.get_file(config_file)
809             if not success:
810                 print("Unable to open {0} to check requirements".format(config_file))
811                 Requirements._zbd = True
812             else:
813                 Requirements._zbd = "CONFIG_HAS_BLKZONED" in contents
814                 Requirements._libaio = "CONFIG_LIBAIO" in contents
815
816             contents, success = FioJobTest.get_file("/proc/kallsyms")
817             if not success:
818                 print("Unable to open '/proc/kallsyms' to probe for io_uring support")
819             else:
820                 Requirements._io_uring = "io_uring_setup" in contents
821
822             Requirements._root = (os.geteuid() == 0)
823             if Requirements._zbd and Requirements._root:
824                 try:
825                     subprocess.run(["modprobe", "null_blk"],
826                                    stdout=subprocess.PIPE,
827                                    stderr=subprocess.PIPE)
828                     if os.path.exists("/sys/module/null_blk/parameters/zoned"):
829                         Requirements._zoned_nullb = True
830                 except Exception:
831                     pass
832
833         if platform.system() == "Windows":
834             utest_exe = "unittest.exe"
835         else:
836             utest_exe = "unittest"
837         unittest_path = os.path.join(fio_root, "unittests", utest_exe)
838         Requirements._unittests = os.path.exists(unittest_path)
839
840         Requirements._cpucount4 = multiprocessing.cpu_count() >= 4
841
842         req_list = [Requirements.linux,
843                     Requirements.libaio,
844                     Requirements.io_uring,
845                     Requirements.zbd,
846                     Requirements.root,
847                     Requirements.zoned_nullb,
848                     Requirements.not_macos,
849                     Requirements.not_windows,
850                     Requirements.unittests,
851                     Requirements.cpucount4]
852         for req in req_list:
853             value, desc = req()
854             logging.debug("Requirements: Requirement '%s' met? %s", desc, value)
855
856     @classmethod
857     def linux(cls):
858         """Are we running on Linux?"""
859         return Requirements._linux, "Linux required"
860
861     @classmethod
862     def libaio(cls):
863         """Is libaio available?"""
864         return Requirements._libaio, "libaio required"
865
866     @classmethod
867     def io_uring(cls):
868         """Is io_uring available?"""
869         return Requirements._io_uring, "io_uring required"
870
871     @classmethod
872     def zbd(cls):
873         """Is ZBD support available?"""
874         return Requirements._zbd, "Zoned block device support required"
875
876     @classmethod
877     def root(cls):
878         """Are we running as root?"""
879         return Requirements._root, "root required"
880
881     @classmethod
882     def zoned_nullb(cls):
883         """Are zoned null block devices available?"""
884         return Requirements._zoned_nullb, "Zoned null block device support required"
885
886     @classmethod
887     def not_macos(cls):
888         """Are we running on a platform other than macOS?"""
889         return Requirements._not_macos, "platform other than macOS required"
890
891     @classmethod
892     def not_windows(cls):
893         """Are we running on a platform other than Windws?"""
894         return Requirements._not_windows, "platform other than Windows required"
895
896     @classmethod
897     def unittests(cls):
898         """Were unittests built?"""
899         return Requirements._unittests, "Unittests support required"
900
901     @classmethod
902     def cpucount4(cls):
903         """Do we have at least 4 CPUs?"""
904         return Requirements._cpucount4, "4+ CPUs required"
905
906
907 SUCCESS_DEFAULT = {
908     'zero_return': True,
909     'stderr_empty': True,
910     'timeout': 600,
911     }
912 SUCCESS_NONZERO = {
913     'zero_return': False,
914     'stderr_empty': False,
915     'timeout': 600,
916     }
917 SUCCESS_STDERR = {
918     'zero_return': True,
919     'stderr_empty': False,
920     'timeout': 600,
921     }
922 TEST_LIST = [
923     {
924         'test_id':          1,
925         'test_class':       FioJobTest,
926         'job':              't0001-52c58027.fio',
927         'success':          SUCCESS_DEFAULT,
928         'pre_job':          None,
929         'pre_success':      None,
930         'requirements':     [],
931     },
932     {
933         'test_id':          2,
934         'test_class':       FioJobTest,
935         'job':              't0002-13af05ae-post.fio',
936         'success':          SUCCESS_DEFAULT,
937         'pre_job':          't0002-13af05ae-pre.fio',
938         'pre_success':      None,
939         'requirements':     [Requirements.linux, Requirements.libaio],
940     },
941     {
942         'test_id':          3,
943         'test_class':       FioJobTest,
944         'job':              't0003-0ae2c6e1-post.fio',
945         'success':          SUCCESS_NONZERO,
946         'pre_job':          't0003-0ae2c6e1-pre.fio',
947         'pre_success':      SUCCESS_DEFAULT,
948         'requirements':     [Requirements.linux, Requirements.libaio],
949     },
950     {
951         'test_id':          4,
952         'test_class':       FioJobTest,
953         'job':              't0004-8a99fdf6.fio',
954         'success':          SUCCESS_DEFAULT,
955         'pre_job':          None,
956         'pre_success':      None,
957         'requirements':     [Requirements.linux, Requirements.libaio],
958     },
959     {
960         'test_id':          5,
961         'test_class':       FioJobTest_t0005,
962         'job':              't0005-f7078f7b.fio',
963         'success':          SUCCESS_DEFAULT,
964         'pre_job':          None,
965         'pre_success':      None,
966         'output_format':    'json',
967         'requirements':     [Requirements.not_windows],
968     },
969     {
970         'test_id':          6,
971         'test_class':       FioJobTest_t0006,
972         'job':              't0006-82af2a7c.fio',
973         'success':          SUCCESS_DEFAULT,
974         'pre_job':          None,
975         'pre_success':      None,
976         'output_format':    'json',
977         'requirements':     [Requirements.linux, Requirements.libaio],
978     },
979     {
980         'test_id':          7,
981         'test_class':       FioJobTest_t0007,
982         'job':              't0007-37cf9e3c.fio',
983         'success':          SUCCESS_DEFAULT,
984         'pre_job':          None,
985         'pre_success':      None,
986         'output_format':    'json',
987         'requirements':     [],
988     },
989     {
990         'test_id':          8,
991         'test_class':       FioJobTest_t0008,
992         'job':              't0008-ae2fafc8.fio',
993         'success':          SUCCESS_DEFAULT,
994         'pre_job':          None,
995         'pre_success':      None,
996         'output_format':    'json',
997         'requirements':     [],
998     },
999     {
1000         'test_id':          9,
1001         'test_class':       FioJobTest_t0009,
1002         'job':              't0009-f8b0bd10.fio',
1003         'success':          SUCCESS_DEFAULT,
1004         'pre_job':          None,
1005         'pre_success':      None,
1006         'output_format':    'json',
1007         'requirements':     [Requirements.not_macos,
1008                              Requirements.cpucount4],
1009         # mac os does not support CPU affinity
1010     },
1011     {
1012         'test_id':          10,
1013         'test_class':       FioJobTest,
1014         'job':              't0010-b7aae4ba.fio',
1015         'success':          SUCCESS_DEFAULT,
1016         'pre_job':          None,
1017         'pre_success':      None,
1018         'requirements':     [],
1019     },
1020     {
1021         'test_id':          11,
1022         'test_class':       FioJobTest_iops_rate,
1023         'job':              't0011-5d2788d5.fio',
1024         'success':          SUCCESS_DEFAULT,
1025         'pre_job':          None,
1026         'pre_success':      None,
1027         'output_format':    'json',
1028         'requirements':     [],
1029     },
1030     {
1031         'test_id':          12,
1032         'test_class':       FioJobTest_t0012,
1033         'job':              't0012.fio',
1034         'success':          SUCCESS_DEFAULT,
1035         'pre_job':          None,
1036         'pre_success':      None,
1037         'output_format':    'json',
1038         'requirements':     [],
1039     },
1040     {
1041         'test_id':          13,
1042         'test_class':       FioJobTest,
1043         'job':              't0013.fio',
1044         'success':          SUCCESS_DEFAULT,
1045         'pre_job':          None,
1046         'pre_success':      None,
1047         'output_format':    'json',
1048         'requirements':     [],
1049     },
1050     {
1051         'test_id':          14,
1052         'test_class':       FioJobTest_t0014,
1053         'job':              't0014.fio',
1054         'success':          SUCCESS_DEFAULT,
1055         'pre_job':          None,
1056         'pre_success':      None,
1057         'output_format':    'json',
1058         'requirements':     [],
1059     },
1060     {
1061         'test_id':          15,
1062         'test_class':       FioJobTest_t0015,
1063         'job':              't0015-e78980ff.fio',
1064         'success':          SUCCESS_DEFAULT,
1065         'pre_job':          None,
1066         'pre_success':      None,
1067         'output_format':    'json',
1068         'requirements':     [Requirements.linux, Requirements.libaio],
1069     },
1070     {
1071         'test_id':          16,
1072         'test_class':       FioJobTest_t0015,
1073         'job':              't0016-d54ae22.fio',
1074         'success':          SUCCESS_DEFAULT,
1075         'pre_job':          None,
1076         'pre_success':      None,
1077         'output_format':    'json',
1078         'requirements':     [],
1079     },
1080     {
1081         'test_id':          17,
1082         'test_class':       FioJobTest_t0015,
1083         'job':              't0017.fio',
1084         'success':          SUCCESS_DEFAULT,
1085         'pre_job':          None,
1086         'pre_success':      None,
1087         'output_format':    'json',
1088         'requirements':     [Requirements.not_windows],
1089     },
1090     {
1091         'test_id':          18,
1092         'test_class':       FioJobTest,
1093         'job':              't0018.fio',
1094         'success':          SUCCESS_DEFAULT,
1095         'pre_job':          None,
1096         'pre_success':      None,
1097         'requirements':     [Requirements.linux, Requirements.io_uring],
1098     },
1099     {
1100         'test_id':          19,
1101         'test_class':       FioJobTest_t0019,
1102         'job':              't0019.fio',
1103         'success':          SUCCESS_DEFAULT,
1104         'pre_job':          None,
1105         'pre_success':      None,
1106         'requirements':     [],
1107     },
1108     {
1109         'test_id':          20,
1110         'test_class':       FioJobTest_t0020,
1111         'job':              't0020.fio',
1112         'success':          SUCCESS_DEFAULT,
1113         'pre_job':          None,
1114         'pre_success':      None,
1115         'requirements':     [],
1116     },
1117     {
1118         'test_id':          21,
1119         'test_class':       FioJobTest_t0020,
1120         'job':              't0021.fio',
1121         'success':          SUCCESS_DEFAULT,
1122         'pre_job':          None,
1123         'pre_success':      None,
1124         'requirements':     [],
1125     },
1126     {
1127         'test_id':          22,
1128         'test_class':       FioJobTest_t0022,
1129         'job':              't0022.fio',
1130         'success':          SUCCESS_DEFAULT,
1131         'pre_job':          None,
1132         'pre_success':      None,
1133         'requirements':     [],
1134     },
1135     {
1136         'test_id':          23,
1137         'test_class':       FioJobTest_t0023,
1138         'job':              't0023.fio',
1139         'success':          SUCCESS_DEFAULT,
1140         'pre_job':          None,
1141         'pre_success':      None,
1142         'requirements':     [],
1143     },
1144     {
1145         'test_id':          1000,
1146         'test_class':       FioExeTest,
1147         'exe':              't/axmap',
1148         'parameters':       None,
1149         'success':          SUCCESS_DEFAULT,
1150         'requirements':     [],
1151     },
1152     {
1153         'test_id':          1001,
1154         'test_class':       FioExeTest,
1155         'exe':              't/ieee754',
1156         'parameters':       None,
1157         'success':          SUCCESS_DEFAULT,
1158         'requirements':     [],
1159     },
1160     {
1161         'test_id':          1002,
1162         'test_class':       FioExeTest,
1163         'exe':              't/lfsr-test',
1164         'parameters':       ['0xFFFFFF', '0', '0', 'verify'],
1165         'success':          SUCCESS_STDERR,
1166         'requirements':     [],
1167     },
1168     {
1169         'test_id':          1003,
1170         'test_class':       FioExeTest,
1171         'exe':              't/readonly.py',
1172         'parameters':       ['-f', '{fio_path}'],
1173         'success':          SUCCESS_DEFAULT,
1174         'requirements':     [],
1175     },
1176     {
1177         'test_id':          1004,
1178         'test_class':       FioExeTest,
1179         'exe':              't/steadystate_tests.py',
1180         'parameters':       ['{fio_path}'],
1181         'success':          SUCCESS_DEFAULT,
1182         'requirements':     [],
1183     },
1184     {
1185         'test_id':          1005,
1186         'test_class':       FioExeTest,
1187         'exe':              't/stest',
1188         'parameters':       None,
1189         'success':          SUCCESS_STDERR,
1190         'requirements':     [],
1191     },
1192     {
1193         'test_id':          1006,
1194         'test_class':       FioExeTest,
1195         'exe':              't/strided.py',
1196         'parameters':       ['{fio_path}'],
1197         'success':          SUCCESS_DEFAULT,
1198         'requirements':     [],
1199     },
1200     {
1201         'test_id':          1007,
1202         'test_class':       FioExeTest,
1203         'exe':              't/zbd/run-tests-against-nullb',
1204         'parameters':       ['-s', '1'],
1205         'success':          SUCCESS_DEFAULT,
1206         'requirements':     [Requirements.linux, Requirements.zbd,
1207                              Requirements.root],
1208     },
1209     {
1210         'test_id':          1008,
1211         'test_class':       FioExeTest,
1212         'exe':              't/zbd/run-tests-against-nullb',
1213         'parameters':       ['-s', '2'],
1214         'success':          SUCCESS_DEFAULT,
1215         'requirements':     [Requirements.linux, Requirements.zbd,
1216                              Requirements.root, Requirements.zoned_nullb],
1217     },
1218     {
1219         'test_id':          1009,
1220         'test_class':       FioExeTest,
1221         'exe':              'unittests/unittest',
1222         'parameters':       None,
1223         'success':          SUCCESS_DEFAULT,
1224         'requirements':     [Requirements.unittests],
1225     },
1226     {
1227         'test_id':          1010,
1228         'test_class':       FioExeTest,
1229         'exe':              't/latency_percentiles.py',
1230         'parameters':       ['-f', '{fio_path}'],
1231         'success':          SUCCESS_DEFAULT,
1232         'requirements':     [],
1233     },
1234     {
1235         'test_id':          1011,
1236         'test_class':       FioExeTest,
1237         'exe':              't/jsonplus2csv_test.py',
1238         'parameters':       ['-f', '{fio_path}'],
1239         'success':          SUCCESS_DEFAULT,
1240         'requirements':     [],
1241     },
1242     {
1243         'test_id':          1012,
1244         'test_class':       FioExeTest,
1245         'exe':              't/log_compression.py',
1246         'parameters':       ['-f', '{fio_path}'],
1247         'success':          SUCCESS_DEFAULT,
1248         'requirements':     [],
1249     },
1250 ]
1251
1252
1253 def parse_args():
1254     """Parse command-line arguments."""
1255
1256     parser = argparse.ArgumentParser()
1257     parser.add_argument('-r', '--fio-root',
1258                         help='fio root path')
1259     parser.add_argument('-f', '--fio',
1260                         help='path to fio executable (e.g., ./fio)')
1261     parser.add_argument('-a', '--artifact-root',
1262                         help='artifact root directory')
1263     parser.add_argument('-s', '--skip', nargs='+', type=int,
1264                         help='list of test(s) to skip')
1265     parser.add_argument('-o', '--run-only', nargs='+', type=int,
1266                         help='list of test(s) to run, skipping all others')
1267     parser.add_argument('-d', '--debug', action='store_true',
1268                         help='provide debug output')
1269     parser.add_argument('-k', '--skip-req', action='store_true',
1270                         help='skip requirements checking')
1271     parser.add_argument('-p', '--pass-through', action='append',
1272                         help='pass-through an argument to an executable test')
1273     args = parser.parse_args()
1274
1275     return args
1276
1277
1278 def main():
1279     """Entry point."""
1280
1281     args = parse_args()
1282     if args.debug:
1283         logging.basicConfig(level=logging.DEBUG)
1284     else:
1285         logging.basicConfig(level=logging.INFO)
1286
1287     pass_through = {}
1288     if args.pass_through:
1289         for arg in args.pass_through:
1290             if not ':' in arg:
1291                 print("Invalid --pass-through argument '%s'" % arg)
1292                 print("Syntax for --pass-through is TESTNUMBER:ARGUMENT")
1293                 return
1294             split = arg.split(":", 1)
1295             pass_through[int(split[0])] = split[1]
1296         logging.debug("Pass-through arguments: %s", pass_through)
1297
1298     if args.fio_root:
1299         fio_root = args.fio_root
1300     else:
1301         fio_root = str(Path(__file__).absolute().parent.parent)
1302     print("fio root is %s" % fio_root)
1303
1304     if args.fio:
1305         fio_path = args.fio
1306     else:
1307         if platform.system() == "Windows":
1308             fio_exe = "fio.exe"
1309         else:
1310             fio_exe = "fio"
1311         fio_path = os.path.join(fio_root, fio_exe)
1312     print("fio path is %s" % fio_path)
1313     if not shutil.which(fio_path):
1314         print("Warning: fio executable not found")
1315
1316     artifact_root = args.artifact_root if args.artifact_root else \
1317         "fio-test-{0}".format(time.strftime("%Y%m%d-%H%M%S"))
1318     os.mkdir(artifact_root)
1319     print("Artifact directory is %s" % artifact_root)
1320
1321     if not args.skip_req:
1322         req = Requirements(fio_root)
1323
1324     passed = 0
1325     failed = 0
1326     skipped = 0
1327
1328     for config in TEST_LIST:
1329         if (args.skip and config['test_id'] in args.skip) or \
1330            (args.run_only and config['test_id'] not in args.run_only):
1331             skipped = skipped + 1
1332             print("Test {0} SKIPPED (User request)".format(config['test_id']))
1333             continue
1334
1335         if issubclass(config['test_class'], FioJobTest):
1336             if config['pre_job']:
1337                 fio_pre_job = os.path.join(fio_root, 't', 'jobs',
1338                                            config['pre_job'])
1339             else:
1340                 fio_pre_job = None
1341             if config['pre_success']:
1342                 fio_pre_success = config['pre_success']
1343             else:
1344                 fio_pre_success = None
1345             if 'output_format' in config:
1346                 output_format = config['output_format']
1347             else:
1348                 output_format = 'normal'
1349             test = config['test_class'](
1350                 fio_path,
1351                 os.path.join(fio_root, 't', 'jobs', config['job']),
1352                 config['success'],
1353                 fio_pre_job=fio_pre_job,
1354                 fio_pre_success=fio_pre_success,
1355                 output_format=output_format)
1356             desc = config['job']
1357         elif issubclass(config['test_class'], FioExeTest):
1358             exe_path = os.path.join(fio_root, config['exe'])
1359             if config['parameters']:
1360                 parameters = [p.format(fio_path=fio_path) for p in config['parameters']]
1361             else:
1362                 parameters = []
1363             if Path(exe_path).suffix == '.py' and platform.system() == "Windows":
1364                 parameters.insert(0, exe_path)
1365                 exe_path = "python.exe"
1366             if config['test_id'] in pass_through:
1367                 parameters += pass_through[config['test_id']].split()
1368             test = config['test_class'](exe_path, parameters,
1369                                         config['success'])
1370             desc = config['exe']
1371         else:
1372             print("Test {0} FAILED: unable to process test config".format(config['test_id']))
1373             failed = failed + 1
1374             continue
1375
1376         if not args.skip_req:
1377             reqs_met = True
1378             for req in config['requirements']:
1379                 reqs_met, reason = req()
1380                 logging.debug("Test %d: Requirement '%s' met? %s", config['test_id'], reason,
1381                               reqs_met)
1382                 if not reqs_met:
1383                     break
1384             if not reqs_met:
1385                 print("Test {0} SKIPPED ({1}) {2}".format(config['test_id'], reason, desc))
1386                 skipped = skipped + 1
1387                 continue
1388
1389         try:
1390             test.setup(artifact_root, config['test_id'])
1391             test.run()
1392             test.check_result()
1393         except KeyboardInterrupt:
1394             break
1395         except Exception as e:
1396             test.passed = False
1397             test.failure_reason += str(e)
1398             logging.debug("Test %d exception:\n%s\n", config['test_id'], traceback.format_exc())
1399         if test.passed:
1400             result = "PASSED"
1401             passed = passed + 1
1402         else:
1403             result = "FAILED: {0}".format(test.failure_reason)
1404             failed = failed + 1
1405             contents, _ = FioJobTest.get_file(test.stderr_file)
1406             logging.debug("Test %d: stderr:\n%s", config['test_id'], contents)
1407             contents, _ = FioJobTest.get_file(test.stdout_file)
1408             logging.debug("Test %d: stdout:\n%s", config['test_id'], contents)
1409         print("Test {0} {1} {2}".format(config['test_id'], result, desc))
1410
1411     print("{0} test(s) passed, {1} failed, {2} skipped".format(passed, failed, skipped))
1412
1413     sys.exit(failed)
1414
1415
1416 if __name__ == '__main__':
1417     main()