test_crash.py (7465B)
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 glob 6 import os 7 import shutil 8 import sys 9 10 from io import StringIO 11 12 from marionette_driver import Wait 13 from marionette_driver.errors import ( 14 InvalidSessionIdException, 15 NoSuchWindowException, 16 TimeoutException, 17 ) 18 19 from marionette_harness import MarionetteTestCase, expectedFailure 20 21 # Import runner module to monkey patch mozcrash module 22 from mozrunner.base import runner 23 24 25 class MockMozCrash(object): 26 """Mock object to replace original mozcrash methods.""" 27 28 def __init__(self, marionette): 29 self.marionette = marionette 30 31 with self.marionette.using_context("chrome"): 32 self.crash_reporter_enabled = self.marionette.execute_script( 33 """ 34 const { AppConstants } = ChromeUtils.importESModule( 35 "resource://gre/modules/AppConstants.sys.mjs" 36 ); 37 return AppConstants.MOZ_CRASHREPORTER; 38 """ 39 ) 40 41 def check_for_crashes(self, dump_directory, *args, **kwargs): 42 if self.crash_reporter_enabled: 43 # Workaround until bug 1376795 has been fixed 44 # Wait at maximum 5s for the minidump files being created 45 # minidump_files = glob.glob('{}/*.dmp'.format(dump_directory)) 46 try: 47 minidump_files = Wait(None, timeout=5).until( 48 lambda _: glob.glob("{}/*.dmp".format(dump_directory)) 49 ) 50 except TimeoutException: 51 minidump_files = [] 52 53 if os.path.isdir(dump_directory): 54 shutil.rmtree(dump_directory) 55 56 return len(minidump_files) 57 else: 58 return len(minidump_files) == 0 59 60 def log_crashes(self, logger, dump_directory, *args, **kwargs): 61 return self.check_for_crashes(dump_directory, *args, **kwargs) 62 63 64 class BaseCrashTestCase(MarionetteTestCase): 65 # Reduce the timeout for faster processing of the tests 66 socket_timeout = 10 67 68 def setUp(self): 69 super(BaseCrashTestCase, self).setUp() 70 71 # Monkey patch mozcrash to avoid crash info output only for our triggered crashes. 72 mozcrash_mock = MockMozCrash(self.marionette) 73 if not mozcrash_mock.crash_reporter_enabled: 74 self.skipTest("Crash reporter disabled") 75 return 76 77 self.mozcrash = runner.mozcrash 78 runner.mozcrash = mozcrash_mock 79 80 self.crash_count = self.marionette.crashed 81 self.pid = self.marionette.process_id 82 83 def tearDown(self): 84 # Replace mockup with original mozcrash instance 85 runner.mozcrash = self.mozcrash 86 87 self.marionette.crashed = self.crash_count 88 89 super(BaseCrashTestCase, self).tearDown() 90 91 def crash(self, parent=True): 92 socket_timeout = self.marionette.client.socket_timeout 93 self.marionette.client.socket_timeout = self.socket_timeout 94 95 self.marionette.set_context("content") 96 try: 97 self.marionette.navigate( 98 "about:crash{}".format("parent" if parent else "content") 99 ) 100 finally: 101 self.marionette.client.socket_timeout = socket_timeout 102 103 104 class TestCrash(BaseCrashTestCase): 105 def setUp(self): 106 if os.environ.get("MOZ_AUTOMATION"): 107 # Capture stdout, otherwise the Gecko output causes mozharness to fail 108 # the task due to "A content process has crashed" appearing in the log. 109 # To view stdout for debugging, use `print(self.new_out.getvalue())` 110 print( 111 "Suppressing GECKO output. To view, add `print(self.new_out.getvalue())` " 112 "to the end of this test." 113 ) 114 self.new_out, self.new_err = StringIO(), StringIO() 115 self.old_out, self.old_err = sys.stdout, sys.stderr 116 sys.stdout, sys.stderr = self.new_out, self.new_err 117 118 super(TestCrash, self).setUp() 119 120 def tearDown(self): 121 super(TestCrash, self).tearDown() 122 123 if os.environ.get("MOZ_AUTOMATION"): 124 sys.stdout, sys.stderr = self.old_out, self.old_err 125 126 def test_crash_chrome_process(self): 127 self.assertRaisesRegex(IOError, "Process crashed", self.crash, parent=True) 128 129 # A crash results in a non zero exit code 130 self.assertNotIn(self.marionette.instance.runner.returncode, (None, 0)) 131 132 self.assertEqual(self.marionette.crashed, 1) 133 self.assertIsNone(self.marionette.session) 134 with self.assertRaisesRegex( 135 InvalidSessionIdException, "Please start a session" 136 ): 137 self.marionette.get_url() 138 139 self.marionette.start_session() 140 self.assertNotEqual(self.marionette.process_id, self.pid) 141 142 self.marionette.get_url() 143 144 def test_crash_content_process(self): 145 # For a content process crash and MOZ_CRASHREPORTER_SHUTDOWN set the top 146 # browsing context will be gone first. As such the raised NoSuchWindowException 147 # has to be ignored. To check for the IOError, further commands have to 148 # be executed until the process is gone. 149 with self.assertRaisesRegex(IOError, "Content process crashed"): 150 self.crash(parent=False) 151 Wait( 152 self.marionette, 153 timeout=self.socket_timeout, 154 ignored_exceptions=NoSuchWindowException, 155 ).until( 156 lambda _: self.marionette.get_url(), 157 message="Expected IOError exception for content crash not raised.", 158 ) 159 160 # A crash when loading about:crashcontent results in a SIGUSR1 exit code. 161 self.assertEqual(self.marionette.instance.runner.returncode, 245) 162 163 self.assertEqual(self.marionette.crashed, 1) 164 self.assertIsNone(self.marionette.session) 165 with self.assertRaisesRegex( 166 InvalidSessionIdException, "Please start a session" 167 ): 168 self.marionette.get_url() 169 170 self.marionette.start_session() 171 self.assertNotEqual(self.marionette.process_id, self.pid) 172 self.marionette.get_url() 173 174 @expectedFailure 175 def test_unexpected_crash(self): 176 self.crash(parent=True) 177 178 179 class TestCrashInSetUp(BaseCrashTestCase): 180 def setUp(self): 181 super(TestCrashInSetUp, self).setUp() 182 183 self.assertRaisesRegex(IOError, "Process crashed", self.crash, parent=True) 184 185 # A crash results in a non zero exit code 186 self.assertNotIn(self.marionette.instance.runner.returncode, (None, 0)) 187 188 self.assertEqual(self.marionette.crashed, 1) 189 self.assertIsNone(self.marionette.session) 190 191 def test_crash_in_setup(self): 192 self.marionette.start_session() 193 self.assertNotEqual(self.marionette.process_id, self.pid) 194 195 196 class TestCrashInTearDown(BaseCrashTestCase): 197 def tearDown(self): 198 try: 199 self.assertRaisesRegex(IOError, "Process crashed", self.crash, parent=True) 200 201 # A crash results in a non zero exit code 202 self.assertNotIn(self.marionette.instance.runner.returncode, (None, 0)) 203 204 self.assertEqual(self.marionette.crashed, 1) 205 self.assertIsNone(self.marionette.session) 206 207 finally: 208 super(TestCrashInTearDown, self).tearDown() 209 210 def test_crash_in_teardown(self): 211 pass