#!/usr/bin/env python

"""
A script to help track down the commit that caused tests to fail.
"""

from standard_script_setup import *
from CIME.utils import expect, run_cmd_no_fail, run_cmd
from CIME.XML.machines import Machines

import argparse, sys, os, re

logger = logging.getLogger(__name__)

_MACHINE = Machines()

###############################################################################
def parse_command_line(args, description):
###############################################################################
    parser = argparse.ArgumentParser(
        usage="""\n{0} <testargs> <last-known-good-commit> [--bad=<bad>] [--compare=<baseline-id>] [--no-batch]  [--verbose]
OR
{0} --help

\033[1mEXAMPLES:\033[0m
    \033[1;32m# Bisect ERS.f45_g37.B1850C5 which got broken in the last 4 commits \033[0m
    > cd <root-of-broken-cime-repo>
    > {0} HEAD~4 ERS.f45_g37.B1850C5

    \033[1;32m# Bisect ERS.f45_g37.B1850C5 which started to DIFF in the last 4 commits \033[0m
    > cd <root-of-broken-cime-repo>
    > {0} HEAD~4 'ERS.f45_g37.B1850C5 -c -b master'

    \033[1;32m# Bisect a build error for ERS.f45_g37.B1850C5 which got broken in the last 4 commits \033[0m
    > cd <root-of-broken-cime-repo>
    > {0} HEAD~4 'ERS.f45_g37.B1850C5 --no-run'

    \033[1;32m# Bisect two different failing tests which got broken in the last 4 commits \033[0m
    > cd <root-of-broken-cime-repo>
    > {0} HEAD~4 'ERS.f45_g37.B1850C5 --no-run' 'SMS.f45_g37.F'

""".format(os.path.basename(args[0])),

description=description,

formatter_class=argparse.ArgumentDefaultsHelpFormatter
)

    CIME.utils.setup_standard_logging_options(parser)

    parser.add_argument("good", help="Name of most recent known good commit.")

    parser.add_argument("-B", "--bad", default="HEAD",
                        help="Name of bad commit, default is current HEAD.")

    parser.add_argument("-a", "--all-commits", action="store_true",
                        help="Test all commits, not just merges")

    parser.add_argument("-C", "--cime-integration", action="store_true",
                        help="Bisect CIME instead of the whole code. Useful for finding errors after CIME merges")

    parser.add_argument("-S", "--script",
                        help="Use your own custom script instead")

    ct_modifiers = parser.add_argument_group("create_test", "flags for modifying create_test call to be bisected")

    ct_modifiers.add_argument("testargs", nargs="*", help="String to pass to create_test. Combine with single quotes if it includes multiple args.")

    ct_modifiers.add_argument("-r", "--test-root",
                        help="Path to testroot to use for testcases for bisect. WARNING. This will be cleared by this script.")

    ct_modifiers.add_argument("-n", "--check-namelists", action="store_true",
                        help="Consider a commit to be broken if namelist check fails")

    ct_modifiers.add_argument("-t", "--check-throughput", action="store_true",
                        help="Consider a commit to be broken if throughput check fails (fail if tests slow down)")

    ct_modifiers.add_argument("-m", "--check-memory", action="store_true",
                        help="Consider a commit to be broken if memory check fails (fail if tests footprint grows)")

    ct_modifiers.add_argument("-l", "--check-memleak", action="store_true",
                        help="Consider a commit to be broken if a memleak is detected")

    args = CIME.utils.parse_args_and_handle_standard_logging_options(args, parser)

    if args.test_root is None:
        args.test_root = os.path.join(_MACHINE.get_value("CIME_OUTPUT_ROOT"), "cime_bisect")

    if args.cime_integration:
        expect(os.path.basename(os.getcwd()) == "cime" and os.path.isdir(".git"), \
"""
In order for --cime-integration mode to work, it is expected that you have deleted
your cime subtree and replaced it with a clone of ESMCI cime. It is also expected
that you run this script from the cime direcory.
""")

    expect(os.path.isdir(".git"), "Please run the root of a repo")

    expect( not (bool(args.script) and (bool(args.testargs) or args.check_namelists or args.check_throughput or args.check_memory or args.check_memleak) ),
            "Makes no sense to use custom script but also ask for create_test modifiers")

    return args.testargs, args.good, args.bad, args.test_root, \
        args.check_namelists, args.check_throughput, args.check_memory, args.check_memleak, args.all_commits, args.script

###############################################################################
def cime_bisect(testargs, good, bad, testroot,
                check_namelists, check_throughput, check_memory, check_memleak, commits_to_skip,
                custom_script):
###############################################################################
    logger.info("####################################################")
    logger.info("TESTING WITH ARGS '{}'".format(testargs))
    logger.info("####################################################")

    if os.path.exists("scripts/create_test"):
        create_test = os.path.join(os.getcwd(), "scripts", "create_test")
    else:
        create_test = os.path.join(os.getcwd(), "cime", "scripts", "create_test")

    expect(os.path.exists(create_test), "Please run the root of a CIME repo")

    # Basic setup
    run_cmd_no_fail("git bisect start")
    run_cmd_no_fail("git bisect good {}".format(good), verbose=True)
    run_cmd_no_fail("git bisect bad {}".format(bad), verbose=True)
    if commits_to_skip:
        run_cmd_no_fail("git bisect skip {}".format(" ".join(commits_to_skip)))

    # Formulate the create_test command, let create_test make the test-id, it will use
    # a timestamp that will allow us to avoid collisions
    bisect_cmd = "git submodule update --recursive && {} {} --test-root {}".format(create_test, testargs, testroot)

    is_batch = _MACHINE.has_batch_system()
    if (is_batch and "--no-run" not in testargs and "--no-build" not in testargs and "--no-setup" not in testargs):
        # Forumulate the wait_for_tests command.

        bisect_cmd += " --wait"

        if (check_throughput):
            bisect_cmd += " --wait-check-throughput"
        if (check_memory):
            bisect_cmd += " --wait-check-memory"
        if (not check_namelists):
            bisect_cmd += " --wait-ignore-namelists"
        if (not check_memleak):
            bisect_cmd += " --wait-ignore-memleak"

    try:
        if custom_script:
            cmd = "git bisect run {}".format(custom_script)
        else:
            cmd = "git bisect run sh -c '{}'".format(bisect_cmd)

        output = run_cmd(cmd, verbose=True)[1]

        # Get list of potentially bad commits from output
        lines = output.splitlines()
        regex = re.compile(r'^([a-f0-9]{40}).*$')
        bad_commits = set([regex.match(line).groups()[0] for line in lines if regex.match(line)])

        bad_commits_filtered = bad_commits - commits_to_skip

        expect(len(bad_commits_filtered) == 1, bad_commits_filtered)

        logger.info("####################################################")
        logger.info("BAD MERGE FOR ARGS '{}' IS:".format(testargs))
        logger.info("####################################################")
        logger.warning(run_cmd_no_fail("git show {}".format(bad_commits_filtered.pop())))

    finally:
        run_cmd_no_fail("git bisect reset && git submodule update --recursive")

###############################################################################
def _main_func(description):
###############################################################################
    testargs, good, bad, testroot, check_namelists, check_throughput, check_memory, check_memleak, all_commits, custom_script = \
        parse_command_line(sys.argv, description)

    # Important: we only want to test merges
    if not all_commits:
        commits_we_want_to_test = run_cmd_no_fail("git rev-list {}..{} --merges --first-parent".format(good, bad)).splitlines()
        all_commits_            = run_cmd_no_fail("git rev-list {}..{}".format(good, bad)).splitlines()
        commits_to_skip         = set(all_commits_) - set(commits_we_want_to_test)
        logger.info("Skipping {} non-merge commits".format(len(commits_to_skip)))
        for item in commits_to_skip:
            logger.debug(item)
    else:
        commits_to_skip = set()

    if custom_script:
        cime_bisect(custom_script, good, bad, testroot, check_namelists, check_throughput, check_memory, check_memleak, commits_to_skip, custom_script)
    else:
        for set_of_test_args in testargs:
            cime_bisect(set_of_test_args, good, bad, testroot, check_namelists, check_throughput, check_memory, check_memleak, commits_to_skip, custom_script)

###############################################################################

if (__name__ == "__main__"):
    _main_func(__doc__)
