diff --git a/python/servo/testing_commands.py b/python/servo/testing_commands.py index fff3c9b5526..8ee013de984 100644 --- a/python/servo/testing_commands.py +++ b/python/servo/testing_commands.py @@ -113,6 +113,22 @@ def format_with_rustfmt(check_only: bool = True) -> int: ) +def pretty_print_json_decode_error(error: json.decoder.JSONDecodeError) -> None: + print(f"json decode error: {error}") + # Print section around that character that raised the json decode error. + snippet_radius = 25 + start, stop = max(0, error.pos - snippet_radius), error.pos + snippet_radius + snippet = error.doc[start:stop] + print("```") + prefix = "... " if start != 0 else "" + suffix = " ..." if stop < len(error.doc) else "" + print(prefix, snippet, suffix, sep="") + # If the snippet is multiline, this won't work as expected, but it's a best effort. + right_justification = snippet_radius + 1 + len(suffix) + print("^ Offending character".rjust(right_justification)) + print("```") + + @CommandProvider class MachCommands(CommandBase): DEFAULT_RENDER_MODE = "cpu" @@ -617,19 +633,25 @@ class MachCommands(CommandBase): json.dump(output, f, indent=4) def speedometer_runner(self, binary: str, bmf_output: str | None) -> None: - speedometer = json.loads( - subprocess.check_output( - [ - binary, - "https://servospeedometer.netlify.app?headless=1", - "--pref", - "dom_allow_scripts_to_close_windows", - "--window-size=1100x900", - "--headless", - ], - timeout=120, - ).decode() + output = subprocess.check_output( + [ + binary, + "https://servospeedometer.netlify.app?headless=1", + "--pref", + "dom_allow_scripts_to_close_windows", + "--window-size=1100x900", + "--headless", + ], + encoding="utf-8", + timeout=120, ) + try: + speedometer = json.loads(output) + except json.decoder.JSONDecodeError as e: + pretty_print_json_decode_error(e) + print("Error: Failed to parse speedometer results") + print("This can happen if other log messages are printed while running servo...") + exit(1) print(f"Score: {speedometer['Score']['mean']} ± {speedometer['Score']['delta']}") @@ -646,13 +668,13 @@ class MachCommands(CommandBase): hdc_path = path.join(ohos_sdk_native, "../", "toolchains", "hdc") def read_log_file(hdc_path: str) -> str: - subprocess.call([hdc_path, "file", "recv", log_path]) + subprocess.call([hdc_path, "file", "recv", log_path, "servo.log"]) file = "" try: - file = open("servo.log") + with open("servo.log") as file: + return file.read() except OSError: return "" - return file.read() subprocess.call([hdc_path, "shell", "aa", "force-stop", "org.servo.servo"]) @@ -681,9 +703,6 @@ class MachCommands(CommandBase): # A current (2025-06-23) run took 3m 49s = 229s. We keep a safety margin # but we will exit earlier if we see "{" - # Currently ohos has a bug where the event loop gets stuck. We produce a - # touch event every minute to prevent this - # See https://github.com/servo/servo/issues/37727 whole_file: str = "" for i in range(10): sleep(30) @@ -694,9 +713,21 @@ class MachCommands(CommandBase): sleep(2) whole_file = read_log_file(hdc_path) break + else: + print("Error failed to find console logs in log file") + print(f"log-file contents: `{whole_file}`") + exit(1) start_index: int = whole_file.index("[INFO script::dom::console]") + len("[INFO script::dom::console]") + 1 json_string = whole_file[start_index:] - speedometer = json.loads(json_string) + try: + speedometer = json.loads(json_string) + except json.decoder.JSONDecodeError as e: + print(f"Error: Failed to convert log output to JSON: {e}") + pretty_print_json_decode_error(e) + print("Error: Failed to parse speedometer results") + print("This can happen if other log messages are printed while running servo...") + exit(1) + print(f"Score: {speedometer['Score']['mean']} ± {speedometer['Score']['delta']}") if bmf_output: self.speedometer_to_bmf(speedometer, bmf_output, profile)