summaryrefslogtreecommitdiffstats
path: root/testing/marionette/harness/marionette_harness/tests/unit/test_crash.py
blob: b413adda0d1e14297380656a5bff92a4dd24918a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

import glob
import os
import shutil
import sys

from io import StringIO

from marionette_driver import Wait
from marionette_driver.errors import (
    InvalidSessionIdException,
    NoSuchWindowException,
    TimeoutException,
)

from marionette_harness import MarionetteTestCase, expectedFailure

# Import runner module to monkey patch mozcrash module
from mozrunner.base import runner


class MockMozCrash(object):
    """Mock object to replace original mozcrash methods."""

    def __init__(self, marionette):
        self.marionette = marionette

        with self.marionette.using_context("chrome"):
            self.crash_reporter_enabled = self.marionette.execute_script(
                """
                const { AppConstants } = ChromeUtils.importESModule(
                  "resource://gre/modules/AppConstants.sys.mjs"
                );
                return AppConstants.MOZ_CRASHREPORTER;
            """
            )

    def check_for_crashes(self, dump_directory, *args, **kwargs):
        if self.crash_reporter_enabled:
            # Workaround until bug 1376795 has been fixed
            # Wait at maximum 5s for the minidump files being created
            # minidump_files = glob.glob('{}/*.dmp'.format(dump_directory))
            try:
                minidump_files = Wait(None, timeout=5).until(
                    lambda _: glob.glob("{}/*.dmp".format(dump_directory))
                )
            except TimeoutException:
                minidump_files = []

            if os.path.isdir(dump_directory):
                shutil.rmtree(dump_directory)

            return len(minidump_files)
        else:
            return len(minidump_files) == 0

    def log_crashes(self, logger, dump_directory, *args, **kwargs):
        return self.check_for_crashes(dump_directory, *args, **kwargs)


class BaseCrashTestCase(MarionetteTestCase):
    # Reduce the timeout for faster processing of the tests
    socket_timeout = 10

    def setUp(self):
        super(BaseCrashTestCase, self).setUp()

        # Monkey patch mozcrash to avoid crash info output only for our triggered crashes.
        mozcrash_mock = MockMozCrash(self.marionette)
        if not mozcrash_mock.crash_reporter_enabled:
            self.skipTest("Crash reporter disabled")
            return

        self.mozcrash = runner.mozcrash
        runner.mozcrash = mozcrash_mock

        self.crash_count = self.marionette.crashed
        self.pid = self.marionette.process_id

    def tearDown(self):
        # Replace mockup with original mozcrash instance
        runner.mozcrash = self.mozcrash

        self.marionette.crashed = self.crash_count

        super(BaseCrashTestCase, self).tearDown()

    def crash(self, parent=True):
        socket_timeout = self.marionette.client.socket_timeout
        self.marionette.client.socket_timeout = self.socket_timeout

        self.marionette.set_context("content")
        try:
            self.marionette.navigate(
                "about:crash{}".format("parent" if parent else "content")
            )
        finally:
            self.marionette.client.socket_timeout = socket_timeout


class TestCrash(BaseCrashTestCase):
    def setUp(self):
        if os.environ.get("MOZ_AUTOMATION"):
            # Capture stdout, otherwise the Gecko output causes mozharness to fail
            # the task due to "A content process has crashed" appearing in the log.
            # To view stdout for debugging, use `print(self.new_out.getvalue())`
            print(
                "Suppressing GECKO output. To view, add `print(self.new_out.getvalue())` "
                "to the end of this test."
            )
            self.new_out, self.new_err = StringIO(), StringIO()
            self.old_out, self.old_err = sys.stdout, sys.stderr
            sys.stdout, sys.stderr = self.new_out, self.new_err

        super(TestCrash, self).setUp()

    def tearDown(self):
        super(TestCrash, self).tearDown()

        if os.environ.get("MOZ_AUTOMATION"):
            sys.stdout, sys.stderr = self.old_out, self.old_err

    def test_crash_chrome_process(self):
        self.assertRaisesRegexp(IOError, "Process crashed", self.crash, parent=True)

        # A crash results in a non zero exit code
        self.assertNotIn(self.marionette.instance.runner.returncode, (None, 0))

        self.assertEqual(self.marionette.crashed, 1)
        self.assertIsNone(self.marionette.session)
        with self.assertRaisesRegexp(
            InvalidSessionIdException, "Please start a session"
        ):
            self.marionette.get_url()

        self.marionette.start_session()
        self.assertNotEqual(self.marionette.process_id, self.pid)

        self.marionette.get_url()

    def test_crash_content_process(self):
        # For a content process crash and MOZ_CRASHREPORTER_SHUTDOWN set the top
        # browsing context will be gone first. As such the raised NoSuchWindowException
        # has to be ignored. To check for the IOError, further commands have to
        # be executed until the process is gone.
        with self.assertRaisesRegexp(IOError, "Content process crashed"):
            self.crash(parent=False)
            Wait(
                self.marionette,
                timeout=self.socket_timeout,
                ignored_exceptions=NoSuchWindowException,
            ).until(
                lambda _: self.marionette.get_url(),
                message="Expected IOError exception for content crash not raised.",
            )

        # A crash when loading about:crashcontent results in a SIGUSR1 exit code.
        self.assertEqual(self.marionette.instance.runner.returncode, 245)

        self.assertEqual(self.marionette.crashed, 1)
        self.assertIsNone(self.marionette.session)
        with self.assertRaisesRegexp(
            InvalidSessionIdException, "Please start a session"
        ):
            self.marionette.get_url()

        self.marionette.start_session()
        self.assertNotEqual(self.marionette.process_id, self.pid)
        self.marionette.get_url()

    @expectedFailure
    def test_unexpected_crash(self):
        self.crash(parent=True)


class TestCrashInSetUp(BaseCrashTestCase):
    def setUp(self):
        super(TestCrashInSetUp, self).setUp()

        self.assertRaisesRegexp(IOError, "Process crashed", self.crash, parent=True)

        # A crash results in a non zero exit code
        self.assertNotIn(self.marionette.instance.runner.returncode, (None, 0))

        self.assertEqual(self.marionette.crashed, 1)
        self.assertIsNone(self.marionette.session)

    def test_crash_in_setup(self):
        self.marionette.start_session()
        self.assertNotEqual(self.marionette.process_id, self.pid)


class TestCrashInTearDown(BaseCrashTestCase):
    def tearDown(self):
        try:
            self.assertRaisesRegexp(IOError, "Process crashed", self.crash, parent=True)

            # A crash results in a non zero exit code
            self.assertNotIn(self.marionette.instance.runner.returncode, (None, 0))

            self.assertEqual(self.marionette.crashed, 1)
            self.assertIsNone(self.marionette.session)

        finally:
            super(TestCrashInTearDown, self).tearDown()

    def test_crash_in_teardown(self):
        pass