Repository: smarr/ReBench

Issue:
CSV Headers should be added only exactly once, to avoid being confused with data
With #220 we are writing a line of headers into the file.

We should only do that when the file is initially empty.
Otherwise, the header line may be confused as data.

@naomiGrew something for next week :) 

Comments:

Patch:
diff --git a/rebench.conf b/rebench.conf
index a43c60c..6052139 100644
--- a/rebench.conf
+++ b/rebench.conf
@@ -17,7 +17,7 @@ benchmark_suites:
     TestSuite1:
         gauge_adapter: Test
         # location: /Users/...
-        command: TestBenchMarks %(benchmark)s %(input)s %(variable)s
+        command: TestBenchMarks %(benchmark)s %(input)s %(variable)s something-else
         input_sizes: [1, 2, 10, 100, 1000]
         benchmarks:
             - Bench1
@@ -29,7 +29,7 @@ benchmark_suites:
             - val2
     TestSuite2:
         gauge_adapter: Test
-        command: TestBenchMarks %(benchmark)s %(input)s %(variable)s
+        command: TestBenchMarks %(benchmark)s %(input)s %(variable)s another-thing
         input_sizes: [1, 2, 10, 100, 1000]
         cores: [7, 13, 55]
         benchmarks:
diff --git a/rebench/executor.py b/rebench/executor.py
index 2ac7163..5a16273 100644
--- a/rebench/executor.py
+++ b/rebench/executor.py
@@ -508,6 +508,8 @@ class Executor(object):
                        + "{ind}{ind}Return code: %d\n"
                        + "{ind}{ind}max_invocation_time: %s\n") % (
                            return_code, run_id.max_invocation_time)
+            elif return_code is None:
+                msg = "{ind}Run failed. Return code: None\n"
             else:
                 msg = "{ind}Run failed. Return code: %d\n" % return_code
 
diff --git a/rebench/persistence.py b/rebench/persistence.py
index c71426d..081a441 100644
--- a/rebench/persistence.py
+++ b/rebench/persistence.py
@@ -317,12 +317,16 @@ class _FilePersistence(_ConcretePersistence):
         shebang_with_metadata += "# Environment: " + json.dumps(determine_environment()) + "\n"
         shebang_with_metadata += "# Source: " + json.dumps(
             determine_source_details(self._configurator)) + "\n"
-        shebang_with_metadata += self._SEP.join(Measurement.get_column_headers()) + "\n"
+
+        csv_header = self._SEP.join(Measurement.get_column_headers()) + "\n"
 
         try:
             # pylint: disable-next=unspecified-encoding,consider-using-with
             data_file = open(self._data_filename, 'a+')
+            is_empty = data_file.tell() == 0
             data_file.write(shebang_with_metadata)
+            if is_empty:
+                data_file.write(csv_header)
             data_file.flush()
             return data_file
         except Exception as err:  # pylint: disable=broad-except


Test patch:
diff --git a/rebench/tests/persistency_test.py b/rebench/tests/persistency_test.py
index ef981df..4cfe05a 100644
--- a/rebench/tests/persistency_test.py
+++ b/rebench/tests/persistency_test.py
@@ -172,27 +172,29 @@ class PersistencyTest(ReBenchTestCase):
         run.close_files()
 
     def test_check_file_lines(self):
-        ds = DataStore(self.ui)
-        cnf = Configurator(load_config(self._path + '/persistency.conf'),
-                            ds, self.ui, data_file=self._tmp_file)
-        ds.load_data(None, False)
-        ex = Executor(cnf.get_runs(), False, self.ui)
-        ex.execute()
+        self._load_config_and_run()
+
         with open(self._tmp_file, 'r') as file: # pylint: disable=unspecified-encoding
             lines = file.readlines()
-            command = self.get_line_after_char('#!', lines[0])
-            self.assertEqual(command, subprocess.list2cmdline(sys.argv))
-            time = self.get_line_after_char('Start:', lines[1])
-            self.assertTrue(self.is_valid_time(time))
-            json_code = self.get_line_after_char('Environment:', lines[2])
-            self.assertTrue(self.is_valid_json(json_code))
-            json_code = self.get_line_after_char('Source:', lines[3])
-            self.assertTrue(self.is_valid_json(json_code))
-            line = lines[4].split("\t")
-            line[-1] = line[-1].rstrip('\n')
-            words = Measurement.get_column_headers()
-            self.assertEqual(line, words)
-            self.assertEqual(len((lines[5]).split("\t")) ,len(line))
+
+        command = self.get_line_after_char('#!', lines[0])
+        self.assertEqual(command, subprocess.list2cmdline(sys.argv))
+
+        time = self.get_line_after_char('Start:', lines[1])
+        self.assertTrue(self.is_valid_time(time))
+
+        self.assertIsNotNone(json.loads(self.get_line_after_char('Environment:', lines[2])))
+        self.assertIsNotNone(json.loads(self.get_line_after_char('Source:', lines[3])))
+
+        column_headers = lines[4].split("\t")
+        # remove the newline character from the last column header
+        column_headers[-1] = column_headers[-1].rstrip('\n')
+
+        expected_headers = Measurement.get_column_headers()
+        self.assertEqual(column_headers, expected_headers)
+
+        self.assertEqual(len((lines[5]).split("\t")), len(column_headers),
+                         'expected same number of column headers as data columns')
 
     def get_line_after_char(self, char, line):
         if char in line:
@@ -207,9 +209,27 @@ class PersistencyTest(ReBenchTestCase):
         except ValueError:
             return False
 
-    def is_valid_json(self, json_str):
-        try:
-            json.loads(json_str)
-            return True
-        except json.JSONDecodeError:
-            return False
+    def _load_config_and_run(self, args=None):
+        ds = DataStore(self.ui)
+        cnf = Configurator(load_config(self._path + '/persistency.conf'),
+                           ds, self.ui, args, data_file=self._tmp_file)
+        ds.load_data(None, False)
+        ex = Executor(cnf.get_runs(), False, self.ui)
+        ex.execute()
+
+    def test_check_single_csv_header(self):
+        """Check that there is only one csv header in the file"""
+        # first run
+        self._load_config_and_run()
+
+        # second run, requesting more invocations
+        opt_parser = ReBench().shell_options()
+        args = opt_parser.parse_args(['-in', '20', '-R', self._path + '/persistency.conf'])
+        self._load_config_and_run(args)
+
+        with open(self._tmp_file, 'r') as file: # pylint: disable=unspecified-encoding
+            lines = file.readlines()
+
+        # count the number of lines starting with 'invocation'
+        invocation_lines = [line for line in lines if line.startswith('invocation')]
+        self.assertEqual(len(invocation_lines), 1)