#############################################################################
##
## Copyright (C) 2017 The Qt Company Ltd.
## Contact: https://www.qt.io/licensing/
##
## This file is part of Qt for Python.
##
## $QT_BEGIN_LICENSE:LGPL$
## Commercial License Usage
## Licensees holding valid commercial Qt licenses may use this file in
## accordance with the commercial license agreement provided with the
## Software or, alternatively, in accordance with the terms contained in
## a written agreement between you and The Qt Company. For licensing terms
## and conditions see https://www.qt.io/terms-conditions. For further
## information use the contact form at https://www.qt.io/contact-us.
##
## GNU Lesser General Public License Usage
## Alternatively, this file may be used under the terms of the GNU Lesser
## General Public License version 3 as published by the Free Software
## Foundation and appearing in the file LICENSE.LGPL3 included in the
## packaging of this file. Please review the following information to
## ensure the GNU Lesser General Public License version 3 requirements
## will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
##
## GNU General Public License Usage
## Alternatively, this file may be used under the terms of the GNU
## General Public License version 2.0 or (at your option) the GNU General
## Public license version 3 or any later version approved by the KDE Free
## Qt Foundation. The licenses are as published by the Free Software
## Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
## included in the packaging of this file. Please review the following
## information to ensure the GNU General Public License requirements will
## be met: https://www.gnu.org/licenses/gpl-2.0.html and
## https://www.gnu.org/licenses/gpl-3.0.html.
##
## $QT_END_LICENSE$
##
#############################################################################

from __future__ import print_function

"""
testrunner
==========

Provide an interface to the pyside tests.
-----------------------------------------

This program can only be run if PySide was built with tests enabled.
All tests are run in a single pass, and if not blacklisted, an error
is raised at the end of the run.

Recommended build process:
There is no need to install the project.
Building the project with something like

    python setup.py build --build-tests --qmake=<qmakepath> --ignore-git --debug

is sufficient. The tests are run by changing into the latest build dir and there
into pyside2, then 'make test'.


New testing policy:
-------------------

The tests are now run 5 times, and errors are reported
when they appear at least 3 times. With the variable COIN_RERUN_FAILED_ONLY it is
possible to configure if all tests should be rerun or the failed ones, only.

The full mode can be tested locally by setting

    export COIN_RERUN_FAILED_ONLY=0
"""

import os
import sys
import argparse
from textwrap import dedent
from collections import OrderedDict
from timeit import default_timer as timer

from .helper import script_dir, decorate
from .buildlog import builds
from .blacklist import BlackList
from .runner import TestRunner
from .parser import TestParser

# Should we repeat only failed tests?
COIN_RERUN_FAILED_ONLY = True
COIN_THRESHOLD = 3    # report error if >=
COIN_TESTING = 5      # number of runs

if (os.environ.get("COIN_RERUN_FAILED_ONLY", "1").lower() in
    "0 f false n no".split()):
    COIN_RERUN_FAILED_ONLY = False

def test_project(project, args, blacklist, runs):
    ret = []

    # remove files from a former run
    for idx in range(runs):
        index = idx + 1
        runner = TestRunner(builds.selected, project, index)
        if os.path.exists(runner.logfile) and not args.skip:
            os.unlink(runner.logfile)
    # now start the real run
    for idx in range(runs):
        index = idx + 1
        runner = TestRunner(builds.selected, project, index)
        print()
        print("********* Start testing of %s *********" % project)
        print("Config: Using", " ".join(builds.classifiers))
        print()
        if os.path.exists(runner.logfile) and args.skip:
            print("Parsing existing log file:", runner.logfile)
        else:
            if index > 1 and COIN_RERUN_FAILED_ONLY:
                rerun = rerun_list
                if not rerun:
                    print("--- no re-runs found, stopping before test {} ---"
                          .format(index))
                    break
            else:
                rerun = None
            runner.run("RUN {}:".format(idx + 1), rerun, 10 * 60)
        results = TestParser(runner.logfile)
        r = 5 * [0]
        rerun_list = []
        print()
        fatal = False
        for item in results.iter_blacklist(blacklist):
            res = item.rich_result
            sharp = "#" + str(item.sharp)
            mod_name = decorate(item.mod_name)
            print("RES {index}: Test {sharp:>4}: {res:<6} {mod_name}()".format(**locals()))
            r[0] += 1 if res == "PASS" else 0
            r[1] += 1 if res == "FAIL!" else 0
            r[2] += 1 if res == "SKIPPED" else 0 # not yet supported
            r[3] += 1 if res == "BFAIL" else 0
            r[4] += 1 if res == "BPASS" else 0
            if res not in ("PASS", "BPASS"):
                rerun_list.append(item.mod_name)
            # PYSIDE-1229: When a fatal error happens, bail out immediately!
            if item.fatal:
                fatal = item
        print()
        print("Totals:", sum(r), "tests.",
              "{} passed, {} failed, {} skipped, {} blacklisted, {} bpassed."
              .format(*r))
        print()
        print("********* Finished testing of %s *********" % project)
        print()
        ret.append(r)
        if fatal:
            print("FATAL ERROR:", fatal)
            print("Repetitions cancelled!")
            break
    return ret, fatal

def main():
    # create the top-level command parser
    start_time = timer()
    all_projects = "shiboken2 pyside2 pyside2-tools".split()
    tested_projects = "shiboken2 pyside2".split()
    parser = argparse.ArgumentParser(
        formatter_class=argparse.RawDescriptionHelpFormatter,
        description=dedent("""\
        Run the tests for some projects, default = '{}'.

        Testing is now repeated up to {rep} times, and errors are
        only reported if they occur {thr} or more times.
        The environment variable COIN_RERUN_FAILED_ONLY controls if errors
        are only repeated if there are errors. The default is "1".
        """.format("' '".join(tested_projects), thr=COIN_THRESHOLD, rep=COIN_TESTING)))
    subparsers = parser.add_subparsers(dest="subparser_name")

    # create the parser for the "test" command
    parser_test = subparsers.add_parser("test")
    group = parser_test.add_mutually_exclusive_group(required=False)
    blacklist_default = os.path.join(script_dir, 'build_history', 'blacklist.txt')
    group.add_argument("--blacklist", "-b", type=argparse.FileType('r'),
                       default=blacklist_default,
                       help='a Qt blacklist file (default: {})'.format(blacklist_default))
    parser_test.add_argument("--skip", action='store_true',
                        help="skip the tests if they were run before")
    parser_test.add_argument("--environ", nargs='+',
                        help="use name=value ... to set environment variables")
    parser_test.add_argument("--buildno", default=-1, type=int,
                        help="use build number n (0-based), latest = -1 (default)")
    parser_test.add_argument("--projects", nargs='+', type=str,
                        default=tested_projects,
                        choices=all_projects,
                        help="use '{}'' (default) or other projects"
                        .format("' '".join(tested_projects)))
    parser_getcwd = subparsers.add_parser("getcwd")
    parser_getcwd.add_argument("filename", type=argparse.FileType('w'),
                        help="write the build dir name into a file")
    parser_getcwd.add_argument("--buildno", default=-1, type=int,
                        help="use build number n (0-based), latest = -1 (default)")
    parser_list = subparsers.add_parser("list")
    args = parser.parse_args()

    if hasattr(args, "buildno"):
        try:
            builds.set_buildno(args.buildno)
        except IndexError:
            print("history out of range. Try '%s list'" % __file__)
            sys.exit(1)

    if args.subparser_name == "getcwd":
        print(builds.selected.build_dir, file=args.filename)
        print(builds.selected.build_dir, "written to file", args.filename.name)
        sys.exit(0)
    elif args.subparser_name == "test":
        pass # we do it afterwards
    elif args.subparser_name == "list":
        rp = os.path.relpath
        print()
        print("History")
        print("-------")
        for idx, build in enumerate(builds.history):
            print(idx, rp(build.log_dir), rp(build.build_dir))
        print()
        print("Note: only the last history entry of a folder is valid!")
        sys.exit(0)
    else:
        parser.print_help()
        sys.exit(1)

    if args.blacklist:
        args.blacklist.close()
        bl = BlackList(args.blacklist.name)
    else:
        bl = BlackList(None)
    if args.environ:
        for line in args.environ:
            things = line.split("=")
            if len(things) != 2:
                raise ValueError("you need to pass one or more name=value pairs.")
            key, value = things
            os.environ[key] = value

    print(dedent("""\
        System:
          Platform={platform}
          Executable={executable}
          Version={version_lf}
          API version={api_version}

        Environment:""")
        .format(version_lf=sys.version.replace("\n", " "), **sys.__dict__))
    for key, value in sorted(os.environ.items()):
        print("  {}={}".format(key, value))
    print()

    q = 5 * [0]

    runs = COIN_TESTING
    fail_crit = COIN_THRESHOLD
    # now loop over the projects and accumulate
    fatal = False
    for project in args.projects:
        res, fatal = test_project(project, args, bl, runs)
        if fatal:
            runs = 1
        for idx, r in enumerate(res):
            q = list(map(lambda x, y: x+y, r, q))

    if len(args.projects) > 1:
        print("All above projects:", sum(q), "tests.",
              "{} passed, {} failed, {} skipped, {} blacklisted, {} bpassed."
              .format(*q))
        print()

    tot_res = OrderedDict()
    for project in args.projects:
        for idx in range(runs):
            index = idx + 1
            runner = TestRunner(builds.selected, project, index)
            results = TestParser(runner.logfile)
            for item in results.iter_blacklist(bl):
                key = project + ":" + item.mod_name
                tot_res.setdefault(key, [])
                tot_res[key].append(item.rich_result)
    tot_flaky = 0
    print("*" * 79)
    print("**")
    print("*   Summary Of All Tests")
    print("*")
    empty = True
    for test, res in tot_res.items():
        pass__c = res.count("PASS")
        bpass_c = res.count("BPASS")
        fail__c = res.count("FAIL!")
        bfail_c = res.count("BFAIL")
        fail2_c = fail__c + bfail_c
        fatal_c = res.count("FATAL")
        if pass__c == len(res):
            continue
        elif bpass_c >= runs and runs > 1:
            msg = "Remove blacklisting; test passes"
        elif fail__c >= runs:
            msg = "Newly detected Real test failure!"
        elif bfail_c >= runs:
            msg = "Keep blacklisting ;-("
        elif fail2_c > 0 and fail2_c < len(res):
            msg = "Flaky test"
            tot_flaky += 1
        elif fatal_c:
            msg = "FATAL format error, repetitions aborted!"
        else:
            continue
        empty = False
        padding = 6 * runs
        txt = " ".join(("{:<{width}}".format(piece, width=5) for piece in res))
        txt = (txt + padding * " ")[:padding]
        testpad = 36
        if len(test) < testpad:
            test += (testpad - len(test)) * " "
        print(txt, decorate(test), msg)
    if empty:
        print("*   (empty)")
    print("*")
    print("**")
    print("*" * 79)
    print()
    if runs > 1:
        print("Total flaky tests: errors but not always = {}".format(tot_flaky))
        print()
    else:
        print("For info about flaky tests, we need to perform more than one run.")
        print("Please activate the COIN mode:    'export QTEST_ENVIRONMENT=ci'")
        print()
    # nag us about unsupported projects
    ap, tp = set(all_projects), set(tested_projects)
    if ap != tp:
        print("+++++ Note: please support", " ".join(ap-tp), "+++++")
        print()

    stop_time = timer()
    used_time = stop_time - start_time
    # Now create an error if the criterion is met:
    try:
        if fatal:
            raise ValueError("FATAL format error:", fatal)
        err_crit = "'FAIL! >= {}'".format(fail_crit)
        for res in tot_res.values():
            if res.count("FAIL!") >= fail_crit:
                raise ValueError("At least one failure was not blacklisted "
                                 "and met the criterion {}"
                                 .format(err_crit))
        print("No test met the error criterion {}".format(err_crit))
    finally:
        print()
        print("Total time of whole Python script = {:0.2f} sec".format(used_time))
        print()
# eof
