# Copyright 2013 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.

"""Utilities for dealing with the python unittest module."""

import fnmatch
import re
import sys
import unittest


class _TextTestResult(unittest._TextTestResult):
  """A test result class that can print formatted text results to a stream.

  Results printed in conformance with gtest output format, like:
  [ RUN        ] autofill.AutofillTest.testAutofillInvalid: "test desc."
  [         OK ] autofill.AutofillTest.testAutofillInvalid
  [ RUN        ] autofill.AutofillTest.testFillProfile: "test desc."
  [         OK ] autofill.AutofillTest.testFillProfile
  [ RUN        ] autofill.AutofillTest.testFillProfileCrazyCharacters: "Test."
  [         OK ] autofill.AutofillTest.testFillProfileCrazyCharacters
  """
  def __init__(self, stream, descriptions, verbosity):
    unittest._TextTestResult.__init__(self, stream, descriptions, verbosity)
    self._fails = set()

  def _GetTestURI(self, test):
    return '%s.%s.%s' % (test.__class__.__module__,
                         test.__class__.__name__,
                         test._testMethodName)

  def getDescription(self, test):
    return '%s: "%s"' % (self._GetTestURI(test), test.shortDescription())

  def startTest(self, test):
    unittest.TestResult.startTest(self, test)
    self.stream.writeln('[ RUN        ] %s' % self.getDescription(test))

  def addSuccess(self, test):
    unittest.TestResult.addSuccess(self, test)
    self.stream.writeln('[         OK ] %s' % self._GetTestURI(test))

  def addError(self, test, err):
    unittest.TestResult.addError(self, test, err)
    self.stream.writeln('[      ERROR ] %s' % self._GetTestURI(test))
    self._fails.add(self._GetTestURI(test))

  def addFailure(self, test, err):
    unittest.TestResult.addFailure(self, test, err)
    self.stream.writeln('[     FAILED ] %s' % self._GetTestURI(test))
    self._fails.add(self._GetTestURI(test))

  def getRetestFilter(self):
    return ':'.join(self._fails)


class TextTestRunner(unittest.TextTestRunner):
  """Test Runner for displaying test results in textual format.

  Results are displayed in conformance with google test output.
  """

  def __init__(self, verbosity=1):
    unittest.TextTestRunner.__init__(self, stream=sys.stderr,
                                     verbosity=verbosity)

  def _makeResult(self):
    return _TextTestResult(self.stream, self.descriptions, self.verbosity)


def GetTestsFromSuite(suite):
  """Returns all the tests from a given test suite."""
  tests = []
  for x in suite:
    if isinstance(x, unittest.TestSuite):
      tests += GetTestsFromSuite(x)
    else:
      tests += [x]
  return tests


def GetTestNamesFromSuite(suite):
  """Returns a list of every test name in the given suite."""
  return map(lambda x: GetTestName(x), GetTestsFromSuite(suite))


def GetTestName(test):
  """Gets the test name of the given unittest test."""
  return '.'.join([test.__class__.__module__,
                   test.__class__.__name__,
                   test._testMethodName])


def FilterTestSuite(suite, gtest_filter):
  """Returns a new filtered tests suite based on the given gtest filter.

  See https://github.com/google/googletest/blob/master/docs/advanced.md
  for gtest_filter specification.
  """
  return unittest.TestSuite(FilterTests(GetTestsFromSuite(suite), gtest_filter))


def FilterTests(all_tests, gtest_filter):
  """Filter a list of tests based on the given gtest filter.

  Args:
    all_tests: List of tests (unittest.TestSuite)
    gtest_filter: Filter to apply.

  Returns:
    Filtered subset of the given list of tests.
  """
  test_names = [GetTestName(test) for test in all_tests]
  filtered_names = FilterTestNames(test_names, gtest_filter)
  return [test for test in all_tests if GetTestName(test) in filtered_names]


def FilterTestNames(all_tests, gtest_filter):
  """Filter a list of test names based on the given gtest filter.

  See https://github.com/google/googletest/blob/master/docs/advanced.md
  for gtest_filter specification.

  Args:
    all_tests: List of test names.
    gtest_filter: Filter to apply.

  Returns:
    Filtered subset of the given list of test names.
  """
  pattern_groups = gtest_filter.split('-')
  positive_patterns = ['*']
  if pattern_groups[0]:
    positive_patterns = pattern_groups[0].split(':')
  negative_patterns = []
  if len(pattern_groups) > 1:
    negative_patterns = pattern_groups[1].split(':')

  neg_pats = None
  if negative_patterns:
    neg_pats = re.compile('|'.join(fnmatch.translate(p) for p in
                                   negative_patterns))

  tests = []
  test_set = set()
  for pattern in positive_patterns:
    pattern_tests = [
        test for test in all_tests
        if (fnmatch.fnmatch(test, pattern)
            and not (neg_pats and neg_pats.match(test))
            and test not in test_set)]
    tests.extend(pattern_tests)
    test_set.update(pattern_tests)
  return tests
