[Piglit] [RFC 02/10] framework: update JSON storage format to version 10

Nicolai Hähnle nhaehnle at gmail.com
Wed Oct 11 10:26:51 UTC 2017


From: Nicolai Hähnle <nicolai.haehnle at amd.com>

Reflect the refactoring of TestResult and TestrunResult in the on-disk
storage format.
---
 framework/backends/abstract.py |  6 +++---
 framework/backends/json.py     | 39 ++++++++++++++++++++++++++++++---------
 framework/results.py           | 17 +++++++++--------
 framework/test/base.py         |  1 +
 4 files changed, 43 insertions(+), 20 deletions(-)

diff --git a/framework/backends/abstract.py b/framework/backends/abstract.py
index 85abfa52d..27dec7e6b 100644
--- a/framework/backends/abstract.py
+++ b/framework/backends/abstract.py
@@ -165,22 +165,20 @@ class FileBackend(Backend):
                         tests. It is important for resumes that this is not
                         overlapping as the Inheriting classes assume they are
                         not. Default: 0
 
     """
     def __init__(self, dest, file_start_count=0, **kwargs):
         self._dest = dest
         self._counter = itertools.count(file_start_count)
         self._write_final = write_compressed
 
-    __INCOMPLETE = TestResult(result=INCOMPLETE)
-
     def __fsync(self, file_):
         """ Sync the file to disk
 
         If options.OPTIONS.sync is truthy this will sync self._file to disk
 
         """
         file_.flush()
         if options.OPTIONS.sync:
             os.fsync(file_.fileno())
 
@@ -210,14 +208,16 @@ class FileBackend(Backend):
             tfile = file_ + '.tmp'
             with open(tfile, 'w') as f:
                 self._write(f, name, val)
                 self.__fsync(f)
             shutil.move(tfile, file_)
 
         file_ = os.path.join(self._dest, 'tests', '{}.{}'.format(
             next(self._counter), self._file_extension))
 
         with open(file_, 'w') as f:
-            self._write(f, name, self.__INCOMPLETE)
+            incomplete = TestResult(result=INCOMPLETE)
+            incomplete.root = name
+            self._write(f, name, incomplete)
             self.__fsync(f)
 
         yield finish
diff --git a/framework/backends/json.py b/framework/backends/json.py
index 882169e09..80d82d0ab 100644
--- a/framework/backends/json.py
+++ b/framework/backends/json.py
@@ -46,21 +46,21 @@ from framework import status, results, exceptions, compat
 from .abstract import FileBackend, write_compressed
 from .register import Registry
 from . import compression
 
 __all__ = [
     'REGISTRY',
     'JSONBackend',
 ]
 
 # The current version of the JSON results
-CURRENT_JSON_VERSION = 9
+CURRENT_JSON_VERSION = 10
 
 # The minimum JSON format supported
 MINIMUM_SUPPORTED_VERSION = 7
 
 # The level to indent a final file
 INDENT = 4
 
 
 def piglit_encoder(obj):
     """ Encoder for piglit that can transform additional classes into json
@@ -139,35 +139,35 @@ class JSONBackend(FileBackend):
 
             # Load the metadata and put it into a dictionary
             with open(os.path.join(self._dest, 'metadata.json'), 'r') as f:
                 data.update(json.load(f))
 
             # If there is more metadata add it the dictionary
             if metadata:
                 data.update(metadata)
 
             # Add the tests to the dictionary
-            data['tests'] = collections.OrderedDict()
+            data['results'] = []
 
             for test in file_list:
                 test = os.path.join(tests_dir, test)
                 if os.path.isfile(test):
                     # Try to open the json snippets. If we fail to open a test
                     # then throw the whole thing out. This gives us atomic
                     # writes, the writing worked and is valid or it didn't
                     # work.
                     try:
                         with open(test, 'r') as f:
-                            data['tests'].update(json.load(f))
+                            data['results'].append(json.load(f))
                     except ValueError:
                         pass
-            assert data['tests']
+            assert data['results']
 
             data = results.TestrunResult.from_dict(data)
 
             # write out the combined file. Use the compression writer from the
             # FileBackend
             with self._write_final(os.path.join(self._dest, 'results.json')) as f:
                 json.dump(data, f, default=piglit_encoder, indent=INDENT)
 
         # Otherwise use jsonstreams to write the final dictionary. This uses an
         # external library, but is slightly faster and uses considerably less
@@ -179,40 +179,40 @@ class JSONBackend(FileBackend):
                 with jsonstreams.Stream(jsonstreams.Type.object, fd=f, indent=4,
                                         encoder=encoder, pretty=True) as s:
                     s.write('__type__', 'TestrunResult')
                     with open(os.path.join(self._dest, 'metadata.json'),
                               'r') as n:
                         s.iterwrite(six.iteritems(json.load(n, object_pairs_hook=collections.OrderedDict)))
 
                     if metadata:
                         s.iterwrite(six.iteritems(metadata))
 
-                    with s.subobject('tests') as t:
+                    with s.subarray('results') as t:
                         for test in file_list:
                             test = os.path.join(tests_dir, test)
                             if os.path.isfile(test):
                                 try:
                                     with open(test, 'r') as f:
                                         a = json.load(f)
                                 except ValueError:
                                     continue
 
-                                t.iterwrite(six.iteritems(a))
+                                t.write(a)
 
 
         # Delete the temporary files
         os.unlink(os.path.join(self._dest, 'metadata.json'))
         shutil.rmtree(os.path.join(self._dest, 'tests'))
 
     @staticmethod
     def _write(f, name, data):
-        json.dump({name: data}, f, default=piglit_encoder)
+        json.dump(data, f, default=piglit_encoder)
 
 
 def load_results(filename, compression_):
     """ Loader function for TestrunResult class
 
     This function takes a single argument of a results file.
 
     It makes quite a few assumptions, first it assumes that it has been passed
     a folder, if that fails then it looks for a plain text json file called
     "main"
@@ -277,32 +277,32 @@ def _resume(results_dir):
     # pylint: disable=maybe-no-member
     assert os.path.isdir(results_dir), \
         "TestrunResult.resume() requires a directory"
 
     # Load the metadata
     with open(os.path.join(results_dir, 'metadata.json'), 'r') as f:
         meta = json.load(f)
     assert meta['results_version'] == CURRENT_JSON_VERSION, \
         "Old results version, resume impossible"
 
-    meta['tests'] = collections.OrderedDict()
+    meta['results'] = []
 
     # Load all of the test names and added them to the test list
     tests_dir = os.path.join(results_dir, 'tests')
     file_list = sorted(
         (l for l in os.listdir(tests_dir) if l.endswith('.json')),
         key=lambda p: int(os.path.splitext(p)[0]))
 
     for file_ in file_list:
         with open(os.path.join(tests_dir, file_), 'r') as f:
             try:
-                meta['tests'].update(json.load(f))
+                meta['results'].append(json.load(f))
             except ValueError:
                 continue
 
     return results.TestrunResult.from_dict(meta)
 
 
 def _update_results(results, filepath):
     """ Update results to the latest version
 
     This function is a wrapper for other update_* functions, providing
@@ -315,20 +315,21 @@ def _update_results(results, filepath):
 
     """
 
     def loop_updates(results):
         """ Helper to select the proper update sequence """
         # Python lacks a switch statement, the workaround is to use a
         # dictionary
         updates = {
             7: _update_seven_to_eight,
             8: _update_eight_to_nine,
+            9: _update_nine_to_ten,
         }
 
         while results['results_version'] < CURRENT_JSON_VERSION:
             results = updates[results['results_version']](results)
 
         return results
 
     if results['results_version'] < MINIMUM_SUPPORTED_VERSION:
         raise exceptions.PiglitFatalError(
             'Unsupported version "{}", '
@@ -393,16 +394,36 @@ def _update_eight_to_nine(result):
         if 'pid' in test:
             test['pid'] = [test['pid']]
         else:
             test['pid'] = []
 
     result['results_version'] = 9
 
     return result
 
 
+def _update_nine_to_ten(result):
+    """Update json results from version 9 to 10.
+
+    This changes the tests dictionary to the results list, and adds the
+    'root' attribute to results.
+
+    """
+    result['results'] = []
+
+    for key, test in six.iteritems(result['tests']):
+        test['root'] = key
+        result['results'].append(test)
+
+    del result['tests']
+
+    result['results_version'] = 10
+
+    return result
+
+
 REGISTRY = Registry(
     extensions=['.json'],
     backend=JSONBackend,
     load=load_results,
     meta=set_meta,
 )
diff --git a/framework/results.py b/framework/results.py
index a44029f3e..26ebbe7a6 100644
--- a/framework/results.py
+++ b/framework/results.py
@@ -206,20 +206,21 @@ class TestResult(object):
 
         try:
             return self.subtests[relative]
         except KeyError:
             raise KeyError(key)
 
     def to_json(self):
         """Return the TestResult as a json serializable object."""
         obj = {
             '__type__': 'TestResult',
+            'root': self.root,
             'command': self.command,
             'environment': self.environment,
             'err': self.err,
             'out': self.out,
             'result': self.result,
             'returncode': self.returncode,
             'subtests': self.subtests.to_json(),
             'time': self.time.to_json(),
             'exception': self.exception,
             'traceback': self.traceback,
@@ -237,21 +238,21 @@ class TestResult(object):
         status.Status object
 
         """
         # pylint will say that assining to inst.out or inst.err is a non-slot
         # because self.err and self.out are descriptors, methods that act like
         # variables. Just silence pylint
         # pylint: disable=assigning-non-slot
         inst = cls()
 
         for each in ['returncode', 'command', 'exception', 'environment',
-                     'traceback', 'dmesg', 'pid', 'result']:
+                     'traceback', 'dmesg', 'pid', 'result', 'root']:
             if each in dict_:
                 setattr(inst, each, dict_[each])
 
         # Set special instances
         if 'subtests' in dict_:
             inst.subtests = Subtests.from_dict(dict_['subtests'])
         if 'time' in dict_:
             inst.time = TimeAttribute.from_dict(dict_['time'])
 
         # out and err must be set manually to avoid replacing the setter
@@ -385,22 +386,24 @@ class TestrunResult(object):
                     self.totals[name].add(total)
                     name = grouptools.groupname(name)
                     worklist[name].add(total)
                 else:
                     self.totals['root'].add(total)
 
     def to_json(self):
         if not self.totals:
             self.calculate_group_totals()
         rep = copy.copy(self.__dict__)
-        rep['tests'] = collections.OrderedDict((t.root, t.to_json())
-                       for t in self.results)
+
+        rep['results'] = [t.to_json() for t in self.results]
+        del rep['_tests']
+
         rep['__type__'] = 'TestrunResult'
         return rep
 
     @classmethod
     def from_dict(cls, dict_, _no_totals=False):
         """Convert a dictionary into a TestrunResult.
 
         This method is meant to be used for loading results from json or
         similar formats
 
@@ -415,28 +418,26 @@ class TestrunResult(object):
             if value:
                 setattr(res, name, value)
 
         # Since this is used to load partial metadata when writing final test
         # results there is no guarantee that this will have a "time_elapsed"
         # key
         if 'time_elapsed' in dict_:
             setattr(res, 'time_elapsed',
                     TimeAttribute.from_dict(dict_['time_elapsed']))
 
-        for root, result_dict in six.iteritems(dict_['tests']):
-            result = TestResult.from_dict(result_dict)
-            result.root = root
-            res.results.append(result)
+        res.results = [TestResult.from_dict(t) for t in dict_['results']]
 
+        for result in res.results:
             if result.subtests:
                 for subtest in six.iterkeys(result.subtests):
-                    fullname = grouptools.join(root, subtest)
+                    fullname = grouptools.join(result.root, subtest)
                     assert fullname not in res._tests
                     res._tests[fullname] = result
             else:
                 if result.root in res._tests:
                     # This can happen with old resumed test results.
                     print('Warning: duplicate results for {}'.format(result.root))
                 res._tests[result.root] = result
 
         if not 'totals' in dict_ and not _no_totals:
             res.calculate_group_totals()
diff --git a/framework/test/base.py b/framework/test/base.py
index 0b7ebab2e..a8f81f547 100644
--- a/framework/test/base.py
+++ b/framework/test/base.py
@@ -190,20 +190,21 @@ class Test(object):
         Run a test, but with features. This times the test, uses dmesg checking
         (if requested), and runs the logger.
 
         Arguments:
         path    -- the name of the test
         log     -- a log.Log instance
         options -- a dictionary containing dmesg and monitoring objects
         """
         log.start(path)
         # Run the test
+        self.result.root = path
         if OPTIONS.execute:
             try:
                 self.result.time.start = time.time()
                 options['dmesg'].update_dmesg()
                 options['monitor'].update_monitoring()
                 self.run()
                 self.result.time.end = time.time()
                 self.result = options['dmesg'].update_result(self.result)
                 options['monitor'].check_monitoring()
             # This is a rare case where a bare exception is okay, since we're
-- 
2.11.0



More information about the Piglit mailing list