t/fiotestlib: make recorded command prettier
[fio.git] / t / fiotestlib.py
index 2060c3002055518189133878c4fc50e8d2348081..1f35de0ae81d0b83d20698f5df8702ccb5619657 100755 (executable)
@@ -18,41 +18,43 @@ import platform
 import traceback
 import subprocess
 from pathlib import Path
-from fiotestcommon import get_file
+from fiotestcommon import get_file, SUCCESS_DEFAULT
 
 
 class FioTest():
     """Base for all fio tests."""
 
-    def __init__(self, exe_path, success):
-        self.paths = {'exe': exe_path}
+    def __init__(self, exe_path, success, testnum, artifact_root):
         self.success = success
+        self.testnum = testnum
         self.output = {}
-        self.testnum = None
         self.passed = True
         self.failure_reason = ''
-        self.filenames = {}
         self.parameters = None
-
-    def setup(self, artifact_root, testnum, parameters):
+        self.paths = {
+                        'exe': exe_path,
+                        'artifacts': artifact_root,
+                        'test_dir': os.path.join(artifact_root, \
+                                f"{testnum:04d}"),
+                        }
+        self.filenames = {
+                            'cmd': os.path.join(self.paths['test_dir'], \
+                                    f"{os.path.basename(self.paths['exe'])}.command"),
+                            'stdout': os.path.join(self.paths['test_dir'], \
+                                    f"{os.path.basename(self.paths['exe'])}.stdout"),
+                            'stderr': os.path.join(self.paths['test_dir'], \
+                                    f"{os.path.basename(self.paths['exe'])}.stderr"),
+                            'exitcode': os.path.join(self.paths['test_dir'], \
+                                    f"{os.path.basename(self.paths['exe'])}.exitcode"),
+                            }
+
+    def setup(self, parameters):
         """Setup instance variables for test."""
 
-        self.testnum = testnum
         self.parameters = parameters
-        self.paths['artifacts'] = artifact_root
-        self.paths['test_dir'] = os.path.join(artifact_root, f"{testnum:04d}")
         if not os.path.exists(self.paths['test_dir']):
             os.mkdir(self.paths['test_dir'])
 
-        self.filenames['cmd'] = os.path.join(self.paths['test_dir'],
-                                             f"{os.path.basename(self.paths['exe'])}.command")
-        self.filenames['stdout'] = os.path.join(self.paths['test_dir'],
-                                                f"{os.path.basename(self.paths['exe'])}.stdout")
-        self.filenames['stderr'] = os.path.join(self.paths['test_dir'],
-                                                f"{os.path.basename(self.paths['exe'])}.stderr")
-        self.filenames['exitcode'] = os.path.join(self.paths['test_dir'],
-                                                  f"{os.path.basename(self.paths['exe'])}.exitcode")
-
     def run(self):
         """Run the test."""
 
@@ -67,23 +69,13 @@ class FioTest():
 class FioExeTest(FioTest):
     """Test consists of an executable binary or script"""
 
-    def __init__(self, exe_path, success):
-        """Construct a FioExeTest which is a FioTest consisting of an
-        executable binary or script.
-
-        exe_path:       location of executable binary or script
-        success:        Definition of test success
-        """
-
-        FioTest.__init__(self, exe_path, success)
-
     def run(self):
         """Execute the binary or script described by this instance."""
 
         command = [self.paths['exe']] + self.parameters
         with open(self.filenames['cmd'], "w+",
                   encoding=locale.getpreferredencoding()) as command_file:
-            command_file.write(f"{command}\n")
+            command_file.write(" \\\n ".join(command))
 
         try:
             with open(self.filenames['stdout'], "w+",
@@ -158,14 +150,17 @@ class FioExeTest(FioTest):
 class FioJobFileTest(FioExeTest):
     """Test consists of a fio job with options in a job file."""
 
-    def __init__(self, fio_path, fio_job, success, fio_pre_job=None,
-                 fio_pre_success=None, output_format="normal"):
+    def __init__(self, fio_path, fio_job, success, testnum, artifact_root,
+                 fio_pre_job=None, fio_pre_success=None,
+                 output_format="normal"):
         """Construct a FioJobFileTest which is a FioExeTest consisting of a
         single fio job file with an optional setup step.
 
         fio_path:           location of fio executable
         fio_job:            location of fio job file
         success:            Definition of test success
+        testnum:            test ID
+        artifact_root:      root directory for artifacts
         fio_pre_job:        fio job for preconditioning
         fio_pre_success:    Definition of test success for fio precon job
         output_format:      normal (default), json, jsonplus, or terse
@@ -178,9 +173,9 @@ class FioJobFileTest(FioExeTest):
         self.precon_failed = False
         self.json_data = None
 
-        FioExeTest.__init__(self, fio_path, success)
+        super().__init__(fio_path, success, testnum, artifact_root)
 
-    def setup(self, artifact_root, testnum, parameters=None):
+    def setup(self, parameters=None):
         """Setup instance variables for fio job test."""
 
         self.filenames['fio_output'] = f"{os.path.basename(self.fio_job)}.output"
@@ -191,7 +186,7 @@ class FioJobFileTest(FioExeTest):
             self.fio_job,
             ]
 
-        super().setup(artifact_root, testnum, fio_args)
+        super().setup(fio_args)
 
         # Update the filenames from the default
         self.filenames['cmd'] = os.path.join(self.paths['test_dir'],
@@ -208,8 +203,10 @@ class FioJobFileTest(FioExeTest):
 
         precon = FioJobFileTest(self.paths['exe'], self.fio_pre_job,
                             self.fio_pre_success,
+                            self.testnum,
+                            self.paths['artifacts'],
                             output_format=self.output_format)
-        precon.setup(self.paths['artifacts'], self.testnum)
+        precon.setup()
         precon.run()
         precon.check_result()
         self.precon_failed = not precon.passed
@@ -274,6 +271,106 @@ class FioJobFileTest(FioExeTest):
             self.passed = False
 
 
+class FioJobCmdTest(FioExeTest):
+    """This runs a fio job with options specified on the command line."""
+
+    def __init__(self, fio_path, success, testnum, artifact_root, fio_opts, basename=None):
+
+        self.basename = basename if basename else os.path.basename(fio_path)
+        self.fio_opts = fio_opts
+        self.json_data = None
+        self.iops_log_lines = None
+
+        super().__init__(fio_path, success, testnum, artifact_root)
+
+        filename_stub = os.path.join(self.paths['test_dir'], f"{self.basename}{self.testnum:03d}")
+        self.filenames['cmd'] = f"{filename_stub}.command"
+        self.filenames['stdout'] = f"{filename_stub}.stdout"
+        self.filenames['stderr'] = f"{filename_stub}.stderr"
+        self.filenames['output'] = os.path.abspath(f"{filename_stub}.output")
+        self.filenames['exitcode'] = f"{filename_stub}.exitcode"
+        self.filenames['iopslog'] = os.path.abspath(f"{filename_stub}")
+
+    def run(self):
+        super().run()
+
+        if 'output-format' in self.fio_opts and 'json' in \
+                self.fio_opts['output-format']:
+            if not self.get_json():
+                print('Unable to decode JSON data')
+                self.passed = False
+
+        if any('--write_iops_log=' in param for param in self.parameters):
+            self.get_iops_log()
+
+    def get_iops_log(self):
+        """Read IOPS log from the first job."""
+
+        log_filename = self.filenames['iopslog'] + "_iops.1.log"
+        with open(log_filename, 'r', encoding=locale.getpreferredencoding()) as iops_file:
+            self.iops_log_lines = iops_file.read()
+
+    def get_json(self):
+        """Convert fio JSON output into a python JSON object"""
+
+        filename = self.filenames['output']
+        with open(filename, 'r', encoding=locale.getpreferredencoding()) as file:
+            file_data = file.read()
+
+        #
+        # Sometimes fio informational messages are included at the top of the
+        # JSON output, especially under Windows. Try to decode output as JSON
+        # data, lopping off up to the first four lines
+        #
+        lines = file_data.splitlines()
+        for i in range(5):
+            file_data = '\n'.join(lines[i:])
+            try:
+                self.json_data = json.loads(file_data)
+            except json.JSONDecodeError:
+                continue
+            else:
+                return True
+
+        return False
+
+    @staticmethod
+    def check_empty(job):
+        """
+        Make sure JSON data is empty.
+
+        Some data structures should be empty. This function makes sure that they are.
+
+        job         JSON object that we need to check for emptiness
+        """
+
+        return job['total_ios'] == 0 and \
+                job['slat_ns']['N'] == 0 and \
+                job['clat_ns']['N'] == 0 and \
+                job['lat_ns']['N'] == 0
+
+    def check_all_ddirs(self, ddir_nonzero, job):
+        """
+        Iterate over the data directions and check whether each is
+        appropriately empty or not.
+        """
+
+        retval = True
+        ddirlist = ['read', 'write', 'trim']
+
+        for ddir in ddirlist:
+            if ddir in ddir_nonzero:
+                if self.check_empty(job[ddir]):
+                    print(f"Unexpected zero {ddir} data found in output")
+                    retval = False
+            else:
+                if not self.check_empty(job[ddir]):
+                    print(f"Unexpected {ddir} data found in output")
+                    retval = False
+
+        return retval
+
+
 def run_fio_tests(test_list, test_env, args):
     """
     Run tests as specified in test_list.
@@ -308,11 +405,24 @@ def run_fio_tests(test_list, test_env, args):
                 test_env['fio_path'],
                 os.path.join(test_env['fio_root'], 't', 'jobs', config['job']),
                 config['success'],
+                config['test_id'],
+                test_env['artifact_root'],
                 fio_pre_job=fio_pre_job,
                 fio_pre_success=fio_pre_success,
                 output_format=output_format)
             desc = config['job']
             parameters = []
+        elif issubclass(config['test_class'], FioJobCmdTest):
+            if not 'success' in config:
+                config['success'] = SUCCESS_DEFAULT
+            test = config['test_class'](test_env['fio_path'],
+                                        config['success'],
+                                        config['test_id'],
+                                        test_env['artifact_root'],
+                                        config['fio_opts'],
+                                        test_env['basename'])
+            desc = config['test_id']
+            parameters = config
         elif issubclass(config['test_class'], FioExeTest):
             exe_path = os.path.join(test_env['fio_root'], config['exe'])
             parameters = []
@@ -324,14 +434,18 @@ def run_fio_tests(test_list, test_env, args):
                 exe_path = "python.exe"
             if config['test_id'] in test_env['pass_through']:
                 parameters += test_env['pass_through'][config['test_id']].split()
-            test = config['test_class'](exe_path, config['success'])
+            test = config['test_class'](
+                    exe_path,
+                    config['success'],
+                    config['test_id'],
+                    test_env['artifact_root'])
             desc = config['exe']
         else:
             print(f"Test {config['test_id']} FAILED: unable to process test config")
             failed = failed + 1
             continue
 
-        if not args.skip_req:
+        if 'requirements' in config and not args.skip_req:
             reqs_met = True
             for req in config['requirements']:
                 reqs_met, reason = req()
@@ -345,7 +459,7 @@ def run_fio_tests(test_list, test_env, args):
                 continue
 
         try:
-            test.setup(test_env['artifact_root'], config['test_id'], parameters)
+            test.setup(parameters)
             test.run()
             test.check_result()
         except KeyboardInterrupt: