detect_crash.py (3670B)
1 import json 2 import tempfile 3 import time 4 from copy import deepcopy 5 from pathlib import Path 6 7 import pytest 8 from webdriver import error 9 10 11 def test_content_process(configuration, geckodriver): 12 def trigger_crash(driver): 13 # The crash is delayed and happens after this command finished. 14 driver.session.url = "about:crashcontent" 15 16 # Bug 1943038: geckodriver fails to detect minidump files for content 17 # crashes when the next command is sent immediately. 18 time.sleep(1) 19 20 # Send another command that should fail 21 with pytest.raises(error.UnknownErrorException): 22 driver.session.url 23 24 run_crash_test(configuration, geckodriver, crash_callback=trigger_crash) 25 26 27 def test_parent_process(configuration, geckodriver): 28 def trigger_crash(driver): 29 with pytest.raises(error.UnknownErrorException): 30 driver.session.url = "about:crashparent" 31 32 run_crash_test(configuration, geckodriver, crash_callback=trigger_crash) 33 34 35 def run_crash_test(configuration, geckodriver, crash_callback): 36 config = deepcopy(configuration) 37 config["capabilities"]["webSocketUrl"] = True 38 39 with tempfile.TemporaryDirectory() as tmpdirname: 40 # Use a custom temporary minidump save path to only see 41 # the minidump files related to this test 42 driver = geckodriver( 43 config=config, extra_env={"MINIDUMP_SAVE_PATH": tmpdirname} 44 ) 45 46 driver.new_session() 47 profile_minidump_path = ( 48 Path(driver.session.capabilities["moz:profile"]) / "minidumps" 49 ) 50 51 crash_callback(driver) 52 53 tmp_minidump_dir = Path(tmpdirname) 54 file_map = verify_minidump_files(tmp_minidump_dir) 55 56 # Check that for both Marionette and Remote Agent the annotations are present 57 extra_data = read_extra_file(file_map[".extra"]) 58 assert extra_data.get("Marionette") == "1", ( 59 "Marionette entry is missing or invalid" 60 ) 61 assert extra_data.get("RemoteAgent") == "1", ( 62 "RemoteAgent entry is missing or invalid" 63 ) 64 65 # Remove original minidump files from the profile directory 66 remove_files(profile_minidump_path, file_map.values()) 67 68 69 def read_extra_file(path): 70 """Read and parse the minidump's .extra file.""" 71 try: 72 with path.open("rb") as file: 73 data = file.read() 74 75 # Try to decode first and replace invalid utf-8 characters. 76 decoded = data.decode("utf-8", errors="replace") 77 78 return json.loads(decoded) 79 except json.JSONDecodeError as e: 80 raise ValueError(f"Invalid JSON in {path}: {e}") 81 82 83 def remove_files(directory, files): 84 """Safely remove a list of files.""" 85 for file in files: 86 file_path = Path(directory) / file.name 87 try: 88 file_path.unlink() 89 except FileNotFoundError: 90 print(f"File not found: {file_path}") 91 except PermissionError as e: 92 raise ValueError(f"Permission error removing {file_path}: {e}") 93 94 95 def verify_minidump_files(directory): 96 """Verify that .dmp and .extra files exist and return their paths.""" 97 minidump_files = list(Path(directory).iterdir()) 98 assert len(minidump_files) == 2, f"Expected 2 files, found {minidump_files}." 99 100 required_extensions = {".dmp", ".extra"} 101 file_map = { 102 file.suffix: file 103 for file in minidump_files 104 if file.suffix in required_extensions 105 } 106 107 missing_extensions = required_extensions - file_map.keys() 108 assert not missing_extensions, ( 109 f"Missing required files with extensions: {missing_extensions}" 110 ) 111 112 return file_map