#!/usr/bin/env python
# utils/coverage/coverage-touch-tests - Touch tests covering git changes
#
# This source file is part of the Swift.org open source project
#
# Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
# Licensed under Apache License v2.0 with Runtime Library Exception
#
# See https://swift.org/LICENSE.txt for license information
# See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors

from __future__ import print_function

import argparse
import logging
import multiprocessing
import os
import re
import subprocess
import sys

logging_format = '%(asctime)s %(levelname)s %(message)s'
logging.basicConfig(level=logging.DEBUG,
                    format=logging_format,
                    filename='/tmp/%s.log' % os.path.basename(__file__),
                    filemode='w')
console = logging.StreamHandler()
console.setLevel(logging.INFO)
formatter = logging.Formatter(logging_format)
console.setFormatter(formatter)
logging.getLogger().addHandler(console)

script_path = os.path.realpath(__file__)
sql_query_covering = '''SELECT DISTINCT test_string.string
FROM strings as src_string, coverage, strings as test_string
WHERE src_string.string = '{0}'
  AND ((istart <= {1} AND {1} <= iend) OR
       (istart <= {2} AND {2} <= iend) OR
       ({1} <= istart AND istart <= {2}))
  AND src_string.id = coverage.src
  AND test_string.id = coverage.test;'''


def worker(args):
    i, (coverage_db, (filename, (rstart, rend))) = args
    logging.info('[%s] Finding covering tests for: %s %s',
                 i + 1, filename, (rstart, rend))
    tmp_covering_tests = subprocess.check_output(
        ['sqlite3',
         coverage_db,
         sql_query_covering.format(filename, rstart, rend)]
    ).splitlines()
    for covering_test in tmp_covering_tests:
        logging.debug('  %s', covering_test)
    logging.info('Found %s covering tests', len(tmp_covering_tests))
    return set(tmp_covering_tests)


NUM_CORES = multiprocessing.cpu_count()
pool = multiprocessing.Pool(NUM_CORES)


def main():
    parser = argparse.ArgumentParser(
        description='Touch tests covering git changes')
    parser.add_argument('--swift-dir',
                        metavar='PATH',
                        help='the path to a swift source directory containing '
                             'changes',
                        required=True)
    parser.add_argument('--coverage-db',
                        metavar='PATH',
                        help='the path to a swift coverage database',
                        required=True)
    parser.add_argument('--log',
                        help='the level of information to log (default: info)',
                        metavar='LEVEL',
                        default='info',
                        choices=['info', 'debug', 'warning', 'error',
                                 'critical'])
    args = parser.parse_args()

    for path in [args.swift_dir, args.coverage_db]:
        assert os.path.exists(path), "Unable to find %s. Try absolute" \
                                     " paths." % path

    console.setLevel(level=args.log.upper())
    logging.debug(args)
    environment = os.environ.copy()
    environment['GIT_EXTERNAL_DIFF'] = script_path
    logging.info('Getting diff of swift dir: %s', args.swift_dir)
    output = subprocess.check_output(['git', '-C', args.swift_dir, 'diff'],
                                     env=environment)
    changed_ranges = []
    for line in output.splitlines():
        logging.debug(line)
        if line.startswith('FILENAME'):
            filename = line.split()[1]
        elif line.startswith('@'):
            start_line, num_lines = re.match(
                r'\@\@ \-(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? \@\@',
                line
            ).group(1, 2)
            if num_lines:
                start_line, num_lines = int(start_line), int(num_lines)
                changed_range = (start_line, start_line + num_lines - 1)
                logging.debug('Found changed range: %s %s',
                              filename, changed_range)
                changed_ranges.append(
                    (args.coverage_db, (filename, changed_range)))
            else:
                start_line = int(start_line)
                logging.debug(
                    'Found changed line: %s %s', filename, start_line)
                changed_ranges.append(
                    (args.coverage_db, (filename, (start_line, start_line))))

    logging.info('Found %s changed ranges/lines', len(changed_ranges))
    relevant_changes = [
        cr for cr in changed_ranges
        if cr[1][0].endswith('.cpp') or cr[1][0].endswith('.h')
    ]
    relevant_changes_count = len(relevant_changes)
    logging.info('Finding covering tests for %s relevant ranges/lines...',
                 relevant_changes_count)
    covering_tests = set().union(*pool.map_async(
        worker,
        enumerate(relevant_changes)
    ).get(999999))

    logging.info('Combined covering tests:')
    for covering_test in covering_tests:
        logging.debug('  %s', covering_test)
    logging.info('Found %s combined covering tests',
                 len(covering_tests))

    logging.info('Touching covering tests:')
    for root, folders, files in os.walk(args.swift_dir):
        for filename in files:
            if filename in covering_tests:
                filepath = os.path.join(root, filename)
                logging.info('  %s', filepath)
                os.utime(filepath, None)

    return 0


def diff():
    parser = argparse.ArgumentParser()
    parser.add_argument('path')
    parser.add_argument('old_file')
    parser.add_argument('old_hex')
    parser.add_argument('old_mode')
    parser.add_argument('new_file')
    parser.add_argument('new_hex')
    parser.add_argument('new_mode')
    args = parser.parse_args()
    print('FILENAME', os.path.basename(args.new_file))
    sys.stdout.flush()
    subprocess.call(['diff', '-U0', args.old_file, args.new_file])
    return 0


if __name__ == '__main__':
    if 'GIT_EXTERNAL_DIFF' in os.environ:
        sys.exit(diff())
    else:
        sys.exit(main())
