#!/usr/bin/env python3
# Copyright (c) 2019, the Dart project authors.  Please see the AUTHORS file
# for details. All rights reserved. Use of this source code is governed by a
# BSD-style license that can be found in the LICENSE file.
#
"""Webscraper for make_a_fuzz nightly cluster run results.

Given the uri of a make_a_fuzz run, this script will first
extract the links pointing to each of the individual shards
and then parse the output generated by each shard to
find divergences reported by the dartfuzz_test.dart program,
concatenate all output, or summarize all test results.

Example:
  collect_data.py --type sum
      https://ci.chromium.org/p/dart/builders/ci.sandbox/fuzz-linux/303
"""

# This script may require a one time install of BeautifulSoup:
# sudo apt-get install python3-bs4

import argparse
import re
import sys

from bs4 import BeautifulSoup

import requests


# Matches shard raw stdout to extract divergence reports.
P_DIV = re.compile("(Isolate.+? !DIVERGENCE! (\n|.)+?)Isolate ", re.MULTILINE)

# Matches shard raw stdout to extract report summaries.
P_SUM = re.compile(
    r"^Tests: (\d+) Success: (\d+) "
    r"\(Rerun: (\d+)\) Skipped: (\d+) "
    r"Timeout: (\d+) Divergences: (\d+)", re.MULTILINE)

# Matches uri to extract shard number.
P_SHARD = re.compile(r".*make_a_fuzz_shard_(\d+)")


def get_shard_links(uri):
    links = []
    resp = requests.get(uri)
    soup = BeautifulSoup(resp.text, "html.parser")
    for a in soup.findAll("a"):
        if "stdout" in a.text:
            href = a["href"]
            if ("make_a_fuzz_shard" in href and "__trigger__" not in href):
                links.append(href + "?format=raw")
    return links


def print_reencoded(text):
    # Re-encoding avoids breaking some terminals.
    print(text.encode("ascii", errors="ignore").decode("unicode-escape"))


def print_output_all(text):
    print_reencoded(text)


def print_output_div(shard, text, keywords):
    sys.stderr.write("Shard: " + shard + "  \r")
    m = P_DIV.findall(text)
    if m:
        for x in m:
            keep = True
            for word in keywords:
                if word in x[0]:
                    keep = False
                    break
            if keep:
                print_reencoded(x[0])


def get_output_sum(shard, text, should_print, s=[0, 0, 0, 0, 0, 0], divs=[]):
    m = P_SUM.findall(text)
    if not m:
        sys.stderr.write("Failed to parse shard %s stdout for summary" % shard)
        return
    for test in m:
        if int(test[-1]) == 1:
            divs.append(shard)
        for i in range(len(s)):
            s[i] += int(test[i])
    if should_print:
        print(
            "Tests: %d Success: %d (Rerun: %d) Skipped: %d Timeout: %d "
            "Divergences: %d (failing shards: %s)    \r" %
            tuple(s + [", ".join(divs) if divs else "none"]),
            end="")
    return s


def get_stats(uri, output_type, keywords, output_csv):
    resp = requests.get(uri)

    if output_type == "all":
        print_output_all(resp.text)
    elif output_type == "div":
        shard = P_SHARD.findall(uri)[0]
        print_output_div(shard, resp.text, keywords)
    elif output_type == "sum":
        shard = P_SHARD.findall(uri)[0]
        should_print = not output_csv
        return get_output_sum(shard, resp.text, should_print)
    return None


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument(
        "--type",
        choices=("div", "sum", "all"),
        required=True,
        help=
        "Select output type (div: divergence report, sum: summary, all: complete stdout)"
    )
    parser.add_argument(
        "--filter",
        nargs="+",
        default=[],
        help="Do not include divergences containing these keywords.")
    parser.add_argument(
        "--output-csv",
        dest="output_csv",
        action="store_true",
        default=False,
        help=
        "Print output in CSV format to stdout. Only supported for --type=sum")
    parser.add_argument(
        "uri",
        type=str,
        help=
        "Uri of one make_a_fuzz run from https://ci.chromium.org/p/dart/builders/ci.sandbox/fuzz-linux."
    )
    args = parser.parse_args()
    if args.type != 'sum' and args.output_csv:
        print('Error: --output-csv can only be provided for --type=sum')
        return

    shard_links = get_shard_links(args.uri)

    if len(shard_links) == 0:
        print("Invalid run")
        sys.exit(-1)
        return

    for link in shard_links:
        stats = get_stats(link, args.type, args.filter, args.output_csv)
    if args.output_csv:
        print("%d,%d,%d,%d,%d,%d" % tuple(stats))
    else:
        print("")
    sys.exit(0)


if __name__ == "__main__":
    main()
