File: summary.py

package info (click to toggle)
piglit 0~git20150829-59d7066-1%2Bdeb9u1
  • links: PTS, VCS
  • area: main
  • in suites: stretch
  • size: 39,280 kB
  • sloc: ansic: 191,513; xml: 43,580; cpp: 29,351; python: 18,307; lisp: 8,347; sh: 507; makefile: 17; pascal: 5
file content (620 lines) | stat: -rw-r--r-- 25,650 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
# 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.

from __future__ import print_function, absolute_import
import os
import os.path as path
import itertools
import shutil
import collections
import tempfile
import datetime
import re
import getpass
import sys
import posixpath
import errno

from mako.template import Template

# a local variable status exists, prevent accidental overloading by renaming
# the module
import framework.status as so
from framework import grouptools, backends, exceptions

__all__ = [
    'Summary',
]


def escape_filename(key):
    """Avoid reserved characters in filenames."""
    return re.sub(r'[<>:"|?*#]', '_', key)


def escape_pathname(key):
    """ Remove / and \\ from names """
    return re.sub(r'[/\\]', '_', key)


def normalize_href(href):
    """Force backward slashes in URLs."""
    return href.replace('\\', '/')


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:
            href = normalize_href(os.path.join(
                escape_pathname(each.name), "index.html"))
            self.append({'type': 'other',
                         'text': '<td class="head"><b>%(name)s</b><br />'
                                 '(<a href="%(href)s">info</a>)'
                                 '</td>' % {'name': each.name,
                                            'href': href}})
        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(grouptools.SEPARATOR)
            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][grouptools.join(*currentDir)],
                        summary.status[each.name][grouptools.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[grouptools.groupname(key)]['subtest']:
                        href = grouptools.groupname(key)
                except KeyError:
                    href = key

                href = escape_filename(href)

                try:
                    self._testResult(escape_pathname(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 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 text == so.NOTRUN:
            css = 'skip'
            href = None
        else:
            css = text
            # Use posixpath for this URL because it maintains portability
            # between windows and *nix.
            href = posixpath.join(group, href + ".html")
            href = normalize_href(href)

        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-{}".format(getpass.getuser()),
                         'version-{}'.format(sys.version.split()[0]),
                         "html-summary")
    TEMPLATE_DIR = path.abspath(
        path.join(path.dirname(__file__), '..', '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 = [backends.load(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(),
                      'enabled': set(), 'disabled': set(), 'incomplete': 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 the new status is worse update it, or if the new status is
            # SKIP (which is equivalent to notrun) and the current is NOTRUN
            # update it
            if (status[test] < result or
                    (result == so.SKIP and status[test] == so.NOTRUN)):
                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(lambda: 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():
                # if the first character of key is a / then our while loop will
                # become an infinite loop. Beyond that / should never be the
                # leading character, if it is then there is a bug in one of the
                # test profiles.
                assert key[0] != '/'

                # 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.
                if 'subtest' in value:
                    for (subt, subv) in value['subtest'].iteritems():
                        subt = grouptools.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 = grouptools.groupname(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)
                else:
                    # 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 = grouptools.groupname(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
            for key, value in temp_results.iteritems():
                results.tests[key] = value

        # 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 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)

            if so.INCOMPLETE in status:
                self.tests['incomplete'].add(test)

            # find fixes, regressions, and changes
            for i in xrange(len(status) - 1):
                first = status[i]
                last = status[i + 1]
                if first in [so.SKIP, so.NOTRUN] and \
                        last not in [so.SKIP, so.NOTRUN]:
                    self.tests['enabled'].add(test)
                    self.tests['changes'].add(test)
                elif last in [so.SKIP, so.NOTRUN] and \
                        first not in [so.SKIP, so.NOTRUN]:
                    self.tests['disabled'].add(test)
                    self.tests['changes'].add(test)
                elif first < last:
                    self.tests['regressions'].add(test)
                    self.tests['changes'].add(test)
                elif first > last:
                    self.tests['fixes'].add(test)
                    self.tests['changes'].add(test)

    def __find_totals(self, results):
        """
        Private: Find the total number of pass, fail, crash, skip, and warn in
        the specified results.
        """
        self.totals = {'pass': 0, 'fail': 0, 'crash': 0, 'skip': 0,
                       'timeout': 0, 'warn': 0, 'dmesg-warn': 0,
                       'dmesg-fail': 0, 'incomplete': 0,}

        for test in results.tests.itervalues():
            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",
                             encoding_errors='replace',
                             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",
                            encoding_errors='replace',
                            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:
            name = escape_pathname(each.name)
            try:
                os.mkdir(path.join(destination, name))
            except OSError as e:
                if e.errno == errno.EEXIST:
                    raise exceptions.PiglitFatalError(
                        'Two or more of your results have the same "name" '
                        'attribute. Try changing one or more of the "name" '
                        'values in your json files.\n'
                        'Duplicate value: {}'.format(name))
                else:
                    raise e

            if each.time_elapsed is not None:
                time = datetime.timedelta(0, each.time_elapsed)
            else:
                time = None

            self.__find_totals(each)

            with open(path.join(destination, name, "index.html"), 'w') as out:
                out.write(testindex.render(
                    name=each.name,
                    totals=self.totals,
                    time=time,
                    options=each.options,
                    uname=each.uname,
                    glxinfo=each.glxinfo,
                    lspci=each.lspci))

            # Then build the individual test results
            for key, value in each.tests.iteritems():
                html_path = path.join(destination, name,
                                      escape_filename(key + ".html"))
                temp_path = path.dirname(html_path)

                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)

                    if value.get('time') is not None:
                        value['time'] = datetime.timedelta(0, value['time'])

                    with open(html_path, 'w') as out:
                        out.write(testfile.render(
                            testname=key,
                            value=value,
                            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",
                         encoding_errors='replace',
                         module_directory=self.TEMP_DIR)

        empty_status = Template(filename=path.join(self.TEMPLATE_DIR,
                                                   "empty_status.mako"),
                                output_encoding="utf-8",
                                encoding_errors='replace',
                                module_directory=self.TEMP_DIR)

        pages = frozenset(['changes', 'problems', 'skipped', 'fixes',
                           'regressions', 'enabled', 'disabled'])

        # 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, mode):
        """ Write summary information to the console """
        assert mode in ['summary', 'diff', 'incomplete', 'all'], mode
        self.__find_totals(self.results[-1])

        def printer(list_):
            """Takes a list of test names to print and prints the name and
            result.

            """
            for test in list_:
                print("{test}: {statuses}".format(
                    test='/'.join(test.split(grouptools.SEPARATOR)),
                    statuses=' '.join(str(i.tests.get(test, {'result': so.SKIP})
                                          ['result']) for i in self.results)))

        def print_summary():
            """print a summary."""
            print("summary:\n"
                  "       pass: {pass}\n"
                  "       fail: {fail}\n"
                  "      crash: {crash}\n"
                  "       skip: {skip}\n"
                  "    timeout: {timeout}\n"
                  "       warn: {warn}\n"
                  " incomplete: {incomplete}\n"
                  " dmesg-warn: {dmesg-warn}\n"
                  " dmesg-fail: {dmesg-fail}".format(**self.totals))
            if self.tests['changes']:
                print("    changes: {changes}\n"
                      "      fixes: {fixes}\n"
                      "regressions: {regressions}".format(
                          **{k: len(v) for k, v in self.tests.iteritems()}))

            print("      total: {}".format(sum(self.totals.itervalues())))

        # Print the name of the test and the status from each test run
        if mode == 'all':
            printer(self.tests['all'])
            print_summary()
        elif mode == 'diff':
            printer(self.tests['changes'])
            print_summary()
        elif mode == 'incomplete':
            printer(self.tests['incomplete'])
        elif mode == 'summary':
            print_summary()