output.py (6384B)
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 5 import json 6 import threading 7 from collections import defaultdict 8 9 from mozlog.formatters import TbplFormatter 10 from mozrunner.utils import get_stack_fixer_function 11 12 13 class 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" % ( 61 status_msg, 62 test, 63 data.get("subtest", "unknown test"), 64 ) 65 if data.get("message"): 66 output_text += " | %s" % data["message"] 67 68 if "reftest_screenshots" in extra: 69 screenshots = extra["reftest_screenshots"] 70 image_1 = screenshots[0]["screenshot"] 71 72 if len(screenshots) == 3: 73 image_2 = screenshots[2]["screenshot"] 74 output_text += ( 75 "\nREFTEST IMAGE 1 (TEST): data:image/png;base64,%s\n" 76 "REFTEST IMAGE 2 (REFERENCE): data:image/png;base64,%s" 77 ) % (image_1, image_2) 78 elif len(screenshots) == 1: 79 output_text += "\nREFTEST IMAGE: data:image/png;base64,%s" % image_1 80 81 return output_text + "\n" 82 83 def test_end(self, data): 84 status = data["status"] 85 test = data["test"] 86 87 output_text = "" 88 if status != "OK": 89 status_msg = self._format_status(data) 90 output_text = "%s | %s | %s" % (status_msg, test, data.get("message", "")) 91 92 if output_text: 93 output_text += "\nREFTEST " 94 output_text += "TEST-END | %s" % test 95 return "%s\n" % output_text 96 97 def process_output(self, data): 98 return "%s\n" % data["data"] 99 100 def suite_end(self, data): 101 lines = [] 102 summary = data["extra"]["results"] 103 summary["success"] = summary["Pass"] + summary["LoadOnly"] 104 lines.append( 105 "Successful: %(success)s (%(Pass)s pass, %(LoadOnly)s load only)" % summary 106 ) 107 summary["unexpected"] = ( 108 summary["Exception"] 109 + summary["FailedLoad"] 110 + summary["UnexpectedFail"] 111 + summary["UnexpectedPass"] 112 + summary["AssertionUnexpected"] 113 + summary["AssertionUnexpectedFixed"] 114 ) 115 lines.append( 116 ( 117 "Unexpected: %(unexpected)s (%(UnexpectedFail)s unexpected fail, " 118 "%(UnexpectedPass)s unexpected pass, " 119 "%(AssertionUnexpected)s unexpected asserts, " 120 "%(FailedLoad)s failed load, " 121 "%(Exception)s exception)" 122 ) 123 % summary 124 ) 125 summary["known"] = ( 126 summary["KnownFail"] 127 + summary["AssertionKnown"] 128 + summary["Random"] 129 + summary["Skip"] 130 + summary["Slow"] 131 ) 132 lines.append( 133 ( 134 "Known problems: %(known)s (" 135 + "%(KnownFail)s known fail, " 136 + "%(AssertionKnown)s known asserts, " 137 + "%(Random)s random, " 138 + "%(Skip)s skipped, " 139 + "%(Slow)s slow)" 140 ) 141 % summary 142 ) 143 lines = ["REFTEST INFO | %s" % s for s in lines] 144 lines.append("REFTEST SUITE-END | Shutdown") 145 return "INFO | Result summary:\n{}\n".format("\n".join(lines)) 146 147 148 class OutputHandler: 149 """Process the output of a process during a test run and translate 150 raw data logged from reftest.js to an appropriate structured log action, 151 where applicable. 152 """ 153 154 def __init__(self, log, utilityPath, symbolsPath=None): 155 self.stack_fixer_function = get_stack_fixer_function(utilityPath, symbolsPath) 156 self.log = log 157 self.proc_name = None 158 self.results = defaultdict(int) 159 160 def __call__(self, line): 161 # need to return processed messages to appease remoteautomation.py 162 if not line.strip(): 163 return [] 164 line = line.decode("utf-8", errors="replace") 165 166 try: 167 data = json.loads(line) 168 except ValueError: 169 self.verbatim(line) 170 return [line] 171 172 if isinstance(data, dict) and "action" in data: 173 if data["action"] == "results": 174 for k, v in data["results"].items(): 175 self.results[k] += v 176 else: 177 self.log.log_raw(data) 178 else: 179 self.verbatim(json.dumps(data)) 180 181 return [data] 182 183 def write(self, data): 184 return self.__call__(data) 185 186 def verbatim(self, line): 187 if self.stack_fixer_function: 188 line = self.stack_fixer_function(line) 189 name = self.proc_name or threading.current_thread().name 190 self.log.process_output(name, line)