# Copyright 2018 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import os
import re


_CMDLINE_NAME_SEGMENT_RE = re.compile(r' with(?:out)? \{[^\}]*\}')


def ParseFilterFile(input_lines):
    """Converts test filter file contents to positive and negative pattern lists.

    See //testing/buildbot/filters/README.md for description of the
    syntax that |input_lines| are expected to follow.

    See
    https://github.com/google/googletest/blob/main/docs/advanced.md#running-a-subset-of-the-tests
    for description of the syntax that --gtest_filter argument should follow.

    Args:
      input_lines: An iterable (e.g. a list or a file) containing input lines.
    Returns:
      tuple containing the lists of positive patterns and negative patterns
    """
    # Strip comments and whitespace from each line and filter non-empty lines.
    stripped_lines = (l.split('#', 1)[0].strip() for l in input_lines)
    filter_lines = [l for l in stripped_lines if l]

    # Split the tests into positive and negative patterns (gtest treats
    # every pattern after the first '-' sign as an exclusion).
    positive_patterns = [l for l in filter_lines if l[0] != '-']
    negative_patterns = [l[1:] for l in filter_lines if l[0] == '-']
    return positive_patterns, negative_patterns


def AddFilterOptions(parser):
    """Adds filter command-line options to the provided parser.

    Args:
      parser: an argparse.ArgumentParser instance.
    """
    parser.add_argument(
        # Deprecated argument.
        '--gtest-filter-file',
        # New argument.
        '--test-launcher-filter-file',
        action='append',
        dest='test_filter_files',
        help='Path to file that contains googletest-style filter strings. '
        'See also //testing/buildbot/filters/README.md.',
    )

    filter_group = parser.add_mutually_exclusive_group()
    filter_group.add_argument(
        '-f',
        '--test-filter',
        '--gtest_filter',
        '--gtest-filter',
        dest='test_filters',
        action='append',
        help='googletest-style filter string.',
        default=os.environ.get('GTEST_FILTER'),
    )
    filter_group.add_argument(
        '--isolated-script-test-filter',
        action='append',
        dest='isolated_script_test_filters',
        help='isolated script filter string. '
        'Like gtest filter strings, but with :: separators instead of :',
    )


def AppendPatternsToFilter(
    test_filter, positive_patterns=None, negative_patterns=None
):
    """Returns a test-filter string with additional patterns.

    Args:
      test_filter: test filter string
      positive_patterns: list of positive patterns to add to string
      negative_patterns: list of negative patterns to add to string
    """
    positives = []
    negatives = []
    positive = ''
    negative = ''

    split_filter = test_filter.split('-', 1)
    if len(split_filter) == 1:
        positive = split_filter[0]
    else:
        positive, negative = split_filter

    positives += [f for f in positive.split(':') if f]
    negatives += [f for f in negative.split(':') if f]

    positives += positive_patterns if positive_patterns else []
    negatives += negative_patterns if negative_patterns else []

    final_filter = ':'.join([p.replace('#', '.') for p in positives])
    if negatives:
        final_filter += '-' + ':'.join([n.replace('#', '.') for n in negatives])
    return final_filter


def HasPositivePatterns(test_filter):
    """Returns True if test_filter contains a positive pattern, else False

    Args:
      test_filter: test-filter style string
    """
    return bool(len(test_filter) > 0 and test_filter[0] != '-')


def InitializeFiltersFromArgs(args):
    """Returns a filter string from the command-line option values.

    Args:
      args: an argparse.Namespace instance resulting from a using parser
        to which the filter options above were added.
    """
    # Convert isolated_script_test_filters into test_filters.
    if args.isolated_script_test_filters:
        isolated_filters = [
            isolated_script_test_filter.replace('::', ':')
            for isolated_script_test_filter in args.isolated_script_test_filters
        ]
        if args.test_filters:
            if isinstance(args.test_filters, list):
                args.test_filters.extend(isolated_filters)
            else:
                args.test_filters = [args.test_filters] + isolated_filters
        else:
            args.test_filters = isolated_filters

    test_filters = []
    if args.test_filters:
        # In case args.test_filters is a string (e.g. from default)
        all_filters = (
            args.test_filters
            if isinstance(args.test_filters, list)
            else [args.test_filters]
        )
        for filt in all_filters:
            test_filters.append(
                _CMDLINE_NAME_SEGMENT_RE.sub('', filt.replace('#', '.'))
            )

    if not args.test_filter_files:
        return test_filters

    # At this point it's potentially several files, in a list and ; separated
    for test_filter_files in args.test_filter_files:
        # At this point it's potentially several files, ; separated
        for test_filter_file in test_filter_files.split(';'):
            # At this point it's individual files
            with open(test_filter_file, 'r') as f:
                positive_patterns, negative_patterns = ParseFilterFile(f)
                filter_string = AppendPatternsToFilter(
                    '', positive_patterns, negative_patterns
                )
                test_filters.append(filter_string)

    return test_filters
