[RFC,6/9] framwork: Add helper functions to abstract the backend better

Submitted by Dylan Baker on April 6, 2015, 9:30 p.m.

Details

Message ID 1428355819-12180-7-git-send-email-baker.dylan.c@gmail.com
State New, archived
Headers show

Not browsing as part of any series.

Commit Message

Dylan Baker April 6, 2015, 9:30 p.m.
This allows the backends to be treated abstractly. Uses simply query
some factory functions which will give them an instance to work with,
which provides a standardized interface. This allows backends to be
freely plugged without any concern for what the backend actually is, the
consumer doesn't need to know at all.

Signed-off-by: Dylan Baker <dylanx.c.baker@intel.com>
---
 framework/backends/__init__.py        |  68 +++++++++++++++++
 framework/backends/json.py            |   7 ++
 framework/backends/junit.py           |   2 +
 framework/backends/register.py        |   5 +-
 framework/programs/run.py             |   3 +-
 framework/programs/summary.py         |   2 +-
 framework/summary.py                  |   2 +-
 framework/tests/backends_tests.py     | 140 ++++++++++++++++++++++++++++++++++
 framework/tests/json_backend_tests.py |  13 ++++
 framework/tests/utils.py              |  21 +++++
 10 files changed, 259 insertions(+), 4 deletions(-)

Patch hide | download patch | download mbox

diff --git a/framework/backends/__init__.py b/framework/backends/__init__.py
index c1a3a0b..e2f199a 100644
--- a/framework/backends/__init__.py
+++ b/framework/backends/__init__.py
@@ -51,6 +51,8 @@  __all__ = [
     'BackendError',
     'BackendNotImplementedError',
     'get_backend',
+    'load',
+    'set_meta',
 ]
 
 
@@ -105,3 +107,69 @@  def get_backend(backend):
             'Backend for {} is not implemented'.format(backend))
 
     return inst
+
+
+def load(file_path):
+    """Wrapper for loading runs.
+
+    This function will attempt to determine how to load the file (based on file
+    extension), and then pass the file path into the appropriate loader, and
+    then return the TestrunResult instance.
+
+    """
+    extension = None
+
+    if os.path.isfile(file_path):
+        extension = os.path.splitext(file_path)[1]
+        if not extension:
+            extension = ''
+    else:
+        for file in os.listdir(file_path):
+            if file.startswith('result'):
+                extension = os.path.splitext(file)[1]
+                break
+            elif file == 'main':
+                extension = ''
+                break
+    tests = os.path.join(file_path, 'tests')
+    if extension is None:
+        if os.path.exists(tests):
+            extension = os.path.splitext(os.listdir(tests)[0])[1]
+        else:
+            # At this point we have failed to find any sort of backend, just except
+            # and die
+            raise BackendError("No backend found for any file in {}".format(
+                file_path))
+
+    for backend in BACKENDS.itervalues():
+        if extension in backend.extensions:
+            loader = backend.load
+            break
+    else:
+        raise BackendError(
+            'No module supports file extensions "{}"'.format(extension))
+
+    if loader is None:
+        raise BackendNotImplementedError(
+            'Loader for {} is not implemented'.format(extension))
+
+    return loader(file_path)
+
+
+def set_meta(backend, result):
+    """Wrapper around meta that gets the right meta function."""
+    try:
+        BACKENDS[backend].meta(result)
+    except KeyError:
+        raise BackendError('No backend {}'.format(backend))
+    except TypeError as e:
+        # Since we initialize non-implemented backends as None, and None isn't
+        # callable then we'll get a TypeError, and we're looking for NoneType
+        # in the message. If we get that we really want a
+        # BackendNotImplementedError
+        if e.message == "'NoneType' object is not callable":
+            raise BackendNotImplementedError(
+                'meta function for {} not implemented.'.format(backend))
+        else:
+            # Otherwise re-raise the error
+            raise
diff --git a/framework/backends/json.py b/framework/backends/json.py
index 52bdd08..affd64e 100644
--- a/framework/backends/json.py
+++ b/framework/backends/json.py
@@ -195,6 +195,11 @@  def load_results(filename):
     return _update_results(testrun, filepath)
 
 
+def set_meta(results):
+    """Set json specific metadata on a TestrunResult."""
+    results.results_version = CURRENT_JSON_VERSION
+
+
 def _load(results_file):
     """Load a json results instance and return a TestrunResult.
 
@@ -521,4 +526,6 @@  def _update_four_to_five(results):
 REGISTRY = Registry(
     extensions=['', '.json'],
     backend=JSONBackend,
+    load=load_results,
+    meta=set_meta,
 )
diff --git a/framework/backends/junit.py b/framework/backends/junit.py
index 122944b..5ec4e8e 100644
--- a/framework/backends/junit.py
+++ b/framework/backends/junit.py
@@ -210,4 +210,6 @@  class JUnitBackend(FileBackend):
 REGISTRY = Registry(
     extensions=['.xml'],
     backend=JUnitBackend,
+    load=None,
+    meta=lambda x: x,  # The venerable no-op function
 )
diff --git a/framework/backends/register.py b/framework/backends/register.py
index 589a0a8..a52083b 100644
--- a/framework/backends/register.py
+++ b/framework/backends/register.py
@@ -22,4 +22,7 @@ 
 
 import collections
 
-Registry = collections.namedtuple('Registry', ['extensions', 'backend'])
+Registry = collections.namedtuple(
+    'Registry',
+    ['extensions', 'backend', 'load', 'meta']
+)
diff --git a/framework/programs/run.py b/framework/programs/run.py
index 989ef45..ca94596 100644
--- a/framework/programs/run.py
+++ b/framework/programs/run.py
@@ -271,6 +271,7 @@  def run(input_):
     core.checkDir(args.results_path, False)
 
     results = framework.results.TestrunResult()
+    backends.set_meta(args.backend, results)
 
     # Set results.name
     if args.name is not None:
@@ -315,7 +316,7 @@  def resume(input_):
     args = parser.parse_args(input_)
     _disable_windows_exception_messages()
 
-    results = framework.results.TestrunResult.resume(args.results_path)
+    results = backends.load(args.results_path)
     opts = core.Options(concurrent=results.options['concurrent'],
                         exclude_filter=results.options['exclude_filter'],
                         include_filter=results.options['filter'],
diff --git a/framework/programs/summary.py b/framework/programs/summary.py
index f098856..fc397f4 100644
--- a/framework/programs/summary.py
+++ b/framework/programs/summary.py
@@ -155,7 +155,7 @@  def csv(input_):
     args = parser.parse_args(input_)
 
     try:
-        testrun = backends.json.load_results(args.testResults)
+        testrun = backends.load(args.testResults)
     except backends.errors.ResultsLoadError as e:
         print('Error: {}'.format(e.message), file=sys.stderr)
         sys.exit(1)
diff --git a/framework/summary.py b/framework/summary.py
index c027d76..9b30b5e 100644
--- a/framework/summary.py
+++ b/framework/summary.py
@@ -300,7 +300,7 @@  class Summary:
         # Create a Result object for each piglit result and append it to the
         # results list
         try:
-            self.results = [backends.json.load_results(i) for i in resultfiles]
+            self.results = [backends.load(i) for i in resultfiles]
         except backends.errors.ResultsLoadError as e:
             print('Error: {}'.format(e.message), file=sys.stderr)
             sys.exit(1)
diff --git a/framework/tests/backends_tests.py b/framework/tests/backends_tests.py
index c15543d..108f114 100644
--- a/framework/tests/backends_tests.py
+++ b/framework/tests/backends_tests.py
@@ -47,6 +47,30 @@  JUNIT_SCHEMA = 'framework/tests/schema/junit-7.xsd'
 
 doc_formatter = utils.DocFormatter({'seperator': grouptools.SEPARATOR})
 
+# Helpers
+
+def _notimplemented_setup():
+    """Setup function that injects a new test Registry into the BACKENDS
+    variable.
+
+    should be used in conjunction with the _registry_teardown method.
+
+    """
+    backends.BACKENDS['test_backend'] = backends.register.Registry(
+        extensions=['.test_backend'],
+        backend=None,
+        load=None,
+        meta=None,
+    )
+
+
+def _registry_teardown():
+    """Remove the test_backend Register from backends.BACKENDS."""
+    del backends.BACKENDS['test_backend']
+
+
+# Tests
+
 
 @utils.nose_generator
 def test_get_backend():
@@ -66,6 +90,122 @@  def test_get_backend():
         yield check, name, inst
 
 
+@nt.raises(backends.BackendError)
+def test_get_backend_unknown():
+    """backends.get_backend: An error is raised with an unkown backend."""
+    backends.get_backend('obviously fake backend')
+
+
+@nt.raises(backends.BackendNotImplementedError)
+@nt.with_setup(_notimplemented_setup, _registry_teardown)
+def test_get_backend_notimplemented():
+    """backends.get_backend: An error is raised if a backend isn't implemented.
+    """
+    backends.get_backend('test_backend')
+
+
+@nt.with_setup(teardown=_registry_teardown)
+@utils.test_in_tempdir
+def test_load():
+    """backends.load(): works as expected.
+
+    This is an interesting function to test, because it is just a wrapper that
+    returns a TestrunResult object. So most of the testing should be happening
+    in the tests for each backend.
+
+    However, we can test this by injecting a fake backend, and ensuring that we
+    get back what we expect. What we do is inject list(), which menas that we
+    should get back [file_path].
+
+    """
+    backends.BACKENDS['test_backend'] = backends.register.Registry(
+        extensions=['.test_extension'],
+        backend=None,
+        load=lambda x: [x],
+        meta=None,
+    )
+
+    file_path = 'foo.test_extension'
+    with open(file_path, 'w') as f:
+        f.write('foo')
+
+    test = backends.load(file_path)
+    nt.assert_list_equal([file_path], test)
+
+
+@nt.raises(backends.BackendError)
+@utils.test_in_tempdir
+def test_load_unknown():
+    """backends.load(): An error is raised if no modules supportes `extension`
+    """
+    file_path = 'foo.test_extension'
+
+    with open(file_path, 'w') as f:
+        f.write('foo')
+    backends.load(file_path)
+
+
+@utils.no_error
+@nt.with_setup(_notimplemented_setup, _registry_teardown)
+@utils.test_in_tempdir
+def test_load_resume():
+    """backends.load: works for resuming (no extension known)."""
+    backends.BACKENDS['test_backend'] = backends.register.Registry(
+        extensions=['.test_backend'],
+        backend=None,
+        load=lambda x: x,
+        meta=None,
+    )
+    os.mkdir('tests')
+    name = os.path.join('tests', '0.test_backend')
+    with open(name, 'w') as f:
+        f.write('foo')
+
+    backends.load('.')
+
+
+@nt.raises(backends.BackendNotImplementedError)
+@nt.with_setup(_notimplemented_setup, _registry_teardown)
+@utils.test_in_tempdir
+def test_load_notimplemented():
+    """backends.load(): An error is raised if a loader isn't properly implmented.
+    """
+    file_path = 'foo.test_backend'
+    with open(file_path, 'w') as f:
+        f.write('foo')
+
+    backends.load(file_path)
+
+
+def test_set_meta():
+    """backends.set_meta(): Works as expected."""
+    backends.BACKENDS['test_backend'] = backends.register.Registry(
+        extensions=None,
+        backend=None,
+        load=None,
+        meta=lambda x: x.append('bar'),
+    )
+
+    test = ['foo']
+
+    backends.set_meta('test_backend', test)
+    nt.assert_list_equal(test, ['foo', 'bar'])
+
+
+@nt.raises(backends.BackendError)
+def test_set_meta_no_backened():
+    """backends.set_meta: raises an error if there is no meta function."""
+    backends.set_meta('foo', {})
+
+
+@nt.raises(backends.BackendNotImplementedError)
+@nt.with_setup(_notimplemented_setup, _registry_teardown)
+def test_set_meta_notimplemented():
+    """backends.load(): An error is raised if a set_meta isn't properly implmented.
+    """
+    backends.set_meta('test_backend', {})
+
+
 class TestJunitNoTests(utils.StaticDirectory):
     @classmethod
     def setup_class(cls):
diff --git a/framework/tests/json_backend_tests.py b/framework/tests/json_backend_tests.py
index 54f24dd..45626e8 100644
--- a/framework/tests/json_backend_tests.py
+++ b/framework/tests/json_backend_tests.py
@@ -273,3 +273,16 @@  def test_load_results_file():
     """ Test that load_results takes a file """
     with utils.resultfile() as tfile:
         backends.json.load_results(tfile.name)
+
+
+def test_load_json():
+    """backends.load(): Loads .json files."""
+    with utils.tempdir() as tdir:
+        filename = os.path.join(tdir, 'results.json')
+        with open(filename, 'w') as f:
+            json.dump(utils.JSON_DATA, f)
+
+        result = backends.load(filename)
+
+    nt.assert_is_instance(result, results.TestrunResult)
+    nt.assert_in('sometest', result.tests)
diff --git a/framework/tests/utils.py b/framework/tests/utils.py
index 556f523..f59f6eb 100644
--- a/framework/tests/utils.py
+++ b/framework/tests/utils.py
@@ -340,3 +340,24 @@  class DocFormatter(object):
             raise UtilsError(e)
 
         return func
+
+
+def test_in_tempdir(func):
+    """Decorator that moves to a new directory to run a test.
+
+    This decorator ensures that the test moves to a new directory, and then
+    returns to the original directory after the test completes.
+
+    """
+    original_dir = os.getcwd()
+
+    @functools.wraps(func)
+    def wrapper(*args, **kwargs):
+        with tempdir() as tdir:
+            try:
+                os.chdir(tdir)
+                func(*args, **kwargs)
+            finally:
+                os.chdir(original_dir)
+
+    return wrapper