diff options
Diffstat (limited to 'framework/summary.py')
-rw-r--r-- | framework/summary.py | 520 |
1 files changed, 520 insertions, 0 deletions
diff --git a/framework/summary.py b/framework/summary.py index 091329e61..10f114b56 100644 --- a/framework/summary.py +++ b/framework/summary.py @@ -20,8 +20,21 @@ # OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. +import os +import os.path as path +import string +from itertools import izip_longest +from shutil import copy +from json import loads +from mako.template import Template + import core +__all__ = [ + 'Summary', + 'NewSummary', +] + class PassVector: """ @@ -248,3 +261,510 @@ testruns is an array of TestrunResult instances Returns an array of all child TestSummary instances. """ return self.root.allTests() + +## New Summary + +def sanitizePath(path): + """ + Helper function to remove illegal characters from the names + """ + return filter(lambda s: s.isalnum() or s == '_', path.repalce('/', '__')) \ + + '.html' + + +class Result(core.TestrunResult): + """ + Object that opens, reads, and stores the data in a resultfile. + """ + def __init__(self, resultfile): + # Run the init from TestrunResult + core.TestrunResult.__init__(self) + + # Load the json file, or if that fails assume that the locations given + # is a folder containing a json file + try: + with open(resultfile, 'r') as file: + result = self.parseFile(file) + except IOError: + with open(path.join(resultfile, 'main'), 'r') as file: + result = self.parseFile(file) + + +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 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(getattr(summary, 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 result in skip + currentDir.append(localGroup) + for each in summary.results: + # Decide which fields need to be updated + try: + self._groupResult( + summary.fractions[each.name][path.join(*currentDir)], + summary.status[each.name][path.join(*currentDir)]) + except KeyError: + self._groupResult((0, 0), 'skip') + + # 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 skip + for each in summary.results: + try: + self._testResult(each.name, key, each.tests[key]['result']) + except KeyError: + self.append({'type': 'other', + 'text': '<td class="skip">skip</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. + """ + + 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. + """ + self.append({'type': 'testResult', + 'class': text, + 'href': path.join(sanitizePath(group), + sanitizePath(href + ".html")), + 'text': text}) + + +class NewSummary: + """ + 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(). + """ + + 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 + """ + + def buildDictionary(summary): + # Build a dictionary from test name to pass count/total count, i.e. + # counts['spec/glsl/foo'] == (456, 800) + counts = {} + + if not summary.tests: + return {} + + lastGroup = '' + + # Build a dictionary of group stati, passing groupname = status. + # This is the "worst" status of the group in descending order: crash, + # skip, fail, warn, pass + status = {} + + # currentStack is a stack containing numerical values that that relate + # to a status output, 5 for crash, 4 for skip, 3 for fail, 2 for warn, + # 1 for pass + currentStatus = [] + + # Stack contains tuples like: (pass count, total count) + stack = [] + + def openGroup(name): + stack.append((0, 0)) + + # Since skip is the "lowest" status for HTML generation, if there + # is another status it will replace skip + currentStatus.append('skip') + + def closeGroup(group_name): + # We're done with this group, record the number of pass/total in + # the dictionary. + (nr_pass, nr_total) = stack.pop() + counts[group_name] = (nr_pass, nr_total) + + # Also add our pass/total count to the parent group's counts + # (which are still being computed) + (parent_pass, parent_total) = stack[-1] + stack[-1] = (parent_pass + nr_pass, parent_total + nr_total) + + # Add the status back to the group hierarchy + if status_to_number(currentStatus[-2]) < \ + status_to_number(currentStatus[-1]): + currentStatus[-2] = currentStatus[-1] + status[group_name] = currentStatus.pop() + + def status_to_number(status): + """ + like status_to_number in the constructor, this function converts + statuses into numbers so they can be comapared + logically/mathematically. The only difference between this and + init::status_to_number is the values assigned. The reason for this + is that here we are looking for the 'worst' status, while in + init::status_to_number we are looking for regressions in status. + """ + if status == 'skip': + return 1 + elif status == 'pass': + return 2 + elif status == 'warn': + return 3 + elif status == 'fail': + return 4 + elif status == 'crash': + return 5 + + openGroup('fake') + openGroup('all') + + # fulltest is a full test name, + # i.e. tests/shaders/glsl-algebraic-add-zero + for fulltest in sorted(summary.tests): + group, test = path.split(fulltest) # or fulltest.rpartition('/') + + if group != lastGroup: + # We're in a different group now. Close the old ones + # and open the new ones. + for x in path.relpath(group, lastGroup).split('/'): + if x != '..': + openGroup(x) + else: + closeGroup(lastGroup) + lastGroup = path.normpath(path.join(lastGroup, "..")) + + lastGroup = group + + # Add the current test + (pass_so_far, total_so_far) = stack[-1] + if summary.tests[fulltest]['result'] == 'pass': + pass_so_far += 1 + if summary.tests[fulltest]['result'] != 'skip': + total_so_far += 1 + stack[-1] = (pass_so_far, total_so_far) + + # compare the status + if status_to_number(summary.tests[fulltest]['result']) > \ + status_to_number(currentStatus[-1]): + currentStatus[-1] = summary.tests[fulltest]['result'] + + # Work back up the stack closing groups as we go until we reach the + # top, where we need to manually close this as "all" + while len(stack) > 2: + closeGroup(lastGroup) + lastGroup = path.dirname(lastGroup) + closeGroup("all") + + assert(len(stack) == 1) + assert(len(currentStatus) == 1) + + return counts, status + + def status_to_number(status): + """ + small helper function to convert named statuses into number, since + number can more easily be compared using logical/mathematical + operators. The use of this is to look for regressions in status. + """ + if status == 'pass': + return 1 + elif status == 'warn': + return 2 + elif status == 'fail': + return 3 + elif status == 'skip': + return 4 + elif status == 'crash': + return 5 + + # Create a Result object for each piglit result and append it to the + # results list + self.results = [Result(i) for i in resultfiles] + + self.status = {} + self.fractions = {} + self.alltests = [] + self.changes = [] + self.problems = [] + self.skipped = [] + self.regressions = [] + self.fixes = [] + + for each in self.results: + # Build a dict of the status output of all of the tests, with the + # name of the test run as the key for that result, this will be + # used to write pull the statuses later + fraction, status = buildDictionary(each) + self.fractions.update({each.name: fraction}) + self.status.update({each.name: status}) + + # Create a list with all the test names in it + self.alltests = list(set(self.alltests) | set(each.tests)) + + # Create lists similar to self.alltests, but for the other root pages, + # (regressions, skips, ect) + for test in self.alltests: + status = [] + for each in self.results: + try: + status.append(status_to_number(each.tests[test]['result'])) + except KeyError: + status.append(status_to_number("skip")) + + # Check and append self.changes + # A set cannot contain duplicate entries, so creating a set out + # the list will reduce it's length to 1 if all entries are the + # same, meaning it is not a change + if len(set(status)) > 1: + self.changes.append(test) + + # Problems + # If the result contains a value other than 1 (pass) or 4 (skip) + # it is a problem. Skips are not problems becasuse they have + # Their own page. + if [i for e in [2, 3, 5] for i in status if e is i]: + self.problems.append(test) + + # skipped + if 4 in status: + self.skipped.append(test) + + # fixes and regressions + # check each member against the next member. If the second member + # has a greater value it is a regression. + # Fixes on the other hand are a former non 1 value, followed by + # a value of 1 + for i in xrange(len(status) - 1): + if status[i] < status[i + 1]: + self.regressions.append(test) + if status[i] > 1 and status[i + 1] == 1: + self.fixes.append(test) + + def generateHTML(self, destination): + """ + 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 + copy("templates/index.css", path.join(destination, "index.css")) + copy("templates/result.css", path.join(destination, "result.css")) + + # Create the mako object for creating the test/index.html file + testindex = Template(filename="templates/test_index.mako", + output_encoding="utf-8", + module_directory=".makotmp") + + # Create the mako object for the individual result files + testfile = Template(filename="templates/test_result.mako", + output_encoding="utf-8", + module_directory=".makotmp") + + # Iterate across the tests creating the various test specific files + for each in self.results: + os.mkdir(path.join(destination, each.name)) + + file = open(path.join(destination, each.name, "index.html"), 'w') + file.write(testindex.render(name=each.name, + time=each.time_elapsed, + options=each.options, + glxinfo=each.glxinfo, + lspci=each.lspci)) + file.close() + + # Then build the individual test results + for key, value in each.tests.iteritems(): + file = open(path.join(destination, + each.name, + sanitizePath(key + ".html")), 'w') + file.write(testfile.render(testname=key, + status=value.get('result', 'None'), + returncode=value.get('returncode', + 'None'), + time=value.get('time', 'None'), + info=value.get('info', 'None'), + command=value.get('command', + 'None'))) + file.close() + + # Finally build the root html files: index, regressions, etc + index = Template(filename="templates/index.mako", + output_encoding="utf-8", + module_directory=".makotmp") + + # A list of pages to be generated + 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. + file = open(path.join(destination, "index.html"), 'w') + file.write(index.render(results=HTMLIndex(self, 'alltests'), + page='all', + colnum=len(self.results))) + file.close() + + # Generate the rest of the pages + for page in pages: + file = open(path.join(destination, page + '.html'), 'w') + file.write(index.render(results=HTMLIndex(self, page), + page=page, + colnum=len(self.results), + exclude=exclude)) + file.close() |