# Copyright 2018 The Chromium Authors. All rights reserved.
# 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)? \{[^\}]*\}')

class ConflictingPositiveFiltersException(Exception):
  """Raised when both filter file and filter argument have positive filters."""


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/master/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_filter',
      help='googletest-style filter string.',
      default=os.environ.get('GTEST_FILTER'))
  filter_group.add_argument(
      '--isolated-script-test-filter',
      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 InitializeFilterFromArgs(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.

  Raises:
    ConflictingPositiveFiltersException if both filter file and command line
    specify positive filters.
  """
  test_filter = ''
  if args.isolated_script_test_filter:
    args.test_filter = args.isolated_script_test_filter.replace('::', ':')
  if args.test_filter:
    test_filter = _CMDLINE_NAME_SEGMENT_RE.sub(
        '', args.test_filter.replace('#', '.'))

  if not args.test_filter_files:
    return test_filter

  # At this point it's potentially several files, in a list and ; separated
  for test_filter_file in args.test_filter_files:
    # At this point it's potentially several files, ; separated
    for test_filter_file in test_filter_file.split(';'):
      # At this point it's individual files
      with open(test_filter_file, 'r') as f:
        positive_file_patterns, negative_file_patterns = ParseFilterFile(f)
        if positive_file_patterns and HasPositivePatterns(test_filter):
          raise ConflictingPositiveFiltersException(
              'Cannot specify positive pattern in both filter file and ' +
              'filter command line argument')
        test_filter = AppendPatternsToFilter(
            test_filter,
            positive_patterns=positive_file_patterns,
            negative_patterns=negative_file_patterns)

  return test_filter
