Title: [291067] trunk/Tools
Revision
291067
Author
lmo...@igalia.com
Date
2022-03-09 14:07:51 -0800 (Wed, 09 Mar 2022)

Log Message

[Tools] Add script to query results database
https://bugs.webkit.org/show_bug.cgi?id=233800

Reviewed by Jonathan Bedard.

This commit adds a script to query https://results.webkit.org for the
history of a testcase for a given bot/config. It includes a
`--only-changes` switch to show only revisions when the actual changed
from the previous one (useful to check when a test started failing).

For now it supports only layout-tests and has GTK/WPE bots predefined,
although full configurations (the query string for results.webkit.org)
can be used to access other bots.

* Scripts/webkit-test-results: Added.

Modified Paths

Added Paths

Diff

Modified: trunk/Tools/ChangeLog (291066 => 291067)


--- trunk/Tools/ChangeLog	2022-03-09 22:05:34 UTC (rev 291066)
+++ trunk/Tools/ChangeLog	2022-03-09 22:07:51 UTC (rev 291067)
@@ -1,5 +1,23 @@
 2022-03-09  Lauro Moura  <lmo...@igalia.com>
 
+        [Tools] Add script to query results database
+        https://bugs.webkit.org/show_bug.cgi?id=233800
+
+        Reviewed by Jonathan Bedard.
+
+        This commit adds a script to query https://results.webkit.org for the
+        history of a testcase for a given bot/config. It includes a
+        `--only-changes` switch to show only revisions when the actual changed
+        from the previous one (useful to check when a test started failing).
+
+        For now it supports only layout-tests and has GTK/WPE bots predefined,
+        although full configurations (the query string for results.webkit.org)
+        can be used to access other bots.
+
+        * Scripts/webkit-test-results: Added.
+
+2022-03-09  Lauro Moura  <lmo...@igalia.com>
+
         [webkitcorepy] Prefer xdg-open to open in linux
         https://bugs.webkit.org/show_bug.cgi?id=237655
 

Added: trunk/Tools/Scripts/webkit-test-results (0 => 291067)


--- trunk/Tools/Scripts/webkit-test-results	                        (rev 0)
+++ trunk/Tools/Scripts/webkit-test-results	2022-03-09 22:07:51 UTC (rev 291067)
@@ -0,0 +1,390 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 Igalia S.L.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this program; if not, write to the
+# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+# Boston, MA 02110-1301, USA.
+# pylint: disable=missing-docstring,invalid-name
+
+"""Command-line interface to the Webkit Results Database"""
+
+import argparse
+from datetime import datetime
+import json
+import logging
+import os
+import pathlib
+import sys
+import textwrap
+from urllib.parse import urljoin, urlencode, parse_qs
+from urllib.request import urlopen
+from urllib.error import HTTPError
+
+BASE_URL = "https://results.webkit.org/"
+
+
+def red(msg):
+    """Returns a shell-aware red string"""
+    return "\033[91m" + msg + "\033[0m"
+
+
+def green(msg):
+    """Returns a shell-aware green string"""
+    return "\033[92m" + msg + "\033[0m"
+
+
+# Roughly ordered from the most desired to the most unwanted.
+OUTCOMES = [
+    "PASS",
+    "TEXT",
+    "IMAGE",
+    "SKIP",
+    "FAIL",
+    "ERROR",
+    "TIMEOUT",  # Put timeout as more severe than ERROR as they often are harder to debug
+    "CRASH",
+]
+
+
+def is_improvement(baseline, current):
+    """Returns true if the current outcome is more favourable."""
+    best_baseline = min(OUTCOMES.index(baseline_token) for baseline_token in baseline.split())
+    return best_baseline > OUTCOMES.index(current)
+
+
+def is_regression(baseline, current):
+    """Returns true if the current outcome downgraded the result."""
+    worst_baseline = max(OUTCOMES.index(baseline_token) for baseline_token in baseline.split())
+    return worst_baseline < OUTCOMES.index(current)
+
+
+def decorate(msg, baseline, current):
+    """Colors the message if the status change represents an improvement or regression."""
+    if is_improvement(baseline, current):
+        msg = green(msg)
+    elif is_regression(baseline, current):
+        msg = red(msg)
+    return msg
+
+
+class Query:
+    """Wrapper around the most common query parameters for a given request"""
+
+    def __init__(self, **kwargs):
+        """Just stores the passed kwargs as parameters to be forwarded to the actual query"""
+        self._params = kwargs.copy()
+
+    @classmethod
+    def from_query_string(cls, query_str):
+        fields = parse_qs(query_str)
+        # parse_qs forces the values to be lists, even when they are single-valued
+        for k, v in fields.items():
+            if len(v) == 1:
+                fields[k] = v[0]
+        return cls(**fields)
+
+    def as_query_string(self):
+        """Escape and build the actual query string"""
+        return urlencode(self._params)
+
+    def add_param(self, **kwargs):
+        """Extends the current set of parameters.
+
+        Beware that it'll overwrite existing values."""
+        self._params.update(kwargs)
+
+
+BOTS = {
+    "gtk-release-x11": Query(
+        platform="GTK", style="release", version_name="Xvfb", version="5.5.0"
+    ),
+    "gtk-release-gtk4": Query(
+        platform="GTK", style="release", version_name="Xvfb", version="4.19.0"
+    ),
+    "gtk-release-wayland": Query(
+        platform="GTK", style="release", version_name="Wayland"
+    ),
+    "wpe-release": Query(platform="WPE", style="release"),
+    "wpe-debug": Query(platform="WPE", style="release"),
+}
+
+
+def get_commit_cache_filename():
+    """Returns the cache filename, ensuring it's parent folder exists"""
+    app_name = "webkit-test-results"
+    xdg_config_home = os.getenv("XDG_CACHE_HOME")
+    if xdg_config_home:
+        directory = os.path.join(xdg_config_home, app_name)
+    else:
+        directory = os.path.join(os.path.expanduser("~"), ".cache", app_name)
+
+    if not os.path.isdir(directory):
+        pathlib.Path(directory).mkdir(parents=True, exist_ok=True)
+
+    return os.path.join(directory, "commits.json")
+
+
+def load_commit_cache(force_reset=False):
+    """Loads the stored commits, fetching again if needed."""
+    filename = get_commit_cache_filename()
+    if force_reset or not os.path.isfile(filename):
+        return reset_commit_cache()
+
+    logging.info("Opening commit cache %s", filename)
+    with open(filename, encoding="utf-8") as handle:
+        return json.load(handle)
+
+
+def reset_commit_cache():
+    """Fetches the last 5000 commits and store their info to be reused in later calls."""
+    # TODO Append to existing instead of resetting
+    logging.info("Resetting commit cache")
+    limit = 5000
+    command = "/api/commits"
+
+    query = {"limit": limit, "branch": "main"}
+
+    url = "" command) + "?" + urlencode(query)
+
+    filename = get_commit_cache_filename()
+    logging.info("Fetching commits from %s", url)
+    with urlopen(url) as response:
+        raw_data = json.load(response)
+
+    uuids = {}
+    for commit in raw_data:
+        # Order is usually zero for single commits pushed to the repo. For example,
+        # commits in SVN. When moving to git, branchs with multiple commits will
+        # make use of it.
+        uuid = str(commit["timestamp"] * 100 + commit["order"])
+        commit["uuid"] = uuid
+        uuids[uuid] = commit
+
+    logging.info("Saving commits to %s", filename)
+    with open(filename, "w", encoding="utf-8") as output:
+        logging.info("Saving downloaded cache")
+        json.dump(uuids, output)
+
+    return uuids
+
+
+def last_run(args):
+    """Shows data about the last test run registered for the selected bot"""
+    endpoint = "api/results/layout-tests"
+    if args.only_changes:
+        logging.info("--only-changes ignored in this command")
+    if args.only_unexpected:
+        logging.info("--only-unexpected ignored in this command")
+
+    configuration = BOTS.get(args.bot, Query.from_query_string(args.bot))
+    configuration.add_param(limit=1)
+    query = configuration.as_query_string()
+
+    url = "" endpoint) + "?" + query
+
+    logging.info("Loading test data from %s", url)
+    try:
+        with urlopen(url) as response:
+            data = ""
+        print(json.dumps(data, sort_keys=True, indent=4, separators=(",", ": ")))
+    except (HTTPError, json.JSONDecodeError) as e:
+        print(e)
+        return 1
+
+    return 0
+
+
+def get_latest_commit(commits):
+    """Returns the newest commit from a set of commits"""
+
+    latest = max(commits.values(), key=lambda x: x["uuid"])
+
+    return latest["identifier"], latest["timestamp"]
+
+
+def report_test(args):
+    """Reports the test history for a single testcase in a single configuration"""
+    endpoint = "api/results/layout-tests"
+    configuration = BOTS.get(args.bot, Query.from_query_string(args.bot))
+    if args.limit > 0:
+        configuration.add_param(limit=args.limit)
+    query = configuration.as_query_string()
+
+    url = "" endpoint + "/" + args.test) + "?" + query
+
+    logging.info("Loading test data from %s", url)
+    try:
+        with urlopen(url) as response:
+            data = ""
+    except IndexError:
+        logging.error("No results returned. Exiting.")
+        return 1
+
+    results = sorted(data["results"], key=lambda result: result["uuid"])
+
+    commits = load_commit_cache(force_reset=args.reset_cache)
+    logging.info("Found %d cached commits", len(commits))
+
+    previous = None
+
+    matched = False
+
+    for result in results:
+        actual = result["actual"]
+        expected = result["expected"]
+        start_time = datetime.fromtimestamp(result["start_time"])
+        uuid = result["uuid"]
+
+        try:
+            commit = commits[str(uuid)]
+            matched = True
+        except KeyError:
+            logging.info("Could not find commit with uuid %s", uuid)
+            continue
+
+        if args.only_changes:
+            if previous == actual:
+                continue
+        elif args.only_unexpected:
+            if actual == expected:
+                continue
+
+        msg = "commit {} expected {} actual {} time {}".format(
+            commit["identifier"], expected, actual, start_time
+        )
+
+        if args.color:
+            print(decorate(msg, expected, actual))
+        else:
+            print(msg)
+        previous = actual
+
+    if not matched:
+        latest_revision, latest_timestamp = get_latest_commit(commits)
+        latest_date = datetime.fromtimestamp(latest_timestamp)
+        print(
+            f"No commits matched. The latest commit in cache is {latest_revision} from {latest_date}",
+            file=sys.stderr,
+        )
+        return 1
+
+    return 0
+
+
+def parse_args():
+    """Parse command line arguments and switches"""
+    parser = argparse.ArgumentParser(
+        description=textwrap.dedent(
+            """
+                Command line tool to query https://results.webkit.org for test history.
+
+                Passing a test case in the command line will show one entry for each
+                test run registered, alongside commit information, expected and actual outcomes,
+                and timestamp of the run. For example:
+
+                $ wk-test-result --bot wpe-release ietestcenter/css3/text/textshadow-001.htm --limit=5
+                commit 244781@main expected PASS actual TEXT time 2021-12-02 18:02:13
+                commit 244787@main expected PASS actual TEXT time 2021-12-02 19:43:10
+                commit 244796@main expected PASS actual TEXT time 2021-12-02 20:41:52
+                commit 244797@main expected PASS actual TEXT time 2021-12-02 21:40:29
+                commit 244803@main expected PASS actual TEXT time 2021-12-02 23:13:19
+
+                To avoid querying the commit data at each invocation, the last 5000 commits
+                are cached (by default, to $XDG_CACHE_HOME/wk-gardening-tools/commits.json).
+
+                Currently, only GLIB-based bots and layout-test suite are supported.
+            """
+        ),
+        formatter_class=argparse.RawTextHelpFormatter,
+    )
+
+    bots_group = parser.add_mutually_exclusive_group()
+    bots_group.add_argument("-b", "--bot", help="Bot to query")
+    bots_group.add_argument("--list-bots", action="" help="List predefined bots")
+
+    # Common options
+    parser.add_argument("-v", "--verbose", action="" help="Verbose output")
+    parser.add_argument(
+        "-r",
+        "--reset-cache",
+        action=""
+        help="Reset the commit cache if needed",
+    )
+    parser.add_argument(
+        "-c",
+        "--color",
+        action=""
+        help="Use colors to highlight unexpected results",
+    )
+
+    parser.add_argument(
+        "--last-run",
+        action=""
+        help="Show status of the last registered run and exit.",
+    )
+
+    parser.add_argument(
+        "--limit",
+        type=int,
+        default=100,
+        help="""Limit the number of results. Defaults to 100, use -1 to ask the server for
+                its default value. Use together with --test""",
+    )
+
+
+    # Output options
+    output_group = parser.add_mutually_exclusive_group()
+    output_group.add_argument(
+        "--only-changes",
+        action=""
+        help="Display only revisions where the state changed. Use with --test",
+    )
+    output_group.add_argument(
+        "--only-unexpected",
+        action=""
+        help="Display only unexpected results. Use with --test",
+    )
+
+    parser.add_argument(
+        "test", help="Test case to be searched", nargs="?", default=None
+    )
+
+    return parser.parse_args()
+
+
+def main():
+    """Main entry point"""
+    args = parse_args()
+
+    if args.verbose:
+        logging.basicConfig(level=logging.DEBUG)
+
+    if args.list_bots:
+        for bot in sorted(BOTS.keys()):
+            print(bot)
+    elif args.last_run:
+        sys.exit(last_run(args))
+    elif args.test:
+        if not args.bot:
+            print("Test history requires a bot to be specified with -b or --bot. Exiting.")
+            sys.exit(1)
+        sys.exit(report_test(args))
+    else:
+        print("No test case provided. Did you forget to pass it?")
+        sys.exit(1)
+
+
+if __name__ == "__main__":
+    main()
Property changes on: trunk/Tools/Scripts/webkit-test-results
___________________________________________________________________

Added: svn:executable

+* \ No newline at end of property
_______________________________________________
webkit-changes mailing list
webkit-changes@lists.webkit.org
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to