layout/tools/reftest/output.py
author Noemi Erli <nerli@mozilla.com>
Tue, 18 Jan 2022 17:41:19 +0200
changeset 604772 310661795ca22ab62de923ce9fc3b36850e6d72d
parent 563956 a0191fea780cbfe3b4113f79db4df28911d9883f
permissions -rw-r--r--
Merge autoland to mozilla-central. a=merge

# 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/.

from __future__ import absolute_import, print_function

import json
import threading
from collections import defaultdict

from mozlog.formatters import TbplFormatter
from mozrunner.utils import get_stack_fixer_function


class ReftestFormatter(TbplFormatter):
    """
    Formatter designed to preserve the legacy "tbpl" format in reftest.

    This is needed for both the reftest-analyzer and mozharness log parsing.
    We can change this format when both reftest-analyzer and mozharness have
    been changed to read structured logs.
    """

    def __call__(self, data):
        if "component" in data and data["component"] == "mozleak":
            # Output from mozleak requires that no prefix be added
            # so that mozharness will pick up these failures.
            return "%s\n" % data["message"]

        formatted = TbplFormatter.__call__(self, data)

        if formatted is None:
            return
        if data["action"] == "process_output":
            return formatted
        return "REFTEST %s" % formatted

    def log(self, data):
        prefix = "%s |" % data["level"].upper()
        return "%s %s\n" % (prefix, data["message"])

    def _format_status(self, data):
        extra = data.get("extra", {})
        status = data["status"]

        status_msg = "TEST-"
        if "expected" in data:
            status_msg += "UNEXPECTED-%s" % status
        else:
            if status not in ("PASS", "SKIP"):
                status_msg += "KNOWN-"
            status_msg += status
            if extra.get("status_msg") == "Random":
                status_msg += "(EXPECTED RANDOM)"
        return status_msg

    def test_status(self, data):
        extra = data.get("extra", {})
        test = data["test"]

        status_msg = self._format_status(data)
        output_text = "%s | %s | %s" % (
            status_msg,
            test,
            data.get("subtest", "unknown test"),
        )
        if data.get("message"):
            output_text += " | %s" % data["message"]

        if "reftest_screenshots" in extra:
            screenshots = extra["reftest_screenshots"]
            image_1 = screenshots[0]["screenshot"]

            if len(screenshots) == 3:
                image_2 = screenshots[2]["screenshot"]
                output_text += (
                    "\nREFTEST   IMAGE 1 (TEST): data:image/png;base64,%s\n"
                    "REFTEST   IMAGE 2 (REFERENCE): data:image/png;base64,%s"
                ) % (image_1, image_2)
            elif len(screenshots) == 1:
                output_text += "\nREFTEST   IMAGE: data:image/png;base64,%s" % image_1

        return output_text + "\n"

    def test_end(self, data):
        status = data["status"]
        test = data["test"]

        output_text = ""
        if status != "OK":
            status_msg = self._format_status(data)
            output_text = "%s | %s | %s" % (status_msg, test, data.get("message", ""))

        if output_text:
            output_text += "\nREFTEST "
        output_text += "TEST-END | %s" % test
        return "%s\n" % output_text

    def process_output(self, data):
        return "%s\n" % data["data"]

    def suite_end(self, data):
        lines = []
        summary = data["extra"]["results"]
        summary["success"] = summary["Pass"] + summary["LoadOnly"]
        lines.append(
            "Successful: %(success)s (%(Pass)s pass, %(LoadOnly)s load only)" % summary
        )
        summary["unexpected"] = (
            summary["Exception"]
            + summary["FailedLoad"]
            + summary["UnexpectedFail"]
            + summary["UnexpectedPass"]
            + summary["AssertionUnexpected"]
            + summary["AssertionUnexpectedFixed"]
        )
        lines.append(
            (
                "Unexpected: %(unexpected)s (%(UnexpectedFail)s unexpected fail, "
                "%(UnexpectedPass)s unexpected pass, "
                "%(AssertionUnexpected)s unexpected asserts, "
                "%(FailedLoad)s failed load, "
                "%(Exception)s exception)"
            )
            % summary
        )
        summary["known"] = (
            summary["KnownFail"]
            + summary["AssertionKnown"]
            + summary["Random"]
            + summary["Skip"]
            + summary["Slow"]
        )
        lines.append(
            (
                "Known problems: %(known)s ("
                + "%(KnownFail)s known fail, "
                + "%(AssertionKnown)s known asserts, "
                + "%(Random)s random, "
                + "%(Skip)s skipped, "
                + "%(Slow)s slow)"
            )
            % summary
        )
        lines = ["REFTEST INFO | %s" % s for s in lines]
        lines.append("REFTEST SUITE-END | Shutdown")
        return "INFO | Result summary:\n{}\n".format("\n".join(lines))


class OutputHandler(object):
    """Process the output of a process during a test run and translate
    raw data logged from reftest.js to an appropriate structured log action,
    where applicable.
    """

    def __init__(self, log, utilityPath, symbolsPath=None):
        self.stack_fixer_function = get_stack_fixer_function(utilityPath, symbolsPath)
        self.log = log
        self.proc_name = None
        self.results = defaultdict(int)

    def __call__(self, line):
        # need to return processed messages to appease remoteautomation.py
        if not line.strip():
            return []
        line = line.decode("utf-8", errors="replace")

        try:
            data = json.loads(line)
        except ValueError:
            self.verbatim(line)
            return [line]

        if isinstance(data, dict) and "action" in data:
            if data["action"] == "results":
                for k, v in data["results"].items():
                    self.results[k] += v
            else:
                self.log.log_raw(data)
        else:
            self.verbatim(json.dumps(data))

        return [data]

    def write(self, data):
        return self.__call__(data)

    def verbatim(self, line):
        if self.stack_fixer_function:
            line = self.stack_fixer_function(line)
        name = self.proc_name or threading.current_thread().name
        self.log.process_output(name, line)