1# This Source Code Form is subject to the terms of the Mozilla Public
2# License, v. 2.0. If a copy of the MPL was not distributed with this
3# file, You can obtain one at http://mozilla.org/MPL/2.0/.
4
5import json
6import threading
7from collections import defaultdict
8
9from mozlog.formatters import TbplFormatter
10from mozrunner.utils import get_stack_fixer_function
11
12
13class ReftestFormatter(TbplFormatter):
14    """
15    Formatter designed to preserve the legacy "tbpl" format in reftest.
16
17    This is needed for both the reftest-analyzer and mozharness log parsing.
18    We can change this format when both reftest-analyzer and mozharness have
19    been changed to read structured logs.
20    """
21
22    def __call__(self, data):
23        if 'component' in data and data['component'] == 'mozleak':
24            # Output from mozleak requires that no prefix be added
25            # so that mozharness will pick up these failures.
26            return "%s\n" % data['message']
27
28        formatted = TbplFormatter.__call__(self, data)
29
30        if formatted is None:
31            return
32        if data['action'] == 'process_output':
33            return formatted
34        return 'REFTEST %s' % formatted
35
36    def log(self, data):
37        prefix = "%s |" % data['level'].upper()
38        return "%s %s\n" % (prefix, data['message'])
39
40    def _format_status(self, data):
41        extra = data.get('extra', {})
42        status = data['status']
43
44        status_msg = "TEST-"
45        if 'expected' in data:
46            status_msg += "UNEXPECTED-%s" % status
47        else:
48            if status not in ("PASS", "SKIP"):
49                status_msg += "KNOWN-"
50            status_msg += status
51            if extra.get('status_msg') == 'Random':
52                status_msg += "(EXPECTED RANDOM)"
53        return status_msg
54
55    def test_status(self, data):
56        extra = data.get('extra', {})
57        test = data['test']
58
59        status_msg = self._format_status(data)
60        output_text = "%s | %s | %s" % (status_msg, test, data.get("subtest", "unknown test"))
61        if data.get('message'):
62            output_text += " | %s" % data['message']
63
64        if "reftest_screenshots" in extra:
65            screenshots = extra["reftest_screenshots"]
66            image_1 = screenshots[0]["screenshot"]
67
68            if len(screenshots) == 3:
69                image_2 = screenshots[2]["screenshot"]
70                output_text += ("\nREFTEST   IMAGE 1 (TEST): data:image/png;base64,%s\n"
71                                "REFTEST   IMAGE 2 (REFERENCE): data:image/png;base64,%s") % (
72                                image_1, image_2)
73            elif len(screenshots) == 1:
74                output_text += "\nREFTEST   IMAGE: data:image/png;base64,%s" % image_1
75
76        return output_text + "\n"
77
78    def test_end(self, data):
79        status = data['status']
80        test = data['test']
81
82        output_text = ""
83        if status != "OK":
84            status_msg = self._format_status(data)
85            output_text = "%s | %s | %s" % (status_msg, test, data.get("message", ""))
86
87        if output_text:
88            output_text += "\nREFTEST "
89        output_text += "TEST-END | %s" % test
90        return "%s\n" % output_text
91
92    def process_output(self, data):
93        return "%s\n" % data["data"]
94
95    def suite_end(self, data):
96        lines = []
97        summary = data['extra']['results']
98        summary['success'] = summary['Pass'] + summary['LoadOnly']
99        lines.append("Successful: %(success)s (%(Pass)s pass, %(LoadOnly)s load only)" %
100                     summary)
101        summary['unexpected'] = (summary['Exception'] + summary['FailedLoad'] +
102                                 summary['UnexpectedFail'] + summary['UnexpectedPass'] +
103                                 summary['AssertionUnexpected'] +
104                                 summary['AssertionUnexpectedFixed'])
105        lines.append(("Unexpected: %(unexpected)s (%(UnexpectedFail)s unexpected fail, "
106                      "%(UnexpectedPass)s unexpected pass, "
107                      "%(AssertionUnexpected)s unexpected asserts, "
108                      "%(FailedLoad)s failed load, "
109                      "%(Exception)s exception)") % summary)
110        summary['known'] = (summary['KnownFail'] + summary['AssertionKnown'] +
111                            summary['Random'] + summary['Skip'] + summary['Slow'])
112        lines.append(("Known problems: %(known)s (" +
113                      "%(KnownFail)s known fail, " +
114                      "%(AssertionKnown)s known asserts, " +
115                      "%(Random)s random, " +
116                      "%(Skip)s skipped, " +
117                      "%(Slow)s slow)") % summary)
118        lines = ["REFTEST INFO | %s" % s for s in lines]
119        lines.append("REFTEST SUITE-END | Shutdown")
120        return "INFO | Result summary:\n{}\n".format('\n'.join(lines))
121
122
123class OutputHandler(object):
124    """Process the output of a process during a test run and translate
125    raw data logged from reftest.js to an appropriate structured log action,
126    where applicable.
127    """
128
129    def __init__(self, log, utilityPath, symbolsPath=None):
130        self.stack_fixer_function = get_stack_fixer_function(utilityPath, symbolsPath)
131        self.log = log
132        self.proc_name = None
133        self.results = defaultdict(int)
134
135    def __call__(self, line):
136        # need to return processed messages to appease remoteautomation.py
137        if not line.strip():
138            return []
139        line = line.decode('utf-8', errors='replace')
140
141        try:
142            data = json.loads(line)
143        except ValueError:
144            self.verbatim(line)
145            return [line]
146
147        if isinstance(data, dict) and 'action' in data:
148            if data['action'] == 'results':
149                for k, v in data['results'].items():
150                    self.results[k] += v
151            else:
152                self.log.log_raw(data)
153        else:
154            self.verbatim(json.dumps(data))
155
156        return [data]
157
158    def verbatim(self, line):
159        if self.stack_fixer_function:
160            line = self.stack_fixer_function(line)
161        name = self.proc_name or threading.current_thread().name
162        self.log.process_output(name, line)
163