summaryrefslogtreecommitdiffstats
path: root/testing/mozharness/mozharness/mozilla/structuredlog.py
diff options
context:
space:
mode:
Diffstat (limited to 'testing/mozharness/mozharness/mozilla/structuredlog.py')
-rw-r--r--testing/mozharness/mozharness/mozilla/structuredlog.py323
1 files changed, 323 insertions, 0 deletions
diff --git a/testing/mozharness/mozharness/mozilla/structuredlog.py b/testing/mozharness/mozharness/mozilla/structuredlog.py
new file mode 100644
index 0000000000..b8d58b5ef2
--- /dev/null
+++ b/testing/mozharness/mozharness/mozilla/structuredlog.py
@@ -0,0 +1,323 @@
+# ***** BEGIN LICENSE BLOCK *****
+# 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/.
+# ***** END LICENSE BLOCK *****
+import json
+from collections import defaultdict, namedtuple
+
+from mozsystemmonitor.resourcemonitor import SystemResourceMonitor
+
+from mozharness.base import log
+from mozharness.base.log import ERROR, INFO, WARNING, OutputParser
+from mozharness.mozilla.automation import (
+ TBPL_FAILURE,
+ TBPL_RETRY,
+ TBPL_SUCCESS,
+ TBPL_WARNING,
+ TBPL_WORST_LEVEL_TUPLE,
+)
+from mozharness.mozilla.testing.errors import TinderBoxPrintRe
+from mozharness.mozilla.testing.unittest import tbox_print_summary
+
+
+class StructuredOutputParser(OutputParser):
+ # The script class using this must inherit the MozbaseMixin to ensure
+ # that mozlog is available.
+ def __init__(self, **kwargs):
+ """Object that tracks the overall status of the test run"""
+ # The 'strict' argument dictates whether the presence of output
+ # from the harness process other than line-delimited json indicates
+ # failure. If it does not, the errors_list parameter may be used
+ # to detect additional failure output from the harness process.
+ if "strict" in kwargs:
+ self.strict = kwargs.pop("strict")
+ else:
+ self.strict = True
+
+ self.suite_category = kwargs.pop("suite_category", None)
+
+ tbpl_compact = kwargs.pop("log_compact", False)
+ super(StructuredOutputParser, self).__init__(**kwargs)
+ self.allow_crashes = kwargs.pop("allow_crashes", False)
+
+ mozlog = self._get_mozlog_module()
+ self.formatter = mozlog.formatters.TbplFormatter(compact=tbpl_compact)
+ self.handler = mozlog.handlers.StatusHandler()
+ self.log_actions = mozlog.structuredlog.log_actions()
+
+ self.worst_log_level = INFO
+ self.tbpl_status = TBPL_SUCCESS
+ self.harness_retry_re = TinderBoxPrintRe["harness_error"]["retry_regex"]
+ self.prev_was_unstructured = False
+
+ def _get_mozlog_module(self):
+ try:
+ import mozlog
+ except ImportError:
+ self.fatal(
+ "A script class using structured logging must inherit "
+ "from the MozbaseMixin to ensure that mozlog is available."
+ )
+ return mozlog
+
+ def _handle_unstructured_output(self, line, log_output=True):
+ self.log_output = log_output
+ return super(StructuredOutputParser, self).parse_single_line(line)
+
+ def parse_single_line(self, line):
+ """Parses a line of log output from the child process and passes
+ it to mozlog to update the overall status of the run.
+ Re-emits the logged line in human-readable format.
+ """
+ level = INFO
+ tbpl_level = TBPL_SUCCESS
+
+ data = None
+ try:
+ candidate_data = json.loads(line)
+ if (
+ isinstance(candidate_data, dict)
+ and "action" in candidate_data
+ and candidate_data["action"] in self.log_actions
+ ):
+ data = candidate_data
+ except ValueError:
+ pass
+
+ if data is None:
+ if self.strict:
+ if not self.prev_was_unstructured:
+ self.info(
+ "Test harness output was not a valid structured log message"
+ )
+ self.info(line)
+ else:
+ self.info(line)
+ self.prev_was_unstructured = True
+ else:
+ self._handle_unstructured_output(line)
+ return
+
+ self.prev_was_unstructured = False
+
+ self.handler(data)
+
+ action = data["action"]
+ if action == "test_start":
+ SystemResourceMonitor.begin_marker("test", data["test"])
+ elif action == "test_end":
+ SystemResourceMonitor.end_marker("test", data["test"])
+ elif action == "suite_start":
+ SystemResourceMonitor.begin_marker("suite", data["source"])
+ elif action == "suite_end":
+ SystemResourceMonitor.end_marker("suite", data["source"])
+ elif action == "group_start":
+ SystemResourceMonitor.begin_marker("test", data["name"])
+ elif action == "group_end":
+ SystemResourceMonitor.end_marker("test", data["name"])
+ if line.startswith("TEST-UNEXPECTED-FAIL"):
+ SystemResourceMonitor.record_event(line)
+
+ if action in ("log", "process_output"):
+ if action == "log":
+ message = data["message"]
+ level = getattr(log, data["level"].upper())
+ else:
+ message = data["data"]
+
+ # Run log and process_output actions through the error lists, but make sure
+ # the super parser doesn't print them to stdout (they should go through the
+ # log formatter).
+ error_level = self._handle_unstructured_output(message, log_output=False)
+ if error_level is not None:
+ level = self.worst_level(error_level, level)
+
+ if self.harness_retry_re.search(message):
+ self.update_levels(TBPL_RETRY, log.CRITICAL)
+ tbpl_level = TBPL_RETRY
+ level = log.CRITICAL
+
+ log_data = self.formatter(data)
+ if log_data is not None:
+ self.log(log_data, level=level)
+ self.update_levels(tbpl_level, level)
+
+ def _subtract_tuples(self, old, new):
+ items = set(list(old.keys()) + list(new.keys()))
+ merged = defaultdict(int)
+ for item in items:
+ merged[item] = new.get(item, 0) - old.get(item, 0)
+ if merged[item] <= 0:
+ del merged[item]
+ return merged
+
+ def evaluate_parser(self, return_code, success_codes=None, previous_summary=None):
+ success_codes = success_codes or [0]
+ summary = self.handler.summarize()
+
+ """
+ We can run evaluate_parser multiple times, it will duplicate failures
+ and status which can mean that future tests will fail if a previous test fails.
+ When we have a previous summary, we want to do 2 things:
+ 1) Remove previous data from the new summary to only look at new data
+ 2) Build a joined summary to include the previous + new data
+ """
+ RunSummary = namedtuple(
+ "RunSummary",
+ (
+ "unexpected_statuses",
+ "expected_statuses",
+ "known_intermittent_statuses",
+ "log_level_counts",
+ "action_counts",
+ ),
+ )
+ if previous_summary == {}:
+ previous_summary = RunSummary(
+ defaultdict(int),
+ defaultdict(int),
+ defaultdict(int),
+ defaultdict(int),
+ defaultdict(int),
+ )
+ if previous_summary:
+ # Always preserve retry status: if any failure triggers retry, the script
+ # must exit with TBPL_RETRY to trigger task retry.
+ if self.tbpl_status != TBPL_RETRY:
+ self.tbpl_status = TBPL_SUCCESS
+ joined_summary = summary
+
+ # Remove previously known status messages
+ if "ERROR" in summary.log_level_counts:
+ summary.log_level_counts["ERROR"] -= self.handler.no_tests_run_count
+
+ summary = RunSummary(
+ self._subtract_tuples(
+ previous_summary.unexpected_statuses, summary.unexpected_statuses
+ ),
+ self._subtract_tuples(
+ previous_summary.expected_statuses, summary.expected_statuses
+ ),
+ self._subtract_tuples(
+ previous_summary.known_intermittent_statuses,
+ summary.known_intermittent_statuses,
+ ),
+ self._subtract_tuples(
+ previous_summary.log_level_counts, summary.log_level_counts
+ ),
+ summary.action_counts,
+ )
+
+ # If we have previous data to ignore,
+ # cache it so we don't parse the log multiple times
+ self.summary = summary
+ else:
+ joined_summary = summary
+
+ fail_pair = TBPL_WARNING, WARNING
+ error_pair = TBPL_FAILURE, ERROR
+
+ # These are warning/orange statuses.
+ failure_conditions = [
+ (sum(summary.unexpected_statuses.values()), 0, "statuses", False),
+ (
+ summary.action_counts.get("crash", 0),
+ summary.expected_statuses.get("CRASH", 0),
+ "crashes",
+ self.allow_crashes,
+ ),
+ (
+ summary.action_counts.get("valgrind_error", 0),
+ 0,
+ "valgrind errors",
+ False,
+ ),
+ ]
+ for value, limit, type_name, allow in failure_conditions:
+ if value > limit:
+ msg = "%d unexpected %s" % (value, type_name)
+ if limit != 0:
+ msg += " expected at most %d" % (limit)
+ if not allow:
+ self.update_levels(*fail_pair)
+ msg = "Got " + msg
+ # Force level to be WARNING as message is not necessary in Treeherder
+ self.warning(msg)
+ else:
+ msg = "Ignored " + msg
+ self.warning(msg)
+
+ # These are error/red statuses. A message is output here every time something
+ # wouldn't otherwise be highlighted in the UI.
+ required_actions = {
+ "suite_end": "No suite end message was emitted by this harness.",
+ "test_end": "No checks run.",
+ }
+ for action, diagnostic_message in required_actions.items():
+ if action not in summary.action_counts:
+ self.log(diagnostic_message, ERROR)
+ self.update_levels(*error_pair)
+
+ failure_log_levels = ["ERROR", "CRITICAL"]
+ for level in failure_log_levels:
+ if level in summary.log_level_counts:
+ self.update_levels(*error_pair)
+
+ # If a superclass was used to detect errors with a regex based output parser,
+ # this will be reflected in the status here.
+ if self.num_errors:
+ self.update_levels(*error_pair)
+
+ # Harnesses typically return non-zero on test failure, so don't promote
+ # to error if we already have a failing status.
+ if return_code not in success_codes and self.tbpl_status == TBPL_SUCCESS:
+ self.update_levels(*error_pair)
+
+ return self.tbpl_status, self.worst_log_level, joined_summary
+
+ def update_levels(self, tbpl_level, log_level):
+ self.worst_log_level = self.worst_level(log_level, self.worst_log_level)
+ self.tbpl_status = self.worst_level(
+ tbpl_level, self.tbpl_status, levels=TBPL_WORST_LEVEL_TUPLE
+ )
+
+ def print_summary(self, suite_name):
+ # Summary text provided for compatibility. Counts are currently
+ # in the format <pass count>/<fail count>/<todo count>,
+ # <expected count>/<unexpected count>/<expected fail count> will yield the
+ # expected info from a structured log (fail count from the prior implementation
+ # includes unexpected passes from "todo" assertions).
+ try:
+ summary = self.summary
+ except AttributeError:
+ summary = self.handler.summarize()
+
+ unexpected_count = sum(summary.unexpected_statuses.values())
+ expected_count = sum(summary.expected_statuses.values())
+ expected_failures = summary.expected_statuses.get("FAIL", 0)
+
+ if unexpected_count:
+ fail_text = '<em class="testfail">%s</em>' % unexpected_count
+ else:
+ fail_text = "0"
+
+ text_summary = "%s/%s/%s" % (expected_count, fail_text, expected_failures)
+ self.info("TinderboxPrint: %s<br/>%s\n" % (suite_name, text_summary))
+
+ def append_tinderboxprint_line(self, suite_name):
+ try:
+ summary = self.summary
+ except AttributeError:
+ summary = self.handler.summarize()
+
+ unexpected_count = sum(summary.unexpected_statuses.values())
+ expected_count = sum(summary.expected_statuses.values())
+ expected_failures = summary.expected_statuses.get("FAIL", 0)
+ crashed = 0
+ if "crash" in summary.action_counts:
+ crashed = summary.action_counts["crash"]
+ text_summary = tbox_print_summary(
+ expected_count, unexpected_count, expected_failures, crashed > 0, False
+ )
+ self.info("TinderboxPrint: %s<br/>%s\n" % (suite_name, text_summary))