tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 14fad281ab8df4c2242bae1f3db477536692c1ea
parent b04f143b82586e02dcc070e7b5087621b2734bc9
Author: Florian Quèze <florian@queze.net>
Date:   Mon, 15 Dec 2025 21:20:08 +0000

Bug 2000636 - Track crash actions by test name so we can account for tests that produce multiple crash dumps when ending with expected CRASH status, r=ahal.

Differential Revision: https://phabricator.services.mozilla.com/D276164

Diffstat:
Mtesting/mozbase/mozlog/mozlog/handlers/statushandler.py | 19++++++++++++++++++-
Mtesting/mozbase/mozlog/tests/test_structured.py | 69+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtoolkit/components/extensions/test/xpcshell/xpcshell-content.toml | 1-
3 files changed, 87 insertions(+), 2 deletions(-)

diff --git a/testing/mozbase/mozlog/mozlog/handlers/statushandler.py b/testing/mozbase/mozlog/mozlog/handlers/statushandler.py @@ -33,11 +33,24 @@ class StatusHandler: self.log_level_counts = defaultdict(int) # The count of "No tests run" error messages seen self.no_tests_run_count = 0 + # Track tests that have already had a crash counted to avoid counting + # multiple crashes for the same test + self.tests_with_counted_crash = set() def __call__(self, data): action = data["action"] known_intermittent = data.get("known_intermittent", []) - self.action_counts[action] += 1 + + # Handle crash actions specially to only count once per test + if action == "crash": + test_name = data.get("test") + # Only count the first crash per test, or always count if no test name + if test_name is None or test_name not in self.tests_with_counted_crash: + self.action_counts["crash"] += 1 + if test_name is not None: + self.tests_with_counted_crash.add(test_name) + else: + self.action_counts[action] += 1 if action == "log": if data["level"] == "ERROR" and data["message"] == "No tests ran": @@ -55,6 +68,10 @@ class StatusHandler: if status in known_intermittent: self.known_intermittent_statuses[status] += 1 + # Clean up crash tracking when test ends to handle retry scenarios + if action == "test_end": + self.tests_with_counted_crash.discard(data.get("test")) + if action == "assertion_count": if data["count"] < data["min_expected"]: self.unexpected_statuses["PASS"] += 1 diff --git a/testing/mozbase/mozlog/tests/test_structured.py b/testing/mozbase/mozlog/tests/test_structured.py @@ -106,6 +106,75 @@ class TestStatusHandler(BaseStructuredTest): self.assertIn("OK", summary.expected_statuses) self.assertEqual(2, summary.expected_statuses["OK"]) + def test_crash_with_expected_crash_status(self): + # Test that crashes are accounted for when test ends with expected CRASH status + self.logger.suite_start([]) + self.logger.test_start("test1") + self.logger.crash( + test="test1", + process=1234, + signature="test_signature", + minidump_path="/path/to/dump", + ) + self.logger.crash( + test="test1", + process=5678, + signature="test_signature2", + minidump_path="/path/to/dump2", + ) + self.logger.test_end("test1", "CRASH", expected="CRASH") + self.logger.suite_end() + summary = self.handler.summarize() + # Extra crashes were subtracted, keeping 1 to match the CRASH status + self.assertEqual(1, summary.action_counts.get("crash", 0)) + # Test had expected CRASH status + self.assertEqual(1, summary.expected_statuses["CRASH"]) + + def test_crash_with_retry_pass(self): + # Simulates xpcshell retry: test crashes producing 2 dumps, then passes on retry + self.logger.suite_start([]) + # First run: test crashes + self.logger.test_start("test1") + self.logger.crash( + test="test1", + process=1234, + signature="test_signature", + minidump_path="/path/to/dump", + ) + self.logger.crash( + test="test1", + process=5678, + signature="test_signature2", + minidump_path="/path/to/dump2", + ) + self.logger.test_end("test1", "CRASH", expected="CRASH") + # Retry: test passes + self.logger.test_start("test1") + self.logger.test_end("test1", "PASS", expected="PASS") + self.logger.suite_end() + summary = self.handler.summarize() + # Extra crashes were subtracted, keeping 1 to match the CRASH status + self.assertEqual(1, summary.action_counts.get("crash", 0)) + # Both test runs are counted + self.assertEqual(1, summary.expected_statuses["CRASH"]) + self.assertEqual(1, summary.expected_statuses["PASS"]) + + def test_crash_without_test_name(self): + # Orphaned crash (e.g., shutdown crash) without associated test + self.logger.suite_start([]) + self.logger.test_start("test1") + self.logger.test_end("test1", "PASS", expected="PASS") + self.logger.crash( + process=9999, + signature="shutdown_crash", + minidump_path="/path/to/dump", + ) + self.logger.suite_end() + summary = self.handler.summarize() + # Crash without test name remains unaccounted + self.assertEqual(1, summary.action_counts["crash"]) + self.assertEqual(1, summary.expected_statuses["PASS"]) + class TestSummaryHandler(BaseStructuredTest): def setUp(self): diff --git a/toolkit/components/extensions/test/xpcshell/xpcshell-content.toml b/toolkit/components/extensions/test/xpcshell/xpcshell-content.toml @@ -33,7 +33,6 @@ skip-if = [ skip-if = [ "os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11' && ccov", # Bug 1854843 "tsan", # Bug 1612707 - "true", # Bug 2000636 ] ["test_ext_contentscript_xrays.js"]