[Intel-gfx] [PATCH 02/23] piglit: Import piglit

Damien Lespiau damien.lespiau at intel.com
Fri Nov 15 17:33:19 CET 2013


Let's embed a copy of the piglit test runner, so we don't this external
dependency, removing an excuse to not run a complete series of tests

Signed-off-by: Damien Lespiau <damien.lespiau at intel.com>
---
 .gitignore                         |   2 +
 piglit/framework/__init__.py       |  21 ++
 piglit/framework/core.py           | 696 +++++++++++++++++++++++++++++++++++++
 piglit/framework/exectest.py       | 304 ++++++++++++++++
 piglit/framework/gleantest.py      |  49 +++
 piglit/framework/junit.py          | 377 ++++++++++++++++++++
 piglit/framework/log.py            |  53 +++
 piglit/framework/patterns.py       |  90 +++++
 piglit/framework/status.py         | 226 ++++++++++++
 piglit/framework/summary.py        | 525 ++++++++++++++++++++++++++++
 piglit/framework/threadpool.py     |  67 ++++
 piglit/framework/threads.py        |  43 +++
 piglit/piglit-framework-tests.py   |  47 +++
 piglit/piglit-merge-results.py     |  53 +++
 piglit/piglit-print-commands.py    |  86 +++++
 piglit/piglit-run.py               | 184 ++++++++++
 piglit/piglit-summary-html.py      |  98 ++++++
 piglit/piglit-summary-junit.py     | 128 +++++++
 piglit/piglit-summary.py           |  80 +++++
 piglit/templates/empty_status.mako |  27 ++
 piglit/templates/index.css         |  78 +++++
 piglit/templates/index.mako        |  81 +++++
 piglit/templates/result.css        |  37 ++
 piglit/templates/test_result.mako  |  66 ++++
 piglit/templates/testrun_info.mako |  49 +++
 25 files changed, 3467 insertions(+)
 create mode 100644 piglit/framework/__init__.py
 create mode 100644 piglit/framework/core.py
 create mode 100644 piglit/framework/exectest.py
 create mode 100644 piglit/framework/gleantest.py
 create mode 100644 piglit/framework/junit.py
 create mode 100644 piglit/framework/log.py
 create mode 100644 piglit/framework/patterns.py
 create mode 100644 piglit/framework/status.py
 create mode 100644 piglit/framework/summary.py
 create mode 100644 piglit/framework/threadpool.py
 create mode 100644 piglit/framework/threads.py
 create mode 100755 piglit/piglit-framework-tests.py
 create mode 100755 piglit/piglit-merge-results.py
 create mode 100755 piglit/piglit-print-commands.py
 create mode 100755 piglit/piglit-run.py
 create mode 100755 piglit/piglit-summary-html.py
 create mode 100755 piglit/piglit-summary-junit.py
 create mode 100755 piglit/piglit-summary.py
 create mode 100644 piglit/templates/empty_status.mako
 create mode 100644 piglit/templates/index.css
 create mode 100644 piglit/templates/index.mako
 create mode 100644 piglit/templates/result.css
 create mode 100644 piglit/templates/test_result.mako
 create mode 100644 piglit/templates/testrun_info.mako

diff --git a/.gitignore b/.gitignore
index f5b326e..c9a5302 100644
--- a/.gitignore
+++ b/.gitignore
@@ -78,6 +78,8 @@ core
 #
 *.swo
 *.swp
+*.pyc
+/.makotmp
 cscope.*
 TAGS
 build-aux/
diff --git a/piglit/framework/__init__.py b/piglit/framework/__init__.py
new file mode 100644
index 0000000..3cf6d82
--- /dev/null
+++ b/piglit/framework/__init__.py
@@ -0,0 +1,21 @@
+#
+# Permission is hereby granted, free of charge, to any person
+# obtaining a copy of this software and associated documentation
+# files (the "Software"), to deal in the Software without
+# restriction, including without limitation the rights to use,
+# copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following
+# conditions:
+#
+# This permission notice shall be included in all copies or
+# substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE AUTHOR(S) BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+# AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
+# OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+# DEALINGS IN THE SOFTWARE.
diff --git a/piglit/framework/core.py b/piglit/framework/core.py
new file mode 100644
index 0000000..e7767c2
--- /dev/null
+++ b/piglit/framework/core.py
@@ -0,0 +1,696 @@
+
+# Permission is hereby granted, free of charge, to any person
+# obtaining a copy of this software and associated documentation
+# files (the "Software"), to deal in the Software without
+# restriction, including without limitation the rights to use,
+# copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following
+# conditions:
+#
+# This permission notice shall be included in all copies or
+# substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE AUTHOR(S) BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+# AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
+# OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+# DEALINGS IN THE SOFTWARE.
+
+# Piglit core
+
+import errno
+import os
+import platform
+import re
+import stat
+import subprocess
+import string
+import sys
+import time
+import traceback
+from log import log
+from cStringIO import StringIO
+from textwrap import dedent
+from threads import synchronized_self
+import threading
+import multiprocessing
+try:
+    import simplejson as json
+except ImportError:
+    import json
+
+from threadpool import ThreadPool
+
+import status
+
+__all__ = ['Environment',
+           'checkDir',
+           'loadTestProfile',
+           'TestrunResult',
+           'GroupResult',
+           'TestResult',
+           'TestProfile',
+           'Group',
+           'Test',
+           'testBinDir']
+
+
+class JSONWriter:
+    '''
+    Writes to a JSON file stream
+
+    JSONWriter is threadsafe.
+
+    Example
+    -------
+
+    This call to ``json.dump``::
+        json.dump(
+            {
+                'a': [1, 2, 3],
+                'b': 4,
+                'c': {
+                    'x': 100,
+                },
+            }
+            file,
+            indent=JSONWriter.INDENT)
+
+    is equivalent to::
+        w = JSONWriter(file)
+        w.open_dict()
+        w.write_dict_item('a', [1, 2, 3])
+        w.write_dict_item('b', 4)
+        w.write_dict_item('c', {'x': 100})
+        w.close_dict()
+
+    which is also equivalent to::
+        w = JSONWriter(file)
+        w.open_dict()
+        w.write_dict_item('a', [1, 2, 3])
+        w.write_dict_item('b', 4)
+
+        w.write_dict_key('c')
+        w.open_dict()
+        w.write_dict_item('x', 100)
+        w.close_dict()
+
+        w.close_dict()
+    '''
+
+    INDENT = 4
+
+    def __init__(self, file):
+        self.file = file
+        self.__indent_level = 0
+        self.__inhibit_next_indent = False
+        self.__encoder = json.JSONEncoder(indent=self.INDENT)
+
+        # self.__is_collection_empty
+        #
+        # A stack that indicates if the currect collection is empty
+        #
+        # When open_dict is called, True is pushed onto the
+        # stack. When the first element is written to the newly
+        # opened dict, the top of the stack is set to False.
+        # When the close_dict is called, the stack is popped.
+        #
+        # The top of the stack is element -1.
+        #
+        # XXX: How does one attach docstrings to member variables?
+        #
+        self.__is_collection_empty = []
+
+    @synchronized_self
+    def __write_indent(self):
+        if self.__inhibit_next_indent:
+            self.__inhibit_next_indent = False
+            return
+        else:
+            i = ' ' * self.__indent_level * self.INDENT
+            self.file.write(i)
+
+    @synchronized_self
+    def __write(self, obj):
+        lines = list(self.__encoder.encode(obj).split('\n'))
+        n = len(lines)
+        for i in range(n):
+            self.__write_indent()
+            self.file.write(lines[i])
+            if i != n - 1:
+                self.file.write('\n')
+
+    @synchronized_self
+    def open_dict(self):
+        self.__write_indent()
+        self.file.write('{')
+
+        self.__indent_level += 1
+        self.__is_collection_empty.append(True)
+
+    @synchronized_self
+    def close_dict(self, comma=True):
+        self.__indent_level -= 1
+        self.__is_collection_empty.pop()
+
+        self.file.write('\n')
+        self.__write_indent()
+        self.file.write('}')
+
+    @synchronized_self
+    def write_dict_item(self, key, value):
+        # Write key.
+        self.write_dict_key(key)
+
+        # Write value.
+        self.__write(value)
+
+    @synchronized_self
+    def write_dict_key(self, key):
+        # Write comma if this is not the initial item in the dict.
+        if self.__is_collection_empty[-1]:
+            self.__is_collection_empty[-1] = False
+        else:
+            self.file.write(',')
+
+        self.file.write('\n')
+        self.__write(key)
+        self.file.write(': ')
+
+        self.__inhibit_next_indent = True
+
+
+# Ensure the given directory exists
+def checkDir(dirname, failifexists):
+    exists = True
+    try:
+        os.stat(dirname)
+    except OSError as e:
+        if e.errno == errno.ENOENT or e.errno == errno.ENOTDIR:
+            exists = False
+
+    if exists and failifexists:
+        print >>sys.stderr, "%(dirname)s exists already.\nUse --overwrite if" \
+                            "you want to overwrite it.\n" % locals()
+        exit(1)
+
+    try:
+        os.makedirs(dirname)
+    except OSError as e:
+        if e.errno != errno.EEXIST:
+            raise
+
+if 'PIGLIT_BUILD_DIR' in os.environ:
+    testBinDir = os.path.join(os.environ['PIGLIT_BUILD_DIR'], 'bin')
+else:
+    testBinDir = os.path.normpath(os.path.join(os.path.dirname(__file__),
+                                               '../bin'))
+
+if 'PIGLIT_SOURCE_DIR' not in os.environ:
+    p = os.path
+    os.environ['PIGLIT_SOURCE_DIR'] = p.abspath(p.join(p.dirname(__file__),
+                                                       '..'))
+
+# In debug builds, Mesa will by default log GL API errors to stderr.
+# This is useful for application developers or driver developers
+# trying to debug applications that should execute correctly.  But for
+# piglit we expect to generate errors regularly as part of testing,
+# and for exhaustive error-generation tests (particularly some in
+# khronos's conformance suite), it can end up ooming your system
+# trying to parse the strings.
+if 'MESA_DEBUG' not in os.environ:
+    os.environ['MESA_DEBUG'] = 'silent'
+
+class TestResult(dict):
+    def __init__(self, *args):
+        dict.__init__(self, *args)
+
+        # Replace the result with a status object
+        try:
+            self['result'] = status.status_lookup(self['result'])
+        except KeyError:
+            # If there isn't a result (like when used by piglit-run), go on
+            # normally
+            pass
+
+
+class GroupResult(dict):
+    def get_subgroup(self, path, create=True):
+        '''
+        Retrieve subgroup specified by path
+
+        For example, ``self.get_subgroup('a/b/c')`` will attempt to
+        return ``self['a']['b']['c']``. If any subgroup along ``path``
+        does not exist, then it will be created if ``create`` is true;
+        otherwise, ``None`` is returned.
+        '''
+        group = self
+        for subname in path.split('/'):
+            if subname not in group:
+                if create:
+                    group[subname] = GroupResult()
+                else:
+                    return None
+            group = group[subname]
+            assert(isinstance(group, GroupResult))
+        return group
+
+    @staticmethod
+    def make_tree(tests):
+        '''
+        Convert a flat dict of test results to a hierarchical tree
+
+        ``tests`` is a dict whose items have form ``(path, TestResult)``,
+        where path is a string with form ``group1/group2/.../test_name``.
+
+        Return a tree whose leaves are the values of ``tests`` and
+        whose nodes, which have type ``GroupResult``, reflect the
+        paths in ``tests``.
+        '''
+        root = GroupResult()
+
+        for (path, result) in tests.items():
+            group_path = os.path.dirname(path)
+            test_name = os.path.basename(path)
+
+            group = root.get_subgroup(group_path)
+            group[test_name] = TestResult(result)
+
+        return root
+
+
+class TestrunResult:
+    def __init__(self, resultfile=None):
+        self.serialized_keys = ['options',
+                                'name',
+                                'tests',
+                                'wglinfo',
+                                'glxinfo',
+                                'lspci',
+                                'time_elapsed']
+        self.name = None
+        self.glxinfo = None
+        self.lspci = None
+        self.time_elapsed = None
+        self.tests = {}
+
+        if resultfile:
+            # Attempt to open the json file normally, if it fails then attempt
+            # to repair it.
+            try:
+                raw_dict = json.load(resultfile)
+            except ValueError:
+                raw_dict = json.load(self.__repairFile(resultfile))
+
+            # Check that only expected keys were unserialized.
+            for key in raw_dict:
+                if key not in self.serialized_keys:
+                    raise Exception('unexpected key in results file: ', str(key))
+
+            self.__dict__.update(raw_dict)
+
+            # Replace each raw dict in self.tests with a TestResult.
+            for (path, result) in self.tests.items():
+                self.tests[path] = TestResult(result)
+
+    def __repairFile(self, file):
+        '''
+        Reapair JSON file if necessary
+
+        If the JSON file is not closed properly, perhaps due a system
+        crash during a test run, then the JSON is repaired by
+        discarding the trailing, incomplete item and appending braces
+        to the file to close the JSON object.
+
+        The repair is performed on a string buffer, and the given file
+        is never written to. This allows the file to be safely read
+        during a test run.
+
+        :return: If no repair occured, then ``file`` is returned.
+                Otherwise, a new file object containing the repaired JSON
+                is returned.
+        '''
+
+        file.seek(0)
+        lines = file.readlines()
+
+        # JSON object was not closed properly.
+        #
+        # To repair the file, we execute these steps:
+        #   1. Find the closing brace of the last, properly written
+        #      test result.
+        #   2. Discard all subsequent lines.
+        #   3. Remove the trailing comma of that test result.
+        #   4. Append enough closing braces to close the json object.
+        #   5. Return a file object containing the repaired JSON.
+
+        # Each non-terminal test result ends with this line:
+        safe_line = 2 * JSONWriter.INDENT * ' ' + '},\n'
+
+        # Search for the last occurence of safe_line.
+        safe_line_num = None
+        for i in range(-1, - len(lines), -1):
+            if lines[i] == safe_line:
+                safe_line_num = i
+                break
+
+        if safe_line_num is None:
+            raise Exception('failed to repair corrupt result file: ' +
+                            file.name)
+
+        # Remove corrupt lines.
+        lines = lines[0:(safe_line_num + 1)]
+
+        # Remove trailing comma.
+        lines[-1] = 2 * JSONWriter.INDENT * ' ' + '}\n'
+
+        # Close json object.
+        lines.append(JSONWriter.INDENT * ' ' + '}\n')
+        lines.append('}')
+
+        # Return new file object containing the repaired JSON.
+        new_file = StringIO()
+        new_file.writelines(lines)
+        new_file.flush()
+        new_file.seek(0)
+        return new_file
+
+    def write(self, file):
+        # Serialize only the keys in serialized_keys.
+        keys = set(self.__dict__.keys()).intersection(self.serialized_keys)
+        raw_dict = dict([(k, self.__dict__[k]) for k in keys])
+        json.dump(raw_dict, file, indent=JSONWriter.INDENT)
+
+
+class Environment:
+    def __init__(self, concurrent=True, execute=True, include_filter=[],
+                 exclude_filter=[], valgrind=False, dmesg=False):
+        self.concurrent = concurrent
+        self.execute = execute
+        self.filter = []
+        self.exclude_filter = []
+        self.exclude_tests = set()
+        self.valgrind = valgrind
+        self.dmesg = dmesg
+
+        """
+        The filter lists that are read in should be a list of string objects,
+        however, the filters need to be a list or regex object.
+
+        This code uses re.compile to rebuild the lists and set self.filter
+        """
+        for each in include_filter:
+            self.filter.append(re.compile(each))
+        for each in exclude_filter:
+            self.exclude_filter.append(re.compile(each))
+
+    def run(self, command):
+        try:
+            p = subprocess.Popen(command,
+                                 stdout=subprocess.PIPE,
+                                 stderr=subprocess.PIPE,
+                                 universal_newlines=True)
+            (stdout, stderr) = p.communicate()
+        except:
+            return "Failed to run " + command
+        return stderr+stdout
+
+    def collectData(self):
+        result = {}
+        system = platform.system()
+        if (system == 'Windows' or system.find("CYGWIN_NT") == 0):
+            result['wglinfo'] = self.run('wglinfo')
+        else:
+            result['glxinfo'] = self.run('glxinfo')
+        if system == 'Linux':
+            result['lspci'] = self.run('lspci')
+        return result
+
+
+class Test:
+    ignoreErrors = []
+
+    def __init__(self, runConcurrent=False):
+        '''
+                'runConcurrent' controls whether this test will
+                execute it's work (i.e. __doRunWork) on the calling thread
+                (i.e. the main thread) or from the ConcurrentTestPool threads.
+        '''
+        self.runConcurrent = runConcurrent
+        self.skip_test = False
+
+    def run(self):
+        raise NotImplementedError
+
+    def execute(self, env, path, json_writer):
+        '''
+        Run the test.
+
+        :path:
+            Fully qualified test name as a string.  For example,
+            ``spec/glsl-1.30/preprocessor/compiler/keywords/void.frag``.
+        '''
+        def status(msg):
+            log(msg=msg, channel=path)
+
+        # Run the test
+        if env.execute:
+            try:
+                status("running")
+                time_start = time.time()
+                result = self.run(env)
+                time_end = time.time()
+                if 'time' not in result:
+                    result['time'] = time_end - time_start
+                if 'result' not in result:
+                    result['result'] = 'fail'
+                if not isinstance(result, TestResult):
+                    result = TestResult(result)
+                    result['result'] = 'warn'
+                    result['note'] = 'Result not returned as an instance ' \
+                                     'of TestResult'
+            except:
+                result = TestResult()
+                result['result'] = 'fail'
+                result['exception'] = str(sys.exc_info()[0]) + \
+                    str(sys.exc_info()[1])
+                result['traceback'] = \
+                    "".join(traceback.format_tb(sys.exc_info()[2]))
+
+            status(result['result'])
+
+            json_writer.write_dict_item(path, result)
+        else:
+            status("dry-run")
+
+    # Returns True iff the given error message should be ignored
+    def isIgnored(self, error):
+        for pattern in Test.ignoreErrors:
+            if pattern.search(error):
+                return True
+
+        return False
+
+    # Default handling for stderr messages
+    def handleErr(self, results, err):
+        errors = filter(lambda s: len(s) > 0,
+                        map(lambda s: s.strip(), err.split('\n')))
+
+        ignored = [s for s in errors if self.isIgnored(s)]
+        errors = [s for s in errors if s not in ignored]
+
+        if len(errors) > 0:
+            results['errors'] = errors
+
+            if results['result'] == 'pass':
+                results['result'] = 'warn'
+
+        if len(ignored) > 0:
+            results['errors_ignored'] = ignored
+
+
+class Group(dict):
+    pass
+
+
+class TestProfile:
+    def __init__(self):
+        self.tests = Group()
+        self.test_list = {}
+
+    def flatten_group_hierarchy(self):
+        '''
+        Convert Piglit's old hierarchical Group() structure into a flat
+        dictionary mapping from fully qualified test names to "Test" objects.
+
+        For example,
+        tests['spec']['glsl-1.30']['preprocessor']['compiler']['void.frag']
+        would become:
+        test_list['spec/glsl-1.30/preprocessor/compiler/void.frag']
+        '''
+
+        def f(prefix, group, test_dict):
+            for key in group:
+                fullkey = key if prefix == '' else os.path.join(prefix, key)
+                if isinstance(group[key], dict):
+                    f(fullkey, group[key], test_dict)
+                else:
+                    test_dict[fullkey] = group[key]
+        f('', self.tests, self.test_list)
+        # Clear out the old Group()
+        self.tests = Group()
+
+    def prepare_test_list(self, env):
+        self.flatten_group_hierarchy()
+
+        def matches_any_regexp(x, re_list):
+            return True in map(lambda r: r.search(x) is not None, re_list)
+
+        def test_matches(item):
+            path, test = item
+            return ((not env.filter or matches_any_regexp(path, env.filter))
+                    and not path in env.exclude_tests and
+                    not matches_any_regexp(path, env.exclude_filter))
+
+        # Filter out unwanted tests
+        self.test_list = dict(filter(test_matches, self.test_list.items()))
+
+    def run(self, env, json_writer):
+        '''
+        Schedule all tests in profile for execution.
+
+        See ``Test.schedule`` and ``Test.run``.
+        '''
+
+        self.prepare_test_list(env)
+
+        # If using concurrency, add all the concurrent tests to the pool and
+        # execute that pool
+        if env.concurrent:
+            pool = ThreadPool(multiprocessing.cpu_count())
+            for (path, test) in self.test_list.items():
+                if test.runConcurrent:
+                    pool.add(test.execute, (env, path, json_writer))
+            pool.join()
+
+        # Run any remaining tests serially from a single thread pool after the
+        # concurrent tests have finished
+        pool = ThreadPool(1)
+        for (path, test) in self.test_list.items():
+            if not env.concurrent or not test.runConcurrent:
+                pool.add(test.execute, (env, path, json_writer))
+        pool.join()
+
+    def remove_test(self, test_path):
+        """Remove a fully qualified test from the profile.
+
+        ``test_path`` is a string with slash ('/') separated
+        components. It has no leading slash. For example::
+                test_path = 'spec/glsl-1.30/linker/do-stuff'
+        """
+
+        l = test_path.split('/')
+        group = self.tests[l[0]]
+        for group_name in l[1:-2]:
+            group = group[group_name]
+        del group[l[-1]]
+
+
+def loadTestProfile(filename):
+    ns = {'__file__': filename}
+    try:
+        execfile(filename, ns)
+    except:
+        traceback.print_exc()
+        raise Exception('Could not read tests profile')
+    return ns['profile']
+
+
+def load_results(filename):
+    """ 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"
+    
+    """
+    filename = os.path.realpath(filename)
+
+    try:
+        with open(filename, 'r') as resultsfile:
+            testrun = TestrunResult(resultsfile)
+    except IOError:
+        with open(os.path.join(filename, "main"), 'r') as resultsfile:
+            testrun = TestrunResult(resultsfile)
+
+    assert(testrun.name is not None)
+    return testrun
+
+
+# Error messages to be ignored
+Test.ignoreErrors = map(re.compile,
+                        ["couldn't open libtxc_dxtn.so",
+                         "compression/decompression available",
+                         "Mesa: .*build",
+                         "Mesa: CPU.*",
+                         "Mesa: .*cpu detected.",
+                         "Mesa: Test.*",
+                         "Mesa: Yes.*",
+                         "libGL: XF86DRIGetClientDriverName.*",
+                         "libGL: OpenDriver: trying.*",
+                         "libGL: Warning in.*drirc*",
+                         "ATTENTION.*value of option.*",
+                         "drmOpen.*",
+                         "Mesa: Not testing OS support.*",
+                         "Mesa: User error:.*",
+                         "Mesa: Initializing .* optimizations",
+                         "debug_get_.*",
+                         "util_cpu_caps.*",
+                         "Mesa: 3Dnow! detected",
+                         "r300:.*",
+                         "radeon:.*",
+                         "Warning:.*",
+                         "0 errors, .*",
+                         "Mesa.*",
+                         "no rrb",
+                         "; ModuleID.*",
+                         "%.*",
+                         ".*failed to translate tgsi opcode.*to SSE",
+                         ".*falling back to interpreter",
+                         "GLSL version is .*, but requested version .* is "
+                         "required",
+                         "kCGErrorIllegalArgument: CGSOrderWindowList",
+                         "kCGErrorFailure: Set a breakpoint @ "
+                         "CGErrorBreakpoint\(\) to catch errors as they are "
+                         "logged.",
+                         "stw_(init|cleanup).*",
+                         "OpenGLInfo..*",
+                         "AdapterInfo..*",
+                         "frameThrottleRate.*",
+                         ".*DeviceName.*",
+                         "No memory leaks detected.",
+                         "libGL: Can't open configuration file.*"])
+
+
+def parse_listfile(filename):
+    """
+    Parses a newline-seperated list in a text file and returns a python list
+    object. It will expand tildes on Unix-like system to the users home
+    directory.
+
+    ex file.txt:
+        ~/tests1
+        ~/tests2/main
+        /tmp/test3
+
+    returns:
+        ['/home/user/tests1', '/home/users/tests2/main', '/tmp/test3']
+    """
+    with open(filename, 'r') as file:
+        return [path.expanduser(i.rstrip('\n')) for i in file.readlines()]
diff --git a/piglit/framework/exectest.py b/piglit/framework/exectest.py
new file mode 100644
index 0000000..e239940
--- /dev/null
+++ b/piglit/framework/exectest.py
@@ -0,0 +1,304 @@
+#
+# Permission is hereby granted, free of charge, to any person
+# obtaining a copy of this software and associated documentation
+# files (the "Software"), to deal in the Software without
+# restriction, including without limitation the rights to use,
+# copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following
+# conditions:
+#
+# This permission notice shall be included in all copies or
+# substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE AUTHOR(S) BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+# AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
+# OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+# DEALINGS IN THE SOFTWARE.
+
+import errno
+import os
+import subprocess
+import threading
+import shlex
+import types
+import re
+
+from core import Test, testBinDir, TestResult
+
+
+# Platform global variables
+if 'PIGLIT_PLATFORM' in os.environ:
+    PIGLIT_PLATFORM = os.environ['PIGLIT_PLATFORM']
+else:
+    PIGLIT_PLATFORM = ''
+
+
+def read_dmesg():
+    proc = subprocess.Popen(['dmesg', '-l', 'emerg,alert,crit,err,warn,notice'], stdout=subprocess.PIPE)
+    return proc.communicate()[0].rstrip('\n')
+
+def get_dmesg_diff(old, new):
+    # Note that dmesg is a ring buffer, i.e. lines at the beginning may
+    # be removed when new lines are added.
+
+    # Get the last dmesg timestamp from the old dmesg as string.
+    last = old.split('\n')[-1]
+    ts = last[:last.find(']')+1]
+    if ts == '':
+        return ''
+
+    # Find the last occurence of the timestamp.
+    pos = new.find(ts)
+    if pos == -1:
+        return new # dmesg was completely overwritten by new messages
+
+    while pos != -1:
+        start = pos
+        pos = new.find(ts, pos+len(ts))
+
+    # Find the next line and return the rest of the string.
+    nl = new.find('\n', start+len(ts))
+    return new[nl+1:] if nl != -1 else ''
+
+
+# ExecTest: A shared base class for tests that simply runs an executable.
+class ExecTest(Test):
+    def __init__(self, command):
+        Test.__init__(self)
+        self.command = command
+        self.split_command = os.path.split(self.command[0])[1]
+        self.env = {}
+        self.timeout = None
+
+        if isinstance(self.command, basestring):
+            self.command = shlex.split(str(self.command))
+
+        self.skip_test = self.check_for_skip_scenario(command)
+
+    def interpretResult(self, out, returncode, results, dmesg):
+        raise NotImplementedError
+        return out
+
+    def run(self, env):
+        """
+        Run a test.  The return value will be a dictionary with keys
+        including 'result', 'info', 'returncode' and 'command'.
+        * For 'result', the value may be one of 'pass', 'fail', 'skip',
+          'crash', or 'warn'.
+        * For 'info', the value will include stderr/out text.
+        * For 'returncode', the value will be the numeric exit code/value.
+        * For 'command', the value will be command line program and arguments.
+        """
+        fullenv = os.environ.copy()
+        for e in self.env:
+            fullenv[e] = str(self.env[e])
+
+        if self.command is not None:
+            command = self.command
+
+            if env.valgrind:
+                command[:0] = ['valgrind', '--quiet', '--error-exitcode=1',
+                               '--tool=memcheck']
+
+            i = 0
+            dmesg_diff = ''
+            while True:
+                if self.skip_test:
+                    out = "PIGLIT: {'result': 'skip'}\n"
+                    err = ""
+                    returncode = None
+                else:
+                    if env.dmesg:
+                        old_dmesg = read_dmesg()
+                    (out, err, returncode, timeout) = \
+                        self.get_command_result(command, fullenv)
+                    if env.dmesg:
+                        dmesg_diff = get_dmesg_diff(old_dmesg, read_dmesg())
+
+                # https://bugzilla.gnome.org/show_bug.cgi?id=680214 is
+                # affecting many developers.  If we catch it
+                # happening, try just re-running the test.
+                if out.find("Got spurious window resize") >= 0:
+                    i = i + 1
+                    if i >= 5:
+                        break
+                else:
+                    break
+
+            # proc.communicate() returns 8-bit strings, but we need
+            # unicode strings.  In Python 2.x, this is because we
+            # will eventually be serializing the strings as JSON,
+            # and the JSON library expects unicode.  In Python 3.x,
+            # this is because all string operations require
+            # unicode.  So translate the strings into unicode,
+            # assuming they are using UTF-8 encoding.
+            #
+            # If the subprocess output wasn't properly UTF-8
+            # encoded, we don't want to raise an exception, so
+            # translate the strings using 'replace' mode, which
+            # replaces erroneous charcters with the Unicode
+            # "replacement character" (a white question mark inside
+            # a black diamond).
+            out = out.decode('utf-8', 'replace')
+            err = err.decode('utf-8', 'replace')
+
+            results = TestResult()
+
+            if self.skip_test:
+                results['result'] = 'skip'
+            else:
+                results['result'] = 'fail'
+                out = self.interpretResult(out, returncode, results, dmesg_diff)
+
+            crash_codes = [
+                # Unix: terminated by a signal
+                -5,   # SIGTRAP
+                -6,   # SIGABRT
+                -8,   # SIGFPE  (Floating point exception)
+                -10,  # SIGUSR1
+                -11,  # SIGSEGV (Segmentation fault)
+                # Windows:
+                # EXCEPTION_ACCESS_VIOLATION (0xc0000005):
+                -1073741819,
+                # EXCEPTION_INT_DIVIDE_BY_ZERO (0xc0000094):
+                -1073741676
+            ]
+
+            if returncode in crash_codes:
+                results['result'] = 'crash'
+            elif returncode != 0:
+                results['note'] = 'Returncode was {0}'.format(returncode)
+
+            if timeout:
+                results['result'] = 'timeout'
+
+            if env.valgrind:
+                # If the underlying test failed, simply report
+                # 'skip' for this valgrind test.
+                if results['result'] != 'pass':
+                    results['result'] = 'skip'
+                elif returncode == 0:
+                    # Test passes and is valgrind clean.
+                    results['result'] = 'pass'
+                else:
+                    # Test passed but has valgrind errors.
+                    results['result'] = 'fail'
+
+            env = ''
+            for key in self.env:
+                env = env + key + '="' + self.env[key] + '" '
+            if env:
+                results['environment'] = env
+
+            results['info'] = unicode("Returncode: {0}\n\nErrors:\n{1}\n\n"
+                                      "Output:\n{2}").format(returncode,
+                                                             err, out)
+            results['returncode'] = returncode
+            results['command'] = ' '.join(self.command)
+            results['dmesg'] = dmesg_diff
+            results['timeout'] = timeout
+
+            self.handleErr(results, err)
+
+        else:
+            results = TestResult()
+            if 'result' not in results:
+                results['result'] = 'skip'
+
+        return results
+
+    def check_for_skip_scenario(self, command):
+        global PIGLIT_PLATFORM
+        if PIGLIT_PLATFORM == 'gbm':
+            if 'glean' == self.split_command:
+                return True
+            if self.split_command.startswith('glx-'):
+                return True
+        return False
+
+    def get_command_result(self, command, fullenv):
+        try:
+            timeout = False
+            proc = subprocess.Popen(command,
+                                    stdout=subprocess.PIPE,
+                                    stderr=subprocess.PIPE,
+                                    env=fullenv,
+                                    universal_newlines=True)
+            output = ['', '']
+
+            def thread_fn():
+                output[0], output[1] = proc.communicate()
+
+            thread = threading.Thread(target=thread_fn)
+            thread.start()
+
+            thread.join(self.timeout)
+
+            if thread.is_alive():
+                proc.terminate()
+                thread.join()
+                timeout = True
+
+            returncode = proc.returncode
+            out, err = output
+        except OSError as e:
+            # Different sets of tests get built under
+            # different build configurations.  If
+            # a developer chooses to not build a test,
+            # Piglit should not report that test as having
+            # failed.
+            if e.errno == errno.ENOENT:
+                out = "PIGLIT: {'result': 'skip'}\n" \
+                    + "Test executable not found.\n"
+                err = ""
+                returncode = None
+            else:
+                raise e
+        return out, err, returncode, timeout
+
+
+class PlainExecTest(ExecTest):
+    """
+    PlainExecTest: Run a "native" piglit test executable
+
+    Expect one line prefixed PIGLIT: in the output, which contains a result
+    dictionary. The plain output is appended to this dictionary
+    """
+    def __init__(self, command):
+        ExecTest.__init__(self, command)
+        # Prepend testBinDir to the path.
+        self.command[0] = os.path.join(testBinDir, self.command[0])
+
+    def interpretResult(self, out, returncode, results, dmesg):
+        outlines = out.split('\n')
+        outpiglit = map(lambda s: s[7:],
+                        filter(lambda s: s.startswith('PIGLIT:'), outlines))
+
+        if dmesg != '':
+            outpiglit = map(lambda s: s.replace("'pass'", "'dmesg-warn'"), outpiglit)
+            outpiglit = map(lambda s: s.replace("'warn'", "'dmesg-warn'"), outpiglit)
+            outpiglit = map(lambda s: s.replace("'fail'", "'dmesg-fail'"), outpiglit)
+
+        if len(outpiglit) > 0:
+            try:
+                for piglit in outpiglit:
+                    if piglit.startswith('subtest'):
+                        if not 'subtest' in results:
+                            results['subtest'] = {}
+                        results['subtest'].update(eval(piglit[7:]))
+                    else:
+                        results.update(eval(piglit))
+                out = '\n'.join(filter(lambda s: not s.startswith('PIGLIT:'),
+                                       outlines))
+            except:
+                results['result'] = 'fail'
+                results['note'] = 'Failed to parse result string'
+
+        if 'result' not in results:
+            results['result'] = 'fail'
+        return out
diff --git a/piglit/framework/gleantest.py b/piglit/framework/gleantest.py
new file mode 100644
index 0000000..88432e0
--- /dev/null
+++ b/piglit/framework/gleantest.py
@@ -0,0 +1,49 @@
+#
+# Permission is hereby granted, free of charge, to any person
+
+# obtaining a copy of this software and associated documentation
+# files (the "Software"), to deal in the Software without
+# restriction, including without limitation the rights to use,
+# copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following
+# conditions:
+#
+# This permission notice shall be included in all copies or
+# substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE AUTHOR(S) BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+# AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
+# OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+# DEALINGS IN THE SOFTWARE.
+
+import os
+import subprocess
+
+from core import checkDir, testBinDir, Test, TestResult
+from exectest import ExecTest
+
+glean_executable = os.path.join(testBinDir, "glean")
+
+# GleanTest: Execute a sub-test of Glean
+class GleanTest(ExecTest):
+    globalParams = []
+
+    def __init__(self, name):
+        ExecTest.__init__(self, [glean_executable,
+                                 "-o", "-v", "-v", "-v", "-t",
+                                 "+"+name] + GleanTest.globalParams)
+        self.name = name
+
+    def interpretResult(self, out, returncode, results, dmesg):
+        if "{'result': 'skip'}" in out:
+            results['result'] = 'skip'
+        elif out.find('FAIL') >= 0:
+            results['result'] = 'dmesg-fail' if dmesg != '' else 'fail'
+        else:
+            results['result'] = 'dmesg-warn' if dmesg != '' else 'pass'
+        return out
diff --git a/piglit/framework/junit.py b/piglit/framework/junit.py
new file mode 100644
index 0000000..7916731
--- /dev/null
+++ b/piglit/framework/junit.py
@@ -0,0 +1,377 @@
+###########################################################################
+#
+# Copyright 2010-2011 VMware, Inc.
+# All Rights Reserved.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a
+# copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sub license, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice (including the
+# next paragraph) shall be included in all copies or substantial portions
+# of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT.
+# IN NO EVENT SHALL THE AUTHORS AND/OR ITS SUPPLIERS BE LIABLE FOR
+# ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+###########################################################################
+
+"""Testing framework that assists invoking external programs and outputing
+results in ANT's junit XML format, used by Jenkins-CI."""
+
+
+import locale
+import optparse
+import os.path
+import shutil
+import string
+import sys
+import time
+
+
+__all__ = [
+    'Error',
+    'Failure',
+    'Main',
+    'Report',
+    'Test',
+    'TestSuite',
+]
+
+
+class Failure(Exception):
+    pass
+
+
+class Error(Exception):
+    pass
+
+
+# Not all valid Unicode characters are valid XML.
+# See http://www.w3.org/TR/xml/#charsets
+_validXmlAscii = ''.join([((_c >= 0x20 and _c < 0x80) or _c in (0x9, 0xA, 0xD)) and chr(_c) or '?' for _c in range(256)])
+_validXmlUnicode = {}
+for _c in range(0x20):
+    if _c not in (0x9, 0xA, 0xD):
+        _validXmlUnicode[_c] = ord('?')
+del _c
+
+
+def escape(s):
+    '''Escape and encode a XML string.'''
+    if isinstance(s, unicode):
+        s = s.translate(_validXmlUnicode)
+    else:
+        #s = s.decode(locale.getpreferredencoding(), 'replace')
+        s = s.translate(_validXmlAscii)
+        s = s.decode('ascii', 'ignore')
+    s = s.replace('&', '&')
+    s = s.replace('<', '<')
+    s = s.replace('>', '>')
+    s = s.replace('"', '"')
+    s = s.replace("'", ''')
+    s = s.encode('UTF-8')
+    return s
+
+
+# same as string.printable, but without '\v\f'
+_printable = string.digits + string.letters + string.punctuation + ' \t\n\r'
+_printable = ''.join([chr(_c) in _printable and chr(_c) or '?' for _c in range(256)])
+del _c
+
+
+class Report:
+    """Write test results in ANT's junit XML format.
+
+    See also:
+    - https://github.com/jenkinsci/jenkins/tree/master/test/src/test/resources/hudson/tasks/junit
+    - http://www.junit.org/node/399
+    - http://wiki.apache.org/ant/Proposals/EnhancedTestReports
+    """
+
+    def __init__(self, filename, time = True):
+        self.path = os.path.dirname(os.path.abspath(filename))
+        if not os.path.exists(self.path):
+            os.makedirs(self.path)
+
+        self.stream = open(filename, 'wt')
+        self.testsuites = []
+        self.inside_testsuite = False
+        self.inside_testcase = False
+        self.time = time
+
+    def start(self):
+        self.stream.write('<?xml version="1.0" encoding="UTF-8" ?>\n')
+        self.stream.write('<testsuites>\n')
+
+    def stop(self):
+        if self.inside_testcase:
+            self.stream.write('</testcase>\n')
+            self.inside_testcase = False
+        if self.inside_testsuite:
+            self.stream.write('</testsuite>\n')
+            self.inside_testsuite = False
+        self.stream.write('</testsuites>\n')
+        self.stream.flush()
+        self.stream.close()
+
+    def escapeName(self, name):
+        '''Dots are special for junit, so escape them with underscores.'''
+        name = name.replace('.', '_')
+        return name
+
+    def startSuite(self, name):
+        self.testsuites.append(self.escapeName(name))
+
+    def stopSuite(self):
+        if self.inside_testsuite:
+            self.stream.write('</testsuite>\n')
+            self.inside_testsuite = False
+        self.testsuites.pop(-1)
+
+    def startCase(self, name):
+        assert not self.inside_testcase
+        self.inside_testcase = True
+
+        if not self.inside_testsuite:
+            self.stream.write('<testsuite name="%s">\n' % escape('.'.join(self.testsuites[:1])))
+            self.inside_testsuite = True
+
+        self.case_name = name
+        self.buffer = []
+        self.stdout = []
+        self.stderr = []
+        self.start_time = time.time()
+
+    def stopCase(self, duration = None):
+        assert self.inside_testcase
+        self.inside_testcase = False
+
+        if len(self.testsuites) == 1:
+            classname = self.testsuites[0] + '.' + self.testsuites[0]
+        else:
+            classname = '.'.join(self.testsuites)
+        name = self.case_name
+
+        self.stream.write('<testcase classname="%s" name="%s"' % (escape(classname), escape(name)))
+        if duration is None:
+            if self.time:
+                stop_time = time.time()
+                duration = stop_time - self.start_time
+        if duration is not None:
+            self.stream.write(' time="%f"' % duration)
+
+        if not self.buffer and not self.stdout and not self.stderr:
+            self.stream.write('/>\n')
+        else:
+            self.stream.write('>')
+
+            for entry in self.buffer:
+                self.stream.write(entry)
+            if self.stdout:
+                self.stream.write('<system-out>')
+                for text in self.stdout:
+                    self.stream.write(escape(text))
+                self.stream.write('</system-out>')
+            if self.stderr:
+                self.stream.write('<system-err>')
+                for text in self.stderr:
+                    self.stream.write(escape(text))
+                self.stream.write('</system-err>')
+
+            self.stream.write('</testcase>\n')
+
+        self.stream.flush()
+
+    def addStdout(self, text):
+        if isinstance(text, str):
+            text = text.translate(_printable)
+        self.stdout.append(text)
+
+    def addStderr(self, text):
+        if isinstance(text, str):
+            text = text.translate(_printable)
+        self.stderr.append(text)
+
+    def addSkipped(self):
+        self.buffer.append('<skipped/>\n')
+
+    def addError(self, message, stacktrace=""):
+        self.buffer.append('<error message="%s"' % escape(message))
+        if not stacktrace:
+            self.buffer.append('/>')
+        else:
+            self.buffer.append('>')
+            self.buffer.append(escape(stacktrace))
+            self.buffer.append('</error>')
+
+    def addFailure(self, message, stacktrace=""):
+        self.buffer.append('<failure message="%s"' % escape(message))
+        if not stacktrace:
+            self.buffer.append('/>')
+        else:
+            self.buffer.append('>')
+            self.buffer.append(escape(stacktrace))
+            self.buffer.append('</failure>')
+
+    def addMeasurement(self, name, value):
+        '''Embedded a measurement in the standard output.
+
+        https://wiki.jenkins-ci.org/display/JENKINS/Measurement+Plots+Plugin
+        '''
+
+        if value is not None:
+            message = '<measurement><name>%s</name><value>%f</value></measurement>\n' % (name, value)
+            self.addStdout(message)
+
+    def addAttachment(self, path):
+        '''Attach a file.
+
+        https://wiki.jenkins-ci.org/display/JENKINS/JUnit+Attachments+Plugin
+        '''
+
+        attach_dir = os.path.join(self.path, '.'.join(self.testsuites + [self.case_name]))
+        if not os.path.exists(attach_dir):
+            os.makedirs(attach_dir)
+        shutil.copy2(path, attach_dir)
+
+    def addWorkspaceURL(self, path):
+        import urlparse
+        try:
+            workspace_path = os.environ['WORKSPACE']
+            job_url = os.environ['JOB_URL']
+        except KeyError:
+            self.addStdout(path + '\n')
+        else:
+            rel_path = os.path.relpath(path, workspace_path)
+            workspace_url = urlparse.urljoin(job_url, 'ws/')
+            url = urlparse.urljoin(workspace_url, rel_path)
+            if os.path.isdir(path):
+                url += '/'
+            self.addStdout(url + '\n')
+
+
+class BaseTest:
+
+    def _visit(self, report):
+        raise NotImplementedError
+
+    def fail(self, *args):
+        raise Failure(*args)
+
+    def error(self, *args):
+        raise Error(*args)
+
+
+
+class TestSuite(BaseTest):
+
+    def __init__(self, name, tests=()):
+        self.name = name
+        self.tests = []
+        self.addTests(tests)
+
+    def addTest(self, test):
+        self.tests.append(test)
+
+    def addTests(self, tests):
+        for test in tests:
+            self.addTest(test)
+
+    def run(self, filename = None, report = None):
+        if report is None:
+            if filename is None:
+                filename = self.name + '.xml'
+        report = Report(filename)
+        report.start()
+        try:
+            self._visit(report)
+        finally:
+            report.stop()
+
+    def _visit(self, report):
+        report.startSuite(self.name)
+        try:
+            self.test(report)
+        finally:
+            report.stopSuite()
+
+    def test(self, report):
+        for test in self.tests:
+            test._visit(report)
+
+
+class Test(BaseTest):
+
+    def __init__(self, name):
+        self.name = name
+
+    def _visit(self, report):
+        report.startCase(self.name)
+        try:
+            try:
+                return self.test(report)
+            except Failure as ex:
+                report.addFailure(*ex.args)
+            except Error as ex:
+                report.addError(*ex.args)
+            except KeyboardInterrupt:
+                raise
+            except:
+                report.addError(str(sys.exc_value))
+        finally:
+            report.stopCase()
+
+    def test(self, report):
+        raise NotImplementedError
+
+
+class Main:
+
+    default_timeout = 5*60
+
+    def __init__(self, name):
+        self.name = name
+
+    def optparser(self):
+        optparser = optparse.OptionParser(usage="\n\t%prog [options] ...")
+        optparser.add_option(
+            '-n', '--dry-run',
+            action="store_true",
+            dest="dry_run", default=False,
+            help="perform a trial run without executing")
+        optparser.add_option(
+            '-t', '--timeout', metavar='SECONDS',
+            type="float", dest="timeout", default = self.default_timeout,
+            help="timeout in seconds [default: %default]")
+        #optparser.add_option(
+        #    '-f', '--filter',
+        #    action='append',
+        #    type="choice", metevar='GLOB',
+        #    dest="filters", default=[],
+        #    help="filter")
+        return optparser
+
+    def create_suite(self):
+        raise NotImplementedError
+
+    def run_suite(self, suite):
+        filename = self.name + '.xml'
+        report = Report(filename)
+        suite.run()
+
+    def main(self):
+        optparser = self.optparser()
+        (self.options, self.args) = optparser.parse_args(sys.argv[1:])
+
+        suite = self.create_suite()
+        self.run_suite(suite)
diff --git a/piglit/framework/log.py b/piglit/framework/log.py
new file mode 100644
index 0000000..310c552
--- /dev/null
+++ b/piglit/framework/log.py
@@ -0,0 +1,53 @@
+#
+# Copyright (c) 2010 Intel Corporation
+#
+# Permission is hereby granted, free of charge, to any person obtaining a
+# copy of this software and associated documentation files (the "Software"),
+# to deal in the Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish, distribute, sublicense,
+# and/or sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice (including the next
+# paragraph) shall be included in all copies or substantial portions of the
+# Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+#
+
+import logging
+
+from threads import synchronized_self
+from patterns import Singleton
+
+
+class Logger(Singleton):
+    @synchronized_self
+    def __logMessage(self, logfunc, message, **kwargs):
+        [logfunc(line, **kwargs) for line in message.split('\n')]
+
+    @synchronized_self
+    def getLogger(self, channel=None):
+        if 0 == len(logging.root.handlers):
+            logging.basicConfig(format="[%(asctime)s] :: %(message)+8s "
+                                       ":: %(name)s",
+                                datefmt="%c",
+                                level=logging.INFO)
+        if channel is None:
+            channel = "base"
+        logger = logging.getLogger(channel)
+        return logger
+
+    def log(self, type=logging.INFO, msg="", channel=None):
+        self.__logMessage(lambda m,
+                          **kwargs: self.getLogger(channel).log(type,
+                                                                m,
+                                                                **kwargs), msg)
+
+log = Logger().log
diff --git a/piglit/framework/patterns.py b/piglit/framework/patterns.py
new file mode 100644
index 0000000..bcf4e7e
--- /dev/null
+++ b/piglit/framework/patterns.py
@@ -0,0 +1,90 @@
+#
+# Copyright (c) 2010 Intel Corporation
+#
+# Permission is hereby granted, free of charge, to any person obtaining a
+# copy of this software and associated documentation files (the "Software"),
+# to deal in the Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish, distribute, sublicense,
+# and/or sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice (including the next
+# paragraph) shall be included in all copies or substantial portions of the
+# Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+#
+
+import threading
+
+
+class Singleton(object):
+    '''
+    Modeled after
+    http://www.python.org/download/releases/2.2.3/descrintro/*__new__
+
+    A thread-safe (mostly -- see NOTE) Singleton class pattern.
+
+    NOTE: deleting a singleton instance (i.e. Singleton::delInstance) does not
+    guarantee that nothing else is currently using it. To reduce this risk, a
+    program should not hold a reference to the instance.  Rather, use the
+    create/construct syntax (see example below) to access the instance.  Yet,
+    this still does not guarantee that this type of usage will result in a
+    desired effect in a multithreaded program.
+    You've been warned so use the singleton pattern wisely!
+
+    Example:
+
+    class MySingletonClass(Singleton):
+            def init(self):
+                    print "in MySingletonClass::init()", self
+
+            def foo(self):
+                    print "in MySingletonClass::foo()", self
+
+    MySingletonClass().foo()
+    MySingletonClass().foo()
+    MySingletonClass().foo()
+
+    ---> output will look something like this:
+    in MySingletonClass::init() <__main__.MySingletonClass object at 0x7ff5b322f3d0>
+    in MySingletonClass::foo() <__main__.MySingletonClass object at 0x7ff5b322f3d0>
+    in MySingletonClass::foo() <__main__.MySingletonClass object at 0x7ff5b322f3d0>
+    in MySingletonClass::foo() <__main__.MySingletonClass object at 0x7ff5b322f3d0>
+    '''
+
+    lock = threading.RLock()
+
+    def __new__(cls, *args, **kwargs):
+        try:
+            cls.lock.acquire()
+            it = cls.__dict__.get('__it__')
+            if it is not None:
+                return it
+            cls.__it__ = it = object.__new__(cls)
+            it.init(*args, **kwargs)
+            return it
+        finally:
+            cls.lock.release()
+
+    def init(self, *args, **kwargs):
+        '''
+        Derived classes should override this method to do its initializations
+        The derived class should not implement a '__init__' method.
+        '''
+        pass
+
+    @classmethod
+    def delInstance(cls):
+        cls.lock.acquire()
+        try:
+            if cls.__dict__.get('__it__') is not None:
+                del cls.__it__
+        finally:
+            cls.lock.release()
diff --git a/piglit/framework/status.py b/piglit/framework/status.py
new file mode 100644
index 0000000..3a9e2d3
--- /dev/null
+++ b/piglit/framework/status.py
@@ -0,0 +1,226 @@
+# Copyright (c) 2013 Intel Corporation
+#
+# Permission is hereby granted, free of charge, to any person obtaining a
+# copy of this software and associated documentation files (the "Software"),
+# to deal in the Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish, distribute, sublicense,
+# and/or sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice (including the next
+# paragraph) shall be included in all copies or substantial portions of the
+# Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+
+""" Status ordering from best to worst:
+
+pass
+dmesg-warn
+warn
+dmesg-fail
+fail
+crash
+timeout
+skip
+
+
+The following are regressions:
+
+pass|warn|dmesg-warn|fail|dmesg-fail|crash|timeout -> skip
+pass|warn|dmesg-warn|fail|dmesg-fail|crash -> timeout|skip
+pass|warn|dmesg-warn|fail|dmesg-fail -> crash|timeout|skip
+pass|warn|dmesg-warn|fail -> dmesg-fail|crash|timeout|skip
+pass|warn|dmesg-warn -> fail|dmesg-fail|crash|timeout|skip
+pass|warn -> dmesg-warn|fail|dmesg-fail|crash|timeout|skip
+pass -> warn|dmesg-warn|fail|dmesg-fail|crash|timeout|skip
+
+
+The following are fixes:
+
+skip -> pass|warn|dmesg-warn|fail|dmesg-fail|crash|timeout
+timeout|skip -> pass|warn|dmesg-warn|fail|dmesg-fail|crash
+crash|timeout|skip - >pass|warn|dmesg-warn|fail|dmesg-fail
+dmesg-fail|crash|timeout|skip -> pass|warn|dmesg-warn|fail
+fail|dmesg-fail|crash|timeout|skip -> pass|warn|dmesg-warn
+dmesg-warn|fail|dmesg-fail|crash|timeout|skip -> pass|warn
+warn|dmesg-warn|fail|dmesg-fail|crash|timeout|skip -> pass
+
+
+NotRun -> * and * -> NotRun is a change, but not a fix or a regression. This is
+because NotRun is not a status, but a representation of an unknown status.
+
+"""
+
+
+def status_lookup(status):
+    """ Provided a string return a status object instance
+    
+    When provided a string that corresponds to a key in it's status_dict
+    variable, this function returns a status object instance. If the string
+    does not correspond to a key it will raise an exception
+    
+    """
+    status_dict = {'skip': Skip,
+                   'pass': Pass,
+                   'warn': Warn,
+                   'fail': Fail,
+                   'crash': Crash,
+                   'dmesg-warn': DmesgWarn,
+                   'dmesg-fail': DmesgFail,
+                   'timeout' : Timeout,
+                   'notrun': NotRun}
+
+    try:
+        return status_dict[status]()
+    except KeyError:
+        # Raise a StatusException rather than a key error
+        raise StatusException
+
+
+class StatusException(LookupError):
+    """ Raise this exception when a string is passed to status_lookup that
+    doesn't exists
+    
+    The primary reason to have a special exception is that otherwise
+    status_lookup returns a KeyError, but there are many cases where it is
+    desireable to except a KeyError and have an exception path. Using a custom
+    Error class here allows for more fine-grained control.
+    
+    """
+    pass
+
+
+class Status(object):
+    """
+    A simple class for representing the output values of tests.
+
+    This is a base class, and should not be directly called. Instead a child
+    class should be created and called. This module provides 8 of them: Skip,
+    Pass, Warn, Fail, Crash, NotRun, DmesgWarn, and DmesgFail.
+    """
+
+    # Using __slots__ allows us to implement the flyweight pattern, limiting
+    # the memory consumed for creating tens of thousands of these objects.
+    __slots__ = ['name', 'value', 'fraction']
+
+    name = None
+    value = None
+    fraction = (0, 1)
+
+    def __init__(self):
+        raise NotImplementedError
+
+    def split(self, spliton):
+        return (self.name.split(spliton))
+
+    def __repr__(self):
+        return self.name
+
+    def __str__(self):
+        return str(self.name)
+
+    def __unicode__(self):
+        return unicode(self.name)
+
+    def __lt__(self, other):
+        return int(self) < int(other)
+
+    def __le__(self, other):
+        return int(self) <= int(other)
+
+    def __eq__(self, other):
+        return int(self) == int(other)
+
+    def __ne__(self, other):
+        return int(self) != int(other)
+
+    def __ge__(self, other):
+        return int(self) >= int(other)
+
+    def __gt__(self, other):
+        return int(self) > int(other)
+
+    def __int__(self):
+        return self.value
+
+
+class NotRun(Status):
+    name = 'Not Run'
+    value = 0
+    fraction = (0, 0)
+
+    def __init__(self):
+        pass
+
+
+class Pass(Status):
+    name = 'pass'
+    value = 10
+    fraction = (1, 1)
+
+    def __init__(self):
+        pass
+
+
+class DmesgWarn(Status):
+    name = 'dmesg-warn'
+    value = 20
+
+    def __init__(self):
+        pass
+
+
+class Warn(Status):
+    name = 'warn'
+    value = 25
+
+    def __init__(self):
+        pass
+
+
+class DmesgFail(Status):
+    name = 'dmesg-fail'
+    value = 30
+
+    def __init__(self):
+        pass
+
+
+class Fail(Status):
+    name = 'fail'
+    value = 35
+
+    def __init__(self):
+        pass
+
+
+class Crash(Status):
+    name = 'crash'
+    value = 40
+
+    def __init__(self):
+        pass
+
+
+class Timeout(Status):
+    name = 'timeout'
+    value = 50
+
+    def __init__(self):
+        pass
+
+
+class Skip(Status):
+    name = 'skip'
+    value = 60
+    fraction = (0, 0)
+
+    def __init__(self):
+        pass
diff --git a/piglit/framework/summary.py b/piglit/framework/summary.py
new file mode 100644
index 0000000..8fbe2a8
--- /dev/null
+++ b/piglit/framework/summary.py
@@ -0,0 +1,525 @@
+# Permission is hereby granted, free of charge, to any person
+# obtaining a copy of this software and associated documentation
+# files (the "Software"), to deal in the Software without
+# restriction, including without limitation the rights to use,
+# copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following
+# conditions:
+#
+# This permission notice shall be included in all copies or
+# substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE AUTHOR(S) BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+# AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
+# OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+# DEALINGS IN THE SOFTWARE.
+
+import os
+import os.path as path
+import itertools
+import shutil
+import collections
+import tempfile
+from mako.template import Template
+
+# a local variable status exists, prevent accidental overloading by renaming
+# the module
+import status as so
+import core
+
+
+__all__ = [
+    'Summary',
+]
+
+
+class HTMLIndex(list):
+    """
+    Builds HTML output to be passed to the index mako template, which will be
+    rendered into HTML pages. It does this by parsing the lists provided by the
+    Summary object, and returns itself, an object with one accessor, a list of
+    html strings that will be printed by the mako template.
+    """
+
+    def __init__(self, summary, page):
+        """
+        Steps through the list of groups and tests from all of the results and
+        generates a list of dicts that are passed to mako and turned into HTML
+        """
+
+        def returnList(open, close):
+            """
+            As HTMLIndex iterates through the groups and tests it uses this
+            function to determine which groups to close (and thus reduce the
+            depth of the next write) and which ones to open (thus increasing
+            the depth)
+
+            To that end one of two things happens, the path to the previous
+            group (close) and the next group (open) are equal, in that event we
+            don't want to open and close, becasue that will result in a
+            sawtooth pattern of a group with one test followed by the same
+            group with one test, over and over.  Instead we simply return two
+            empty lists, which will result in writing a long list of test
+            results.  The second option is that the paths are different, and
+            the function determines any commonality between the paths, and
+            returns the differences as close (the groups which are completly
+            written) and open (the new groups to write).
+            """
+            common = []
+
+            # Open and close are lists, representing the group hierarchy, open
+            # being the groups that need are soon to be written, and close
+            # representing the groups that have finished writing.
+            if open == close:
+                return [], []
+            else:
+                for i, j in itertools.izip_longest(open, close):
+                    if i != j:
+                        for k in common:
+                            open.remove(k)
+                            close.remove(k)
+                        return open, close
+                    else:
+                        common.append(i)
+
+        # set a starting depth of 1, 0 is used for 'all' so 1 is the
+        # next available group
+        depth = 1
+
+        # Current dir is a list representing the groups currently being
+        # written.
+        currentDir = []
+
+        # Add a new 'tab' for each result
+        self._newRow()
+        self.append({'type': 'other', 'text': '<td />'})
+        for each in summary.results:
+            self.append({'type': 'other',
+                         'text': '<td class="head"><b>%(name)s</b><br />'
+                                 '(<a href="%(href)s">info</a>)'
+                                 '</td>' % {'name': each.name,
+                                            'href': path.join(each.name,
+                                                              "index.html")}})
+        self._endRow()
+
+        # Add the toplevel 'all' group
+        self._newRow()
+        self._groupRow("head", 0, 'all')
+        for each in summary.results:
+            self._groupResult(summary.fractions[each.name]['all'],
+                              summary.status[each.name]['all'])
+        self._endRow()
+
+        # Add the groups and tests to the out list
+        for key in sorted(page):
+
+            # Split the group names and test names, then determine
+            # which groups to close and which to open
+            openList = key.split('/')
+            test = openList.pop()
+            openList, closeList = returnList(openList, list(currentDir))
+
+            # Close any groups in the close list
+            # for each group closed, reduce the depth by one
+            for i in reversed(closeList):
+                currentDir.remove(i)
+                depth -= 1
+
+            # Open new groups
+            for localGroup in openList:
+                self._newRow()
+
+                # Add the left-most column: the name of the group
+                self._groupRow("head", depth, localGroup)
+
+                # Add the group that we just opened to the currentDir, which
+                # will then be used to add that group to the HTML list. If
+                # there is a KeyError (the group doesn't exist), use (0, 0)
+                # which will get skip. This sets the group coloring correctly
+                currentDir.append(localGroup)
+                for each in summary.results:
+                    # Decide which fields need to be updated
+                    self._groupResult(
+                        summary.fractions[each.name][path.join(*currentDir)],
+                        summary.status[each.name][path.join(*currentDir)])
+
+                # After each group increase the depth by one
+                depth += 1
+                self._endRow()
+
+            # Add the tests for the current group
+            self._newRow()
+
+            # Add the left-most column: the name of the test
+            self._testRow("group", depth, test)
+
+            # Add the result from each test result to the HTML summary If there
+            # is a KeyError (a result doesn't contain a particular test),
+            # return Not Run, with clas skip for highlighting
+            for each in summary.results:
+                # If the "group" at the top of the key heirachy contains
+                # 'subtest' then it is really not a group, link to that page
+                try:
+                    if each.tests[path.dirname(key)]['subtest']:
+                        href = path.dirname(key)
+                except KeyError:
+                    href = key
+
+                try:
+                    self._testResult(each.name, href,
+                                     summary.status[each.name][key])
+                except KeyError:
+                    self.append({'type': 'other',
+                                 'text': '<td class="skip">Not Run</td>'})
+            self._endRow()
+
+    def _newRow(self):
+        self.append({'type': 'newRow'})
+
+    def _endRow(self):
+        self.append({'type': 'endRow'})
+
+    def _groupRow(self, cssclass, depth, groupname):
+        """
+        Helper function for appending new groups to be written out
+        in HTML.
+
+        This particular function is used to write the left most
+        column of the summary. (the one with the indents)
+        """
+        self.append({'type': "groupRow",
+                     'class': cssclass,
+                     'indent': (1.75 * depth),
+                     'text': groupname})
+
+    def _groupResult(self, value, css):
+        """
+        Helper function for appending the results of groups to the
+        HTML summary file.
+        """
+        # "Not Run" is not a valid css class replace it with skip
+        if isinstance(css, so.NotRun):
+            css = 'skip'
+
+        self.append({'type': "groupResult",
+                     'class': css,
+                     'text': "%s/%s" % (value[0], value[1])})
+
+    def _testRow(self, cssclass, depth, groupname):
+        """
+        Helper function for appending new tests to be written out
+        in HTML.
+
+        This particular function is used to write the left most
+        column of the summary. (the one with the indents)
+        """
+        self.append({'type': "testRow",
+                     'class': cssclass,
+                     'indent': (1.75 * depth),
+                     'text': groupname})
+
+    def _testResult(self, group, href, text):
+        """
+        Helper function for writing the results of tests
+
+        This function writes the cells other than the left-most cell,
+        displaying pass/fail/crash/etc and formatting the cell to the
+        correct color.
+        """
+        # "Not Run" is not a valid class, if it apears set the class to skip
+        if isinstance(text, so.NotRun):
+            css = 'skip'
+            href = None
+        else:
+            css = text
+            href = path.join(group, href + ".html")
+
+        self.append({'type': 'testResult',
+                     'class': css,
+                     'href': href,
+                     'text': text})
+
+
+class Summary:
+    """
+    This Summary class creates an initial object containing lists of tests
+    including all, changes, problems, skips, regressions, and fixes. It then
+    uses methods to generate various kinds of output. The reference
+    implementation is HTML output through mako, aptly named generateHTML().
+    """
+    TEMP_DIR = path.join(tempfile.gettempdir(), "piglit/html-summary")
+    TEMPLATE_DIR = path.join(os.environ['PIGLIT_SOURCE_DIR'], 'templates')
+
+    def __init__(self, resultfiles):
+        """
+        Create an initial object with all of the result information rolled up
+        in an easy to process form.
+
+        The constructor of the summary class has an attribute for each HTML
+        summary page, which are fed into the index.mako file to produce HTML
+        files. resultfiles is a list of paths to JSON results generated by
+        piglit-run.
+        """
+
+        # Create a Result object for each piglit result and append it to the
+        # results list
+        self.results = [core.load_results(i) for i in resultfiles]
+
+        self.status = {}
+        self.fractions = {}
+        self.totals = {}
+        self.tests = {'all': set(), 'changes': set(), 'problems': set(),
+                      'skipped': set(), 'regressions': set(), 'fixes': set()}
+
+        def fgh(test, result):
+            """ Helper for updating the fractions and status lists """
+            fraction[test] = tuple(
+                [sum(i) for i in zip(fraction[test], result.fraction)])
+            if result != so.Skip() and status[test] < result:
+                status[test] = result
+
+        for results in self.results:
+            # Create a set of all of the tset names across all of the runs
+            self.tests['all'] = set(self.tests['all'] | set(results.tests))
+
+            # Create two dictionaries that have a default factory: they return
+            # a default value instead of a key error.
+            # This default key must be callable
+            self.fractions[results.name] = collections.defaultdict(lambda: (0, 0))
+            self.status[results.name] = collections.defaultdict(so.NotRun)
+
+            # short names
+            fraction = self.fractions[results.name]
+            status = self.status[results.name]
+
+            # store the results to be appeneded to results. Adding them in the
+            # loop will cause a RuntimeError
+            temp_results = {}
+
+            for key, value in results.tests.iteritems():
+                # Treat a test with subtests as if it is a group, assign the
+                # subtests' statuses and fractions down to the test, and then
+                # proceed like normal.
+                try:
+                    for (subt, subv) in value['subtest'].iteritems():
+                        subt = path.join(key, subt)
+                        subv = so.status_lookup(subv)
+
+                        # Add the subtest to the fractions and status lists
+                        fraction[subt] = subv.fraction
+                        status[subt] = subv
+                        temp_results.update({subt: {'result': subv}})
+
+                        self.tests['all'].add(subt)
+                        while subt != '':
+                            fgh(subt, subv)
+                            subt = path.dirname(subt)
+                        fgh('all', subv)
+
+                    # remove the test from the 'all' list, this will cause to
+                    # be treated as a group
+                    self.tests['all'].discard(key)
+                except KeyError:
+                    # Walk the test name as if it was a path, at each level update
+                    # the tests passed over the total number of tests (fractions),
+                    # and update the status of the current level if the status of
+                    # the previous level was worse, but is not skip
+                    while key != '':
+                        fgh(key, value['result'])
+                        key = path.dirname(key)
+
+                    # when we hit the root update the 'all' group and stop
+                    fgh('all', value['result'])
+
+            # Update the the results.tests dictionary with the subtests so that
+            # they are entered into the appropriate pages other than all.
+            # Updating it in the loop will raise a RuntimeError
+            results.tests.update({k:v for k,v in temp_results.iteritems()})
+
+        # Create the lists of statuses like problems, regressions, fixes,
+        # changes and skips
+        for test in self.tests['all']:
+            status = []
+            for each in self.results:
+                try:
+                    status.append(each.tests[test]['result'])
+                except KeyError:
+                    status.append(so.NotRun())
+
+            # Problems include: warn, dmesg-warn, fail, dmesg-fail, and crash.
+            # Skip does not go on this page, it has the 'skipped' page
+            if so.Skip() > max(status) > so.Pass():
+                self.tests['problems'].add(test)
+
+            # Find all tests with a status of skip
+            if so.Skip() in status:
+                self.tests['skipped'].add(test)
+
+            # find fixes, regressions, and changes
+            for i in xrange(len(status) - 1):
+                first = status[i]
+                last = status[i + 1]
+                if first < last and so.NotRun() not in (first, last):
+                    self.tests['regressions'].add(test)
+                if first > last and so.NotRun() not in (first, last):
+                    self.tests['fixes'].add(test)
+                # Changes cannot be added in the fixes and regressions passes
+                # becasue NotRun is a change, but not a regression or fix
+                if first != last:
+                    self.tests['changes'].add(test)
+
+    def __find_totals(self):
+        """
+        Private: Find the total number of pass, fail, crash, skip, and warn in
+        the *last* set of results stored in self.results.
+        """
+        self.totals = {'pass': 0, 'fail': 0, 'crash': 0, 'skip': 0, 'warn': 0,
+                       'dmesg-warn': 0, 'dmesg-fail': 0}
+
+        for test in self.results[-1].tests.values():
+            self.totals[str(test['result'])] += 1
+
+    def generate_html(self, destination, exclude):
+        """
+        Produce HTML summaries.
+
+        Basically all this does is takes the information provided by the
+        constructor, and passes it to mako templates to generate HTML files.
+        The beauty of this approach is that mako is leveraged to do the
+        heavy lifting, this method just passes it a bunch of dicts and lists
+        of dicts, which mako turns into pretty HTML.
+        """
+
+        # Copy static files
+        shutil.copy(path.join(self.TEMPLATE_DIR, "index.css"),
+                    path.join(destination, "index.css"))
+        shutil.copy(path.join(self.TEMPLATE_DIR, "result.css"),
+                    path.join(destination, "result.css"))
+
+        # Create the mako object for creating the test/index.html file
+        testindex = Template(filename=path.join(self.TEMPLATE_DIR, "testrun_info.mako"),
+                             output_encoding="utf-8",
+                             module_directory=self.TEMP_DIR)
+
+        # Create the mako object for the individual result files
+        testfile = Template(filename=path.join(self.TEMPLATE_DIR, "test_result.mako"),
+                            output_encoding="utf-8",
+                            module_directory=self.TEMP_DIR)
+
+        result_css = path.join(destination, "result.css")
+        index = path.join(destination, "index.html")
+
+        # Iterate across the tests creating the various test specific files
+        for each in self.results:
+            os.mkdir(path.join(destination, each.name))
+
+            with open(path.join(destination, each.name, "index.html"), 'w') as out:
+                out.write(testindex.render(name=each.name,
+                                           time=each.time_elapsed,
+                                           options=each.options,
+                                           glxinfo=each.glxinfo,
+                                           lspci=each.lspci))
+
+            # Then build the individual test results
+            for key, value in each.tests.iteritems():
+                temp_path = path.join(destination, each.name, path.dirname(key))
+
+                if value['result'] not in exclude:
+                    # os.makedirs is very annoying, it throws an OSError if
+                    # the path requested already exists, so do this check to
+                    # ensure that it doesn't
+                    if not path.exists(temp_path):
+                        os.makedirs(temp_path)
+
+                    with open(path.join(destination, each.name, key + ".html"),
+                              'w') as out:
+                        out.write(testfile.render(
+                            testname=key,
+                            status=value.get('result', 'None'),
+                            # Returning a NoneType (instaed of 'None') prevents
+                            # this field from being generated.setting the
+                            # environment to run tests is ugly, and should
+                            # disapear at somepoint
+                            env=value.get('environment', None),
+                            returncode=value.get('returncode', 'None'),
+                            time=value.get('time', 'None'),
+                            info=value.get('info', 'None'),
+                            traceback=value.get('traceback', 'None'),
+                            command=value.get('command', 'None'),
+                            dmesg=value.get('dmesg', 'None'),
+                            css=path.relpath(result_css, temp_path),
+                            index=path.relpath(index, temp_path)))
+
+        # Finally build the root html files: index, regressions, etc
+        index = Template(filename=path.join(self.TEMPLATE_DIR, "index.mako"),
+                         output_encoding="utf-8",
+                         module_directory=self.TEMP_DIR)
+
+        empty_status = Template(filename=path.join(self.TEMPLATE_DIR, "empty_status.mako"),
+                                output_encoding="utf-8",
+                                module_directory=self.TEMP_DIR)
+
+        pages = ('changes', 'problems', 'skipped', 'fixes', 'regressions')
+
+        # Index.html is a bit of a special case since there is index, all, and
+        # alltests, where the other pages all use the same name. ie,
+        # changes.html, self.changes, and page=changes.
+        with open(path.join(destination, "index.html"), 'w') as out:
+            out.write(index.render(results=HTMLIndex(self, self.tests['all']),
+                                   page='all',
+                                   pages=pages,
+                                   colnum=len(self.results),
+                                   exclude=exclude))
+
+        # Generate the rest of the pages
+        for page in pages:
+            with open(path.join(destination, page + '.html'), 'w') as out:
+            # If there is information to display display it
+                if self.tests[page]:
+                    out.write(index.render(results=HTMLIndex(self,
+                                                             self.tests[page]),
+                                           pages=pages,
+                                           page=page,
+                                           colnum=len(self.results),
+                                           exclude=exclude))
+                # otherwise provide an empty page
+                else:
+                    out.write(empty_status.render(page=page, pages=pages))
+
+    def generate_text(self, diff, summary):
+        """ Write summary information to the console """
+        self.__find_totals()
+
+        # Print the name of the test and the status from each test run
+        if not summary:
+            if diff:
+                for test in self.tests['changes']:
+                    print "%(test)s: %(statuses)s" % {'test': test, 'statuses':
+                          ' '.join([str(i.tests.get(test, {'result': so.Skip()})
+                                    ['result']) for i in self.results])}
+            else:
+                for test in self.tests['all']:
+                    print "%(test)s: %(statuses)s" % {'test': test, 'statuses':
+                          ' '.join([str(i.tests.get(test, {'result': so.Skip()})
+                                    ['result']) for i in self.results])}
+
+        # Print the summary
+        print "summary:"
+        print "       pass: %d" % self.totals['pass']
+        print "       fail: %d" % self.totals['fail']
+        print "      crash: %d" % self.totals['crash']
+        print "       skip: %d" % self.totals['skip']
+        print "       warn: %d" % self.totals['warn']
+        print " dmesg-warn: %d" % self.totals['dmesg-warn']
+        print " dmesg-fail: %d" % self.totals['dmesg-fail']
+        if self.tests['changes']:
+            print "    changes: %d" % len(self.tests['changes'])
+            print "      fixes: %d" % len(self.tests['fixes'])
+            print "regressions: %d" % len(self.tests['regressions'])
+
+        print "      total: %d" % sum(self.totals.values())
diff --git a/piglit/framework/threadpool.py b/piglit/framework/threadpool.py
new file mode 100644
index 0000000..5d1fc56
--- /dev/null
+++ b/piglit/framework/threadpool.py
@@ -0,0 +1,67 @@
+# Copyright (c) 2013 Intel Corporation
+#
+# Permission is hereby granted, free of charge, to any person obtaining a
+# copy of this software and associated documentation files (the "Software"),
+# to deal in the Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish, distribute, sublicense,
+# and/or sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice (including the next
+# paragraph) shall be included in all copies or substantial portions of the
+# Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+
+# This code is based on the MIT licensed code by Emilio Monti found here:
+# http://code.activestate.com/recipes/577187-python-thread-pool/
+
+from Queue import Queue
+from threading import Thread
+
+
+class Worker(Thread):
+    """
+    Simple worker thread
+
+    This worker simply consumes tasks off of the queue until it is empty and
+    then waits for more tasks.
+    """
+
+    def __init__(self, queue):
+        Thread.__init__(self)
+        self.queue = queue
+        self.daemon = True
+        self.start()
+
+    def run(self):
+        """ This method is called in the constructor by self.start() """
+        while True:
+            func, args = self.queue.get()
+            func(*args)  # XXX: Does this need to be try/except-ed?
+            self.queue.task_done()
+
+
+class ThreadPool(object):
+    """
+    A simple ThreadPool class that maintains a Queue object and a set of Worker
+    threads.
+    """
+
+    def __init__(self, thread_count):
+        self.queue = Queue(thread_count)
+        self.threads = [Worker(self.queue) for _ in xrange(thread_count)]
+
+    def add(self, func, args):
+        """ Add a function and it's arguments to the queue as a tuple """
+        self.queue.put((func, args))
+
+    def join(self):
+        """ Block until self.queue is empty """
+        self.queue.join()
diff --git a/piglit/framework/threads.py b/piglit/framework/threads.py
new file mode 100644
index 0000000..ec7dfcc
--- /dev/null
+++ b/piglit/framework/threads.py
@@ -0,0 +1,43 @@
+#
+# Copyright (c) 2010 Intel Corporation
+#
+# Permission is hereby granted, free of charge, to any person obtaining a
+# copy of this software and associated documentation files (the "Software"),
+# to deal in the Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish, distribute, sublicense,
+# and/or sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice (including the next
+# paragraph) shall be included in all copies or substantial portions of the
+# Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+#
+
+from weakref import WeakKeyDictionary
+from threading import RLock
+
+
+def synchronized_self(function):
+    '''
+    A decorator function for providing multithreaded, synchronized access
+    amongst one or more functions within a class instance.
+    '''
+    def wrapper(self, *args, **kwargs):
+        synchronized_self.locks.setdefault(self, RLock()).acquire()
+        try:
+            return function(self, *args, **kwargs)
+        finally:
+            synchronized_self.locks[self].release()
+    return wrapper
+
+
+# track the locks for each instance
+synchronized_self.locks = WeakKeyDictionary()
diff --git a/piglit/piglit-framework-tests.py b/piglit/piglit-framework-tests.py
new file mode 100755
index 0000000..796b1e1
--- /dev/null
+++ b/piglit/piglit-framework-tests.py
@@ -0,0 +1,47 @@
+#!/usr/bin/env python
+#
+# Copyright (c) 2013 Intel Corporation
+#
+# Permission is hereby granted, free of charge, to any person obtaining a
+# copy of this software and associated documentation files (the "Software"),
+# to deal in the Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish, distribute, sublicense,
+# and/or sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice (including the next
+# paragraph) shall be included in all copies or substantial portions of the
+# Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+
+import argparse
+import unittest
+
+import framework.tests.summary
+
+# Create a dictionary of all of tests. Do this before the parser so we can use
+# it as a list of optional arguments for the parser
+tests = {"summary": unittest.TestLoader().loadTestsFromModule(framework.tests.summary)}
+
+parser = argparse.ArgumentParser()
+parser.add_argument("tests",
+                    action="append",
+                    choices=tests.keys(),
+                    help="Testing profiles for the framework")
+parser.add_argument("-v", "--verbose",
+                    action="store",
+                    choices=['0', '1', '2'],
+                    default='1',
+                    help="Set the level of verbosity to run tests at")
+args = parser.parse_args()
+
+# Run the tests
+map(unittest.TextTestRunner(verbosity=int(args.verbose)).run,
+    [tests[x] for x in args.tests])
diff --git a/piglit/piglit-merge-results.py b/piglit/piglit-merge-results.py
new file mode 100755
index 0000000..e78a5d0
--- /dev/null
+++ b/piglit/piglit-merge-results.py
@@ -0,0 +1,53 @@
+#!/usr/bin/env python
+#
+# Permission is hereby granted, free of charge, to any person
+# obtaining a copy of this software and associated documentation
+# files (the "Software"), to deal in the Software without
+# restriction, including without limitation the rights to use,
+# copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following
+# conditions:
+#
+# This permission notice shall be included in all copies or
+# substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE AUTHOR(S) BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+# AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
+# OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+# DEALINGS IN THE SOFTWARE.
+
+
+import argparse
+import sys
+import os.path
+
+sys.path.append(os.path.dirname(os.path.realpath(sys.argv[0])))
+import framework.core as core
+
+
+def main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument("results",
+                        metavar="<First Results File>",
+                        nargs="*",
+                        help="Space seperated list of results files")
+    args = parser.parse_args()
+
+    combined = core.load_results(args.results.pop(0))
+
+    for resultsDir in args.results:
+        results = core.load_results(resultsDir)
+
+        for testname, result in results.tests.items():
+            combined.tests[testname] = result
+
+    combined.write(sys.stdout)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/piglit/piglit-print-commands.py b/piglit/piglit-print-commands.py
new file mode 100755
index 0000000..c42ea6d
--- /dev/null
+++ b/piglit/piglit-print-commands.py
@@ -0,0 +1,86 @@
+#!/usr/bin/env python
+#
+# Permission is hereby granted, free of charge, to any person
+# obtaining a copy of this software and associated documentation
+# files (the "Software"), to deal in the Software without
+# restriction, including without limitation the rights to use,
+# copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following
+# conditions:
+#
+# This permission notice shall be included in all copies or
+# substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE AUTHOR(S) BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+# AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
+# OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+# DEALINGS IN THE SOFTWARE.
+
+
+import argparse
+import sys
+import os
+import os.path as path
+import time
+import traceback
+
+sys.path.append(path.dirname(path.realpath(sys.argv[0])))
+import framework.core as core
+from framework.exectest import ExecTest
+from framework.gleantest import GleanTest
+
+
+def main():
+    parser = argparse.ArgumentParser(sys.argv)
+    parser.add_argument("-t", "--include-tests",
+                        default = [],
+                        action  = "append",
+                        metavar = "<regex>",
+                        help    = "Run only matching tests (can be used more than once)")
+    parser.add_argument("-x", "--exclude-tests",
+                        default=[],
+                        action="append",
+                        metavar="<regex>",
+                        help="Exclude matching tests (can be used more than "
+                             "once)")
+    parser.add_argument("testProfile",
+                        metavar="<Path to testfile>",
+                        help="Path to results folder")
+    args = parser.parse_args()
+
+    # Set the environment, pass in the included and excluded tests
+    env = core.Environment(exclude_filter=args.exclude_tests,
+                           include_filter=args.include_tests)
+
+    # Change to the piglit's path
+    piglit_dir = path.dirname(path.realpath(sys.argv[0]))
+    os.chdir(piglit_dir)
+
+    profile = core.loadTestProfile(args.testProfile)
+
+    def getCommand(test):
+        command = ''
+        if isinstance(test, GleanTest):
+            for var, val in test.env.items():
+                command += var + "='" + val + "' "
+
+        # Make the test command relative to the piglit_dir
+        testCommand = test.command[:]
+        testCommand[0] = os.path.relpath(testCommand[0], piglit_dir)
+
+        command += ' '.join(testCommand)
+        return command
+
+    profile.prepare_test_list(env)
+    for name, test in profile.test_list.items():
+        assert(isinstance(test, ExecTest))
+        print name, ':::', getCommand(test)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/piglit/piglit-run.py b/piglit/piglit-run.py
new file mode 100755
index 0000000..1d63cd4
--- /dev/null
+++ b/piglit/piglit-run.py
@@ -0,0 +1,184 @@
+#!/usr/bin/env python
+#
+# Permission is hereby granted, free of charge, to any person
+# obtaining a copy of this software and associated documentation
+# files (the "Software"), to deal in the Software without
+# restriction, including without limitation the rights to use,
+# copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following
+# conditions:
+#
+# This permission notice shall be included in all copies or
+# substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE AUTHOR(S) BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+# AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
+# OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+# DEALINGS IN THE SOFTWARE.
+
+
+import argparse
+import sys
+import os
+import os.path as path
+import time
+import traceback
+
+sys.path.append(path.dirname(path.realpath(sys.argv[0])))
+import framework.core as core
+from framework.threads import synchronized_self
+
+
+def main():
+    parser = argparse.ArgumentParser(sys.argv)
+    # Either require that a name for the test is passed or that
+    # resume is requested
+    excGroup1 = parser.add_mutually_exclusive_group()
+    excGroup1.add_argument("-n", "--name",
+                           metavar="<test name>",
+                           default=None,
+                           help="Name of this test run")
+    excGroup1.add_argument("-r", "--resume",
+                           action="store_true",
+                           help="Resume an interupted test run")
+    # Setting the --dry-run flag is equivalent to env.execute=false
+    parser.add_argument("-d", "--dry-run",
+                        action="store_false",
+                        dest="execute",
+                        help="Do not execute the tests")
+    parser.add_argument("-t", "--include-tests",
+                        default=[],
+                        action="append",
+                        metavar="<regex>",
+                        help="Run only matching tests "
+                             "(can be used more than once)")
+    parser.add_argument("-x", "--exclude-tests",
+                        default=[],
+                        action="append",
+                        metavar="<regex>",
+                        help="Exclude matching tests "
+                             "(can be used more than once)")
+    parser.add_argument("-1", "--no-concurrency",
+                        action="store_false",
+                        dest="concurrency",
+                        help="Disable concurrent test runs")
+    parser.add_argument("-p", "--platform",
+                        choices=["glx", "x11_egl", "wayland", "gbm"],
+                        help="Name of windows system passed to waffle")
+    parser.add_argument("--valgrind",
+                        action="store_true",
+                        help="Run tests in valgrind's memcheck")
+    parser.add_argument("--dmesg",
+                        action="store_true",
+                        help="Capture a difference in dmesg before and "
+                             "after each test")
+    parser.add_argument("testProfile",
+                        metavar="<Path to test profile>",
+                        help="Path to testfile to run")
+    parser.add_argument("resultsPath",
+                        metavar="<Results Path>",
+                        help="Path to results folder")
+    args = parser.parse_args()
+
+    # Set the platform to pass to waffle
+    if args.platform is not None:
+        os.environ['PIGLIT_PLATFORM'] = args.platform
+
+    # Always Convert Results Path from Relative path to Actual Path.
+    resultsDir = path.realpath(args.resultsPath)
+
+    # If resume is requested attempt to load the results file
+    # in the specified path
+    if args.resume is True:
+        # Load settings from the old results JSON
+        old_results = core.load_results(resultsDir)
+        profileFilename = old_results.options['profile']
+
+        # Changing the args to the old args allows us to set them
+        # all in one places down the way
+        args.exclude_tests = old_results.options['exclude_filter']
+        args.include_tests = old_results.options['filter']
+
+    # Otherwise parse additional settings from the command line
+    else:
+        profileFilename = args.testProfile
+
+    # Pass arguments into Environment
+    env = core.Environment(concurrent=args.concurrency,
+                           exclude_filter=args.exclude_tests,
+                           include_filter=args.include_tests,
+                           execute=args.execute,
+                           valgrind=args.valgrind,
+                           dmesg=args.dmesg)
+
+    # Change working directory to the root of the piglit directory
+    piglit_dir = path.dirname(path.realpath(sys.argv[0]))
+    os.chdir(piglit_dir)
+
+    core.checkDir(resultsDir, False)
+
+    results = core.TestrunResult()
+
+    # Set results.name
+    if args.name is not None:
+        results.name = args.name
+    else:
+        results.name = path.basename(resultsDir)
+
+    # Begin json.
+    result_filepath = os.path.join(resultsDir, 'main')
+    result_file = open(result_filepath, 'w')
+    json_writer = core.JSONWriter(result_file)
+    json_writer.open_dict()
+
+    # Write out command line options for use in resuming.
+    json_writer.write_dict_key('options')
+    json_writer.open_dict()
+    json_writer.write_dict_item('profile', profileFilename)
+    json_writer.write_dict_item('filter', args.include_tests)
+    json_writer.write_dict_item('exclude_filter', args.exclude_tests)
+    json_writer.close_dict()
+
+    json_writer.write_dict_item('name', results.name)
+    for (key, value) in env.collectData().items():
+        json_writer.write_dict_item(key, value)
+
+    profile = core.loadTestProfile(profileFilename)
+
+    json_writer.write_dict_key('tests')
+    json_writer.open_dict()
+    # If resuming an interrupted test run, re-write all of the existing
+    # results since we clobbered the results file.  Also, exclude them
+    # from being run again.
+    if args.resume is True:
+        for (key, value) in old_results.tests.items():
+            if os.path.sep != '/':
+                key = key.replace(os.path.sep, '/', -1)
+            json_writer.write_dict_item(key, value)
+            env.exclude_tests.add(key)
+
+    time_start = time.time()
+    profile.run(env, json_writer)
+    time_end = time.time()
+
+    json_writer.close_dict()
+
+    results.time_elapsed = time_end - time_start
+    json_writer.write_dict_item('time_elapsed', results.time_elapsed)
+
+    # End json.
+    json_writer.close_dict()
+    json_writer.file.close()
+
+    print
+    print 'Thank you for running Piglit!'
+    print 'Results have been written to ' + result_filepath
+
+
+if __name__ == "__main__":
+    main()
diff --git a/piglit/piglit-summary-html.py b/piglit/piglit-summary-html.py
new file mode 100755
index 0000000..a37b337
--- /dev/null
+++ b/piglit/piglit-summary-html.py
@@ -0,0 +1,98 @@
+#!/usr/bin/env python
+#
+# Permission is hereby granted, free of charge, to any person
+# obtaining a copy of this software and associated documentation
+# files (the "Software"), to deal in the Software without
+# restriction, including without limitation the rights to use,
+# copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following
+# conditions:
+#
+# This permission notice shall be included in all copies or
+# substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE AUTHOR(S) BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+# AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
+# OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+# DEALINGS IN THE SOFTWARE.
+
+import argparse
+import sys
+import shutil
+import os.path as path
+
+import framework.summary as summary
+import framework.status as status
+from framework.core import checkDir, parse_listfile
+
+sys.path.append(path.dirname(path.realpath(sys.argv[0])))
+
+
+def main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument("-o", "--overwrite",
+                        action="store_true",
+                        help="Overwrite existing directories")
+    parser.add_argument("-l", "--list",
+                        action="store",
+                        help="Load a newline seperated list of results. These "
+                             "results will be prepended to any Results "
+                             "specified on the command line")
+    parser.add_argument("-e", "--exclude-details",
+                        default=[],
+                        action="append",
+                        choices=['skip', 'pass', 'warn', 'crash' 'fail',
+                                 'all'],
+                        help="Optionally exclude the generation of HTML pages "
+                             "for individual test pages with the status(es) "
+                             "given as arguments. This speeds up HTML "
+                             "generation, but reduces the info in the HTML "
+                             "pages. May be used multiple times")
+    parser.add_argument("summaryDir",
+                        metavar="<Summary Directory>",
+                        help="Directory to put HTML files in")
+    parser.add_argument("resultsFiles",
+                        metavar="<Results Files>",
+                        nargs="*",
+                        help="Results files to include in HTML")
+    args = parser.parse_args()
+
+    # If args.list and args.resultsFiles are empty, then raise an error
+    if not args.list and not args.resultsFiles:
+        raise parser.error("Missing required option -l or <resultsFiles>")
+
+    # Convert the exclude_details list to status objects, without this using
+    # the -e option will except
+    if args.exclude_details:
+        # If exclude-results has all, then change it to be all
+        if 'all' in args.exclude_details:
+            args.exclude_details = [status.Skip(), status.Pass(), status.Warn(),
+                                    status.Crash(), status.Fail()]
+        else:
+            args.exclude_details = [status.status_lookup(i) for i in
+                                    args.exclude_details]
+
+
+    # if overwrite is requested delete the output directory
+    if path.exists(args.summaryDir) and args.overwrite:
+        shutil.rmtree(args.summaryDir)
+
+    # If the requested directory doesn't exist, create it or throw an error
+    checkDir(args.summaryDir, not args.overwrite)
+
+    # Merge args.list and args.resultsFiles
+    if args.list:
+        args.resultsFiles.extend(parse_listfile(args.list))
+
+    # Create the HTML output
+    output = summary.Summary(args.resultsFiles)
+    output.generate_html(args.summaryDir, args.exclude_details)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/piglit/piglit-summary-junit.py b/piglit/piglit-summary-junit.py
new file mode 100755
index 0000000..a6f7aae
--- /dev/null
+++ b/piglit/piglit-summary-junit.py
@@ -0,0 +1,128 @@
+#!/usr/bin/env python
+#
+# Copyright 2010-2011 VMware, Inc.
+# All Rights Reserved.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a
+# copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sub license, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice (including the
+# next paragraph) shall be included in all copies or substantial portions
+# of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT.
+# IN NO EVENT SHALL THE AUTHORS AND/OR ITS SUPPLIERS BE LIABLE FOR
+# ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+import argparse
+import os
+import sys
+
+sys.path.append(os.path.dirname(os.path.realpath(sys.argv[0])))
+import framework.core as core
+import framework.status as status
+from framework import junit
+
+
+class Writer:
+
+    def __init__(self, filename):
+        self.report = junit.Report(filename)
+        self.path = []
+
+    def write(self, arg):
+        testrun = core.load_results(arg)
+
+        self.report.start()
+        self.report.startSuite('piglit')
+        try:
+            for (path, result) in testrun.tests.items():
+                self.write_test(testrun, path, result)
+        finally:
+            self.enter_path([])
+            self.report.stopSuite()
+            self.report.stop()
+
+    def write_test(self, testrun, test_path, result):
+        test_path = test_path.split('/')
+        test_name = test_path.pop()
+        self.enter_path(test_path)
+
+        self.report.startCase(test_name)
+        duration = None
+        try:
+            try:
+                self.report.addStdout(result['command'] + '\n')
+            except KeyError:
+                pass
+
+            try:
+                self.report.addStderr(result['info'])
+            except KeyError:
+                pass
+
+            success = result.get('result')
+            if success in (status.Pass(), status.Warn()):
+                pass
+            elif success == status.Skip():
+                self.report.addSkipped()
+            else:
+                self.report.addFailure(success.name)
+
+            try:
+                duration = float(result['time'])
+            except KeyError:
+                pass
+        finally:
+            self.report.stopCase(duration)
+
+    def enter_path(self, path):
+        ancestor = 0
+        try:
+            while self.path[ancestor] == path[ancestor]:
+                ancestor += 1
+        except IndexError:
+            pass
+
+        for dirname in self.path[ancestor:]:
+            self.report.stopSuite()
+
+        for dirname in path[ancestor:]:
+            self.report.startSuite(dirname)
+
+        self.path = path
+
+
+def main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument("-o", "--output",
+						metavar = "<Output File>",
+						action  = "store",
+						dest    = "output",
+						default = "piglit.xml",
+						help    = "Output filename")
+    parser.add_argument("testResults",
+						metavar = "<Input Files>",
+						help    = "JSON results file to be converted")
+    args = parser.parse_args()
+
+
+    writer = Writer(args.output)
+    writer.write(args.testResults)
+
+
+if __name__ == "__main__":
+    main()
+
+
+# vim:set sw=4 ts=4 noet:
diff --git a/piglit/piglit-summary.py b/piglit/piglit-summary.py
new file mode 100755
index 0000000..c3b7ea6
--- /dev/null
+++ b/piglit/piglit-summary.py
@@ -0,0 +1,80 @@
+#!/usr/bin/env python
+#
+# Permission is hereby granted, free of charge, to any person
+# obtaining a copy of this software and associated documentation
+# files (the "Software"), to deal in the Software without
+# restriction, including without limitation the rights to use,
+# copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following
+# conditions:
+#
+# This permission notice shall be included in all copies or
+# substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE AUTHOR(S) BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+# AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
+# OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+# DEALINGS IN THE SOFTWARE.
+
+# Print a very simple summary of piglit results file(s).
+# When multiple result files are specified, compare the results
+# of each test run to look for differences/regressions.
+#
+# Brian Paul
+# April 2013
+
+
+import argparse
+import os.path
+import sys
+
+sys.path.append(os.path.dirname(os.path.realpath(sys.argv[0])))
+import framework.summary as summary
+from framework.core import parse_listfile
+
+def main():
+    parser = argparse.ArgumentParser()
+
+    # Set the -d and -s options as exclusive, since it's silly to call for diff
+    # and then call for only summary
+    excGroup1 = parser.add_mutually_exclusive_group()
+    excGroup1.add_argument("-d", "--diff",
+                           action="store_true",
+                           help="Only display the differences between multiple "
+                                "result files")
+    excGroup1.add_argument("-s", "--summary",
+                           action="store_true",
+                           help="Only display the summary, not the individual "
+                                "test results")
+    parser.add_argument("-l", "--list",
+                        action="store",
+                        help="Use test results from a list file")
+    parser.add_argument("results",
+                        metavar="<Results Path(s)>",
+                        nargs="+",
+                        help="Space seperated paths to at least one results "
+                             "file")
+    args = parser.parse_args()
+
+    # Throw an error if -d/--diff is called, but only one results file is
+    # provided
+    if args.diff and len(args.results) < 2:
+        parser.error('-d/--diff cannot be specified unless two or more '
+                     'results files are specified')
+
+    # make list of results
+    if args.list:
+        args.results.extend(parse_listfile(args.list))
+
+    # Generate the output
+    output = summary.Summary(args.results)
+    output.generate_text(args.diff, args.summary)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/piglit/templates/empty_status.mako b/piglit/templates/empty_status.mako
new file mode 100644
index 0000000..8ee6fba
--- /dev/null
+++ b/piglit/templates/empty_status.mako
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+    <title>Result summary</title>
+    <link rel="stylesheet" href="status.css" type="text/css" />
+  </head>
+  <body>
+    <h1>Result summary</h1>
+    <p>Currently showing: ${page}</p>
+    <p>Show:
+      ## Index is a logical choice to put first, it will always be a link
+      ## and we don't want in preceeded by a |
+      <a href="index.html">index</a>
+      % for i in pages:
+        % if i == page:
+          | ${i}
+        % else:
+          | <a href="${i}.html">${i}</a>
+        % endif
+      % endfor
+    </p>
+    <h1>No ${page}</h1>
+  </body>
+</html>
diff --git a/piglit/templates/index.css b/piglit/templates/index.css
new file mode 100644
index 0000000..3389738
--- /dev/null
+++ b/piglit/templates/index.css
@@ -0,0 +1,78 @@
+
+table {
+	border: 0pt;
+	border-collapse: collapse;
+	padding-left: 1.75em;
+	padding-right: 1.75em;
+	min-width: 100%;
+	table-layout: fixed;
+}
+
+col:not(:first-child) {
+	width: 70pt;
+}
+
+tr {
+	padding: 4pt;
+}
+
+td {
+	padding: 4pt;
+}
+
+td:first-child {
+	padding: 0;
+}
+
+td:first-child > div {
+	padding: 4pt;
+}
+
+.title {
+	background-color: #c8c838;
+}
+
+.head {
+	background-color: #c8c838
+}
+
+td.skip, td.warn, td.fail, td.pass, td.trap, td.abort, td.crash, td.dmesg-warn, td.dmesg-fail, td.timeout {
+	text-align: right;
+}
+
+td.trap, td.abort, td.crash {
+	color: #ffffff;
+}
+
+td.trap a, td.abort a, td.crash a {
+	color: #ffffff;
+}
+
+tr:nth-child(odd)  > td > div.group { background-color: #ffff95 }
+tr:nth-child(even) > td > div.group { background-color: #e1e183 }
+
+tr:nth-child(odd)  td.pass  { background-color: #20ff20; }
+tr:nth-child(even) td.pass  { background-color: #15e015; }
+
+tr:nth-child(odd)  td.skip  { background-color: #b0b0b0; }
+tr:nth-child(even) td.skip  { background-color: #a0a0a0; }
+
+tr:nth-child(odd)  td.warn  { background-color: #ff9020; }
+tr:nth-child(even) td.warn  { background-color: #ef8010; }
+tr:nth-child(odd)  td.dmesg-warn  { background-color: #ff9020; }
+tr:nth-child(even) td.dmesg-warn  { background-color: #ef8010; }
+
+tr:nth-child(odd)  td.fail  { background-color: #ff2020; }
+tr:nth-child(even) td.fail  { background-color: #e00505; }
+tr:nth-child(odd)  td.dmesg-fail  { background-color: #ff2020; }
+tr:nth-child(even) td.dmesg-fail  { background-color: #e00505; }
+
+tr:nth-child(odd)  td.timeout  { background-color: #83bdf6; }
+tr:nth-child(even) td.timeout  { background-color: #4a9ef2; }
+
+tr:nth-child(odd)  td.trap  { background-color: #111111; }
+tr:nth-child(even) td.trap  { background-color: #000000; }
+tr:nth-child(odd)  td.abort { background-color: #111111; }
+tr:nth-child(even) td.abort { background-color: #000000; }
+tr:nth-child(odd)  td.crash { background-color: #111111; }
+tr:nth-child(even) td.crash { background-color: #000000; }
diff --git a/piglit/templates/index.mako b/piglit/templates/index.mako
new file mode 100644
index 0000000..1ca46d3
--- /dev/null
+++ b/piglit/templates/index.mako
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+    <title>Result summary</title>
+    <link rel="stylesheet" href="index.css" type="text/css" />
+  </head>
+  <body>
+    <h1>Result summary</h1>
+    <p>Currently showing: ${page}</p>
+    <p>Show:
+      % if page == 'all':
+        all
+      % else:
+        <a href="index.html">all</a>
+      % endif
+      % for i in pages:
+        % if i == page:
+          | ${i}
+        % else:
+          | <a href="${i}.html">${i}</a>
+        % endif
+      % endfor
+    </p>
+    <table>
+      <colgroup>
+        ## Name Column
+        <col />
+
+        ## Status columns
+        ## Create an additional column for each summary
+        % for _ in xrange(colnum):
+        <col />
+        % endfor
+      </colgroup>
+      % for line in results:
+        % if line['type'] == "newRow":
+        <tr>
+        % elif line['type'] == "endRow":
+        </tr>
+        % elif line['type'] == "groupRow":
+          <td>
+            <div class="${line['class']}" style="margin-left: ${line['indent']}em">
+              <b>${line['text']}</b>
+            </div>
+          </td>
+        % elif line['type'] == "testRow":
+          <td>
+            <div class="${line['class']}" style="margin-left: ${line['indent']}em">
+              ${line['text']}
+            </div>
+          </td>
+        % elif line['type'] == "groupResult":
+          <td class="${line['class']}">
+            <b>${line['text']}</b>
+          </td>
+        % elif line['type'] == "testResult":
+          <td class="${line['class']}">
+          ## If the result is in the excluded results page list from
+          ## argparse, just print the text, otherwise add the link
+          % if line['class'] not in exclude and line['href'] is not None:
+            <a href="${line['href']}">
+              ${line['text']}
+            </a>
+          % else:
+            ${line['text']}
+          % endif
+          </td>
+        % elif line['type'] == "subtestResult":
+          <td class="${line['class']}">
+            ${line['text']}
+          </td>
+        % elif line['type'] == "other":
+          ${line['text']}
+        % endif
+      % endfor
+    </table>
+  </body>
+</html>
diff --git a/piglit/templates/result.css b/piglit/templates/result.css
new file mode 100644
index 0000000..19bfedc
--- /dev/null
+++ b/piglit/templates/result.css
@@ -0,0 +1,37 @@
+
+td {
+	padding: 4pt;
+}
+
+th {
+	padding: 4pt;
+}
+
+table {
+	border: 0pt;
+	border-collapse: collapse;
+}
+
+th {
+	background-color: #c8c838
+}
+
+/* Second column (details) */
+tr:nth-child(even) > td {
+	background-color: #ffff95
+}
+
+tr:nth-child(odd) > td {
+	background-color: #e1e183
+}
+
+/* First column (labels) */
+tr:nth-child(even) > td:first-child {
+	vertical-align: top;
+	background-color: #ffff85;
+}
+
+tr:nth-child(odd) > td:first-child {
+	vertical-align: top;
+	background-color: #d1d173;
+}
diff --git a/piglit/templates/test_result.mako b/piglit/templates/test_result.mako
new file mode 100644
index 0000000..490c009
--- /dev/null
+++ b/piglit/templates/test_result.mako
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//END"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+    <title>${testname} - Details</title>
+    <link rel="stylesheet" href="${css}" type="text/css" />
+  </head>
+  <body>
+    <h1>Results for ${testname}</h1>
+    <h2>Overview</h2>
+    <div>
+      <p><b>Result:</b> ${status}</p>
+    </div>
+    <p><a href="${index}">Back to summary</a></p>
+    <h2>Details</h2>
+    <table>
+      <tr>
+        <th>Detail</th>
+        <th>Value</th>
+      </tr>
+      <tr>
+        <td>Returncode</td>
+        <td>${returncode}</td>
+      </tr>
+      <tr>
+        <td>Time</td>
+        <td>${time}</b>
+      </tr>
+      <tr>
+        <td>Info</td>
+        <td>
+          <pre>${info | h}</pre>
+        </td>
+      </tr>
+      % if env:
+      <tr>
+        <td>Environment</td>
+        <td>
+          <pre>${env | h}</pre>
+        </td>
+      </tr>
+      % endif
+      <tr>
+        <td>Command</td>
+        <td>
+          </pre>${command}</pre>
+        </td>
+      </tr>
+      <tr>
+        <td>Traceback</td>
+        <td>
+          <pre>${traceback | h}</pre>
+        </td>
+      </tr>
+      <tr>
+        <td>dmesg</td>
+        <td>
+          <pre>${dmesg | h}</pre>
+        </td>
+      </tr>
+    </table>
+    <p><a href="${index}">Back to summary</a></p>
+  </body>
+</html>
diff --git a/piglit/templates/testrun_info.mako b/piglit/templates/testrun_info.mako
new file mode 100644
index 0000000..e6e00b3
--- /dev/null
+++ b/piglit/templates/testrun_info.mako
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//END"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+    <title>${name} - System info</title>
+    <link rel="stylesheet" href="../result.css" type="text/css" />
+  </head>
+  <body>
+    <h1>System info for ${name}</h1>
+    <p>
+      <a href="../index.html">Back to summary</a>
+    </p>
+    <table>
+      <tr>
+        <th>Detail</th>
+        <th>Value</th>
+      </tr>
+      <tr>
+        <td>time_elapsed</td>
+        <td>${time}</td>
+      </tr>
+      <tr>
+        <td>name</td>
+        <td>${name}</td>
+      </tr>
+      <tr>
+        <td>options</td>
+        <td>${options}</td>
+      </tr>
+      <tr>
+        <td>lspci</td>
+        <td>
+          <pre>${lspci}</pre>
+        </td>
+      </tr>
+      <tr>
+        <td>glxinfo</td>
+        <td>
+          <pre>${glxinfo}</pre>
+        </td>
+      </tr>
+    </table>
+    <p>
+      <a href="../index.html">Back to summary</a>
+    </p>
+  </body>
+</html>
-- 
1.8.3.1




More information about the Intel-gfx mailing list