#!/usr/bin/env python

import argparse
import glob
import os
import re
import subprocess
import sys

SCRIPT_ROOT_PATH = os.path.dirname(os.path.realpath(__file__))
PERFTEST_JSON_PATH = os.path.join(SCRIPT_ROOT_PATH, 'project.json')
XUNITPERF_REPO_URL = 'https://github.com/microsoft/xunit-performance.git'

script_args = None

class FatalError(Exception):
    def __init__(self, message):
        self.message = message

def check_requirements():
    try:
        run_command('git', '--version', quiet = True)
    except:
        raise FatalError("git not found, please make sure that it's installed and on path.")

    try:
        run_command('msbuild', '-version', quiet = True)
    except:
        raise FatalError("msbuild not found, please make sure that it's installed and on path.")

    if script_args.xunit_perf_path == None:
        raise FatalError("Don't know where to clone xunit-performance. Please specify --xunit-perf-path <path>. " +
            "You can also set/export XUNIT_PERFORMANCE_PATH to not have to set the value every time.")

def process_arguments():
    parser = argparse.ArgumentParser(
        description = "Runs CLI perf tests. Requires 'git' and 'msbuild' to be on the PATH.",
    )

    parser.add_argument(
        'test_cli',
        help = "full path to the dotnet.exe under test",
    )
    parser.add_argument(
        '--runid', '--name', '-n',
        help = "unique ID for this run",
        required = True,
    )
    parser.add_argument(
        '--base', '--baseline', '-b',
        help = "full path to the baseline dotnet.exe",
        metavar = 'baseline_cli',
        dest = 'base_cli',
    )
    parser.add_argument(
        '--xunit-perf-path', '-x',
        help = """Path to local copy of the xunit-performance repository.
            Required unless the environment variable XUNIT_PERFORMANCE_PATH is defined.""",
        default = os.environ.get('XUNIT_PERFORMANCE_PATH'),
        metavar = 'path',
    )
    parser.add_argument(
        '--rebuild', '--rebuild-tools', '-r',
        help = "Rebuilds the test tools from scratch.",
        action = 'store_true',
    )
    parser.add_argument(
        '--verbose', '-v',
        help = "Shows the output of all commands run by this script",
        action = 'store_true',
    )

    global script_args
    script_args = parser.parse_args()

def run_command(*vargs, **kwargs):
    title = kwargs['title'] if 'title' in kwargs else None
    from_dir = kwargs['from_dir'] if 'from_dir' in kwargs else None
    quiet = kwargs['quiet'] if 'quiet' in kwargs else False

    quoted_args = map(lambda x: '"{x}"'.format(x=x) if ' ' in x else x, vargs)
    cmd_line = ' '.join(quoted_args)
    should_log = not script_args.verbose and title != None
    redirect_args = { 'stderr': subprocess.STDOUT }

    nullfile = None
    logfile = None
    cwd = None

    try:
        if should_log:
            log_name = '-'.join(re.sub(r'\W', ' ', title).lower().split()) + '.log'
            log_path = os.path.join(SCRIPT_ROOT_PATH, 'logs', 'run-perftests', log_name)
            log_dir = os.path.dirname(log_path)
            if not os.path.exists(log_dir):
                os.makedirs(log_dir)
            cmd_line += ' > "{log}"'.format(log = log_path)
            logfile = open(log_path, 'w')
            redirect_args['stdout'] = logfile

        elif quiet or not script_args.verbose:
            nullfile = open(os.devnull, 'w')
            redirect_args['stdout'] = nullfile

        prefix = ''
        if not quiet and title != None:
            print('# {msg}...'.format(msg = title))
            prefix = '  $ '

        if from_dir != None:
            cwd = os.getcwd()
            if not quiet: print('{pref}cd "{dir}"'.format(pref = prefix, dir = from_dir))
            os.chdir(from_dir)

        if not quiet: print(prefix + cmd_line)
        returncode = subprocess.call(vargs, **redirect_args)

        if returncode != 0:
            logmsg = " See '{log}' for details.".format(log = log_path) if should_log else ''
            raise FatalError("Command `{cmd}` returned with error code {e}.{log}".format(cmd = cmd_line, e = returncode, log = logmsg))

    finally:
        if logfile != None: logfile.close()
        if nullfile != None: nullfile.close()
        if cwd != None: os.chdir(cwd)

def clone_repo(repo_url, local_path):
    if os.path.exists(local_path):
        # For now, we just assume that if the path exists, it's already the correct repo
        print("# xunit-performance repo was detected at '{path}', skipping git clone".format(path = local_path))
        return
    run_command(
        'git', 'clone', repo_url, local_path,
        title = "Clone the xunit-performance repo",
    )

def get_xunitperf_dotnet_path(xunitperf_src_path):
    return os.path.join(xunitperf_src_path, 'tools', 'bin', 'dotnet')

def get_xunitperf_runner_src_path(xunitperf_src_path):
    return os.path.join(xunitperf_src_path, 'src', 'cli', 'Microsoft.DotNet.xunit.performance.runner.cli')

def get_xunitperf_analyzer_path(xunitperf_src_path):
    return os.path.join(xunitperf_src_path, 'src', 'xunit.performance.analysis', 'bin', 'Release', 'xunit.performance.analysis')

def make_xunit_perf(xunitperf_src_path):
    dotnet_path = get_xunitperf_dotnet_path(xunitperf_src_path)
    dotnet_base_path = os.path.dirname(dotnet_path)
    analyzer_base_path = os.path.dirname(get_xunitperf_analyzer_path(xunitperf_src_path))
    runner_src_path = get_xunitperf_runner_src_path(xunitperf_src_path)

    if script_args.rebuild or not os.path.exists(dotnet_base_path) or not os.path.exists(analyzer_base_path):
        run_command(
            'CiBuild.cmd', '/release',
            title = "Build xunit-performance",
            from_dir = xunitperf_src_path,
        )
        run_command(
            dotnet_path, 'publish', '-c', 'Release', runner_src_path,
            title = "Build Microsoft.DotNet.xunit.performance.runner.cli",
        )
    else:
        print("# xunit-performance at '{path}' was already built, skipping CiBuild. Use --rebuild to force rebuild.".format(path = xunitperf_src_path))

def run_perf_test(runid, cli_path, xunitperf_src_path):
    cli_path = os.path.realpath(cli_path)
    dotnet_path = get_xunitperf_dotnet_path(xunitperf_src_path)
    runner_src_path = get_xunitperf_runner_src_path(xunitperf_src_path)
    result_xml_path = os.path.join(SCRIPT_ROOT_PATH, '{}.xml'.format(runid))
    project_lock_path = os.path.join(SCRIPT_ROOT_PATH, 'project.lock.json')

    saved_path = os.environ.get('PATH')
    print("# Prepending {dir} to PATH".format(dir = os.path.dirname(cli_path)))
    os.environ['PATH'] = os.path.dirname(cli_path) + ';' + os.environ.get('PATH')
    try:
        if os.path.exists(project_lock_path):
            print("# Deleting {file}".format(file = project_lock_path))
            os.remove(project_lock_path)
        run_command(
            cli_path, 'restore', '-f', 'https://dotnet.myget.org/f/dotnet-core',
            title = "Dotnet restore using \"{cli}\"".format(cli = cli_path),
            from_dir = SCRIPT_ROOT_PATH,
        )
        run_command(
            dotnet_path, 'run', '-p', runner_src_path, '-c', 'Release', '--',
            '-runner', cli_path, '-runid', runid,
            '-runnerargs', 'test {json} -c Release'.format(json = PERFTEST_JSON_PATH),
            title = "Run {id}".format(id = runid),
            from_dir = SCRIPT_ROOT_PATH,
        )
        if not os.path.exists(result_xml_path):
            raise FatalError("Running {id} seems to have failed: {xml} was not generated".format(
                id = runid, xml = result_xml_path
            ))
    finally:
        print("# Reverting PATH")
        os.environ['PATH'] = saved_path

def compare_results(base_id, test_id, out_html, xunitperf_src_path):
    analyzer_path = get_xunitperf_analyzer_path(xunitperf_src_path)

    # Make sure there aren't any stale XMLs in the target dir
    for xml in glob.glob(os.path.join(SCRIPT_ROOT_PATH, '*.xml')):
        if not os.path.basename(xml) in [base_id + '.xml', test_id + '.xml']:
            os.rename(xml, xml + '.bak')

    try:
        run_command(
            analyzer_path, SCRIPT_ROOT_PATH, '-compare', base_id, test_id, '-html', out_html,
            title = "Generate comparison report",
            from_dir = SCRIPT_ROOT_PATH,
        )
        if os.path.exists(out_html):
            print("# Comparison finished, please see \"{report}\" for details.".format(report = out_html))
        else:
            raise FatalError("Failed to genererate comparison report: \"{report}\" not found.".format(report = out_html))

    finally:
        # Revert the renamed XMLs
        for xml in glob.glob(os.path.join(SCRIPT_ROOT_PATH, '*.xml.bak')):
            os.rename(xml, xml[0:-4])

def main():
    try:
        process_arguments()
        check_requirements()

        script_args.xunit_perf_path = os.path.realpath(script_args.xunit_perf_path)

        clone_repo(XUNITPERF_REPO_URL, script_args.xunit_perf_path)
        make_xunit_perf(script_args.xunit_perf_path)

        base_runid = script_args.runid + '.base'
        test_runid = script_args.runid + '.test'
        out_html = os.path.join(SCRIPT_ROOT_PATH, script_args.runid + '.html')

        run_perf_test(test_runid, script_args.test_cli, script_args.xunit_perf_path)
        if script_args.base_cli != None:
            run_perf_test(base_runid, script_args.base_cli, script_args.xunit_perf_path)
            compare_results(base_runid, test_runid, out_html, script_args.xunit_perf_path)

        return 0

    except FatalError as error:
        print("! ERROR: {msg}".format(msg = error.message))
        return 1

if __name__ == "__main__":
    sys.exit(main())