android: generalize support for --test-launcher-filter-file.

This adds filter file support to all test types that previously
supported gtest-style test filtering, notably including instrumentation
and junit tests.

This was requested in crrev.com/c/1208442

Change-Id: I552843898735b52b773bdc397c39ecf64e4df275
Reviewed-on: https://chromium-review.googlesource.com/1214083
Commit-Queue: John Budorick <[email protected]>
Reviewed-by: agrieve <[email protected]>
Cr-Commit-Position: refs/heads/master@{#589682}
diff --git a/build/android/PRESUBMIT.py b/build/android/PRESUBMIT.py
index 9617b51..f341af1 100644
--- a/build/android/PRESUBMIT.py
+++ b/build/android/PRESUBMIT.py
@@ -79,6 +79,7 @@
           J('pylib', 'utils', 'device_dependencies_test.py'),
           J('pylib', 'utils', 'dexdump_test.py'),
           J('pylib', 'utils', 'proguard_test.py'),
+          J('pylib', 'utils', 'test_filter_test.py'),
       ],
       env=pylib_test_env))
 
diff --git a/build/android/pylib/gtest/gtest_test_instance.py b/build/android/pylib/gtest/gtest_test_instance.py
index cd8ddbc..cdbd269 100644
--- a/build/android/pylib/gtest/gtest_test_instance.py
+++ b/build/android/pylib/gtest/gtest_test_instance.py
@@ -16,6 +16,7 @@
 from pylib.base import base_test_result
 from pylib.base import test_instance
 from pylib.symbols import stack_symbolizer
+from pylib.utils import test_filter
 
 with host_paths.SysPath(host_paths.BUILD_COMMON_PATH):
   import unittest_util # pylint: disable=import-error
@@ -234,35 +235,6 @@
   return results
 
 
-def ConvertTestFilterFileIntoGTestFilterArgument(input_lines):
-  """Converts test filter file contents into --gtest_filter argument.
-
-  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/googletest/docs/AdvancedGuide.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:
-    a string suitable for feeding as an argument of --gtest_filter parameter.
-  """
-  # 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 = list(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 = ':'.join(l for l in filter_lines if l[0] != '-')
-  negative_patterns = ':'.join(l[1:] for l in filter_lines if l[0] == '-')
-  if negative_patterns:
-    negative_patterns = '-' + negative_patterns
-
-  # Join the filter lines into one, big --gtest_filter argument.
-  return positive_patterns + negative_patterns
-
 def TestNameWithoutDisabledPrefix(test_name):
   """Modify the test name without disabled prefix if prefix 'DISABLED_' or
   'FLAKY_' presents.
@@ -338,14 +310,7 @@
       error_func('Could not find apk or executable for %s' % self._suite)
 
     self._data_deps = []
-    if args.test_filter:
-      self._gtest_filter = args.test_filter
-    elif args.test_filter_file:
-      with open(args.test_filter_file, 'r') as f:
-        self._gtest_filter = ConvertTestFilterFileIntoGTestFilterArgument(f)
-    else:
-      self._gtest_filter = None
-
+    self._gtest_filter = test_filter.InitializeFilterFromArgs(args)
     self._run_disabled = args.run_disabled
 
     self._data_deps_delegate = data_deps_delegate
diff --git a/build/android/pylib/gtest/gtest_test_instance_test.py b/build/android/pylib/gtest/gtest_test_instance_test.py
index a34ab84..b39da527 100755
--- a/build/android/pylib/gtest/gtest_test_instance_test.py
+++ b/build/android/pylib/gtest/gtest_test_instance_test.py
@@ -181,51 +181,6 @@
     actual = gtest_test_instance.ParseGTestXML(None)
     self.assertEquals([], actual)
 
-  def testConvertTestFilterFile_commentsAndBlankLines(self):
-    input_lines = [
-      'positive1',
-      '# comment',
-      'positive2  # Another comment',
-      ''
-      'positive3'
-    ]
-    actual = gtest_test_instance \
-        .ConvertTestFilterFileIntoGTestFilterArgument(input_lines)
-    expected = 'positive1:positive2:positive3'
-    self.assertEquals(expected, actual)
-
-  def testConvertTestFilterFile_onlyPositive(self):
-    input_lines = [
-      'positive1',
-      'positive2'
-    ]
-    actual = gtest_test_instance \
-        .ConvertTestFilterFileIntoGTestFilterArgument(input_lines)
-    expected = 'positive1:positive2'
-    self.assertEquals(expected, actual)
-
-  def testConvertTestFilterFile_onlyNegative(self):
-    input_lines = [
-      '-negative1',
-      '-negative2'
-    ]
-    actual = gtest_test_instance \
-        .ConvertTestFilterFileIntoGTestFilterArgument(input_lines)
-    expected = '-negative1:negative2'
-    self.assertEquals(expected, actual)
-
-  def testConvertTestFilterFile_positiveAndNegative(self):
-    input_lines = [
-      'positive1',
-      'positive2',
-      '-negative1',
-      '-negative2'
-    ]
-    actual = gtest_test_instance \
-        .ConvertTestFilterFileIntoGTestFilterArgument(input_lines)
-    expected = 'positive1:positive2-negative1:negative2'
-    self.assertEquals(expected, actual)
-
   def testTestNameWithoutDisabledPrefix_disabled(self):
     test_name_list = [
       'A.DISABLED_B',
diff --git a/build/android/pylib/instrumentation/instrumentation_test_instance.py b/build/android/pylib/instrumentation/instrumentation_test_instance.py
index 809fce60..75de58a 100644
--- a/build/android/pylib/instrumentation/instrumentation_test_instance.py
+++ b/build/android/pylib/instrumentation/instrumentation_test_instance.py
@@ -22,6 +22,7 @@
 from pylib.utils import instrumentation_tracing
 from pylib.utils import proguard
 from pylib.utils import shared_preference_utils
+from pylib.utils import test_filter
 
 
 with host_paths.SysPath(host_paths.BUILD_COMMON_PATH):
@@ -62,8 +63,6 @@
 _SKIP_PARAMETERIZATION = 'SkipCommandLineParameterization'
 _COMMANDLINE_PARAMETERIZATION = 'CommandLineParameter'
 _NATIVE_CRASH_RE = re.compile('(process|native) crash', re.IGNORECASE)
-_CMDLINE_NAME_SEGMENT_RE = re.compile(
-    r' with(?:out)? \{[^\}]*\}')
 _PICKLE_FORMAT_VERSION = 12
 
 
@@ -181,7 +180,7 @@
   return results
 
 
-def FilterTests(tests, test_filter=None, annotations=None,
+def FilterTests(tests, filter_str=None, annotations=None,
                 excluded_annotations=None):
   """Filter a list of tests
 
@@ -189,7 +188,7 @@
     tests: a list of tests. e.g. [
            {'annotations": {}, 'class': 'com.example.TestA', 'method':'test1'},
            {'annotations": {}, 'class': 'com.example.TestB', 'method':'test2'}]
-    test_filter: googletest-style filter string.
+    filter_str: googletest-style filter string.
     annotations: a dict of wanted annotations for test methods.
     exclude_annotations: a dict of annotations to exclude.
 
@@ -197,7 +196,7 @@
     A list of filtered tests
   """
   def gtest_filter(t):
-    if not test_filter:
+    if not filter_str:
       return True
     # Allow fully-qualified name as well as an omitted package.
     unqualified_class_test = {
@@ -216,7 +215,7 @@
           GetTestNameWithoutParameterPostfix(unqualified_class_test, sep='.')
       ]
 
-    pattern_groups = test_filter.split('-')
+    pattern_groups = filter_str.split('-')
     if len(pattern_groups) > 1:
       negative_filter = pattern_groups[1]
       if unittest_util.FilterTestNames(names, negative_filter):
@@ -385,9 +384,9 @@
 class UnmatchedFilterException(test_exception.TestException):
   """Raised when a user specifies a filter that doesn't match any tests."""
 
-  def __init__(self, test_filter):
+  def __init__(self, filter_str):
     super(UnmatchedFilterException, self).__init__(
-        'Test filter "%s" matched no tests.' % test_filter)
+        'Test filter "%s" matched no tests.' % filter_str)
 
 
 def GetTestName(test, sep='#'):
@@ -622,9 +621,7 @@
       logging.warning('No data dependencies will be pushed.')
 
   def _initializeTestFilterAttributes(self, args):
-    if args.test_filter:
-      self._test_filter = _CMDLINE_NAME_SEGMENT_RE.sub(
-          '', args.test_filter.replace('#', '.'))
+    self._test_filter = test_filter.InitializeFilterFromArgs(args)
 
     def annotation_element(a):
       a = a.split('=', 1)
diff --git a/build/android/pylib/junit/junit_test_instance.py b/build/android/pylib/junit/junit_test_instance.py
index e8a021a..4dccac9 100644
--- a/build/android/pylib/junit/junit_test_instance.py
+++ b/build/android/pylib/junit/junit_test_instance.py
@@ -3,6 +3,7 @@
 # found in the LICENSE file.
 
 from pylib.base import test_instance
+from pylib.utils import test_filter
 
 
 class JunitTestInstance(test_instance.TestInstance):
@@ -18,7 +19,7 @@
     self._resource_zips = args.resource_zips
     self._robolectric_runtime_deps_dir = args.robolectric_runtime_deps_dir
     self._runner_filter = args.runner_filter
-    self._test_filter = args.test_filter
+    self._test_filter = test_filter.InitializeFilterFromArgs(args)
     self._test_suite = args.test_suite
 
   #override
diff --git a/build/android/pylib/linker/linker_test_instance.py b/build/android/pylib/linker/linker_test_instance.py
index 6ace7a36..5f19db9 100644
--- a/build/android/pylib/linker/linker_test_instance.py
+++ b/build/android/pylib/linker/linker_test_instance.py
@@ -5,6 +5,7 @@
 from pylib.base import test_instance
 from pylib.constants import host_paths
 from pylib.linker import test_case
+from pylib.utils import test_filter
 
 with host_paths.SysPath(host_paths.BUILD_COMMON_PATH):
   import unittest_util
@@ -15,7 +16,7 @@
   def __init__(self, args):
     super(LinkerTestInstance, self).__init__()
     self._test_apk = args.test_apk
-    self._test_filter = args.test_filter
+    self._test_filter = test_filter.InitializeFilterFromArgs(args)
 
   @property
   def test_apk(self):
diff --git a/build/android/pylib/perf/perf_test_instance.py b/build/android/pylib/perf/perf_test_instance.py
index d219e58..49d75e4 100644
--- a/build/android/pylib/perf/perf_test_instance.py
+++ b/build/android/pylib/perf/perf_test_instance.py
@@ -14,6 +14,7 @@
 from pylib.base import base_test_result
 from pylib.base import test_instance
 from pylib.constants import host_paths
+from pylib.utils import test_filter
 
 
 _GIT_CR_POS_RE = re.compile(r'^Cr-Commit-Position: refs/heads/master@{#(\d+)}$')
@@ -77,7 +78,7 @@
     self._single_step = (
         ' '.join(args.single_step_command) if args.single_step else None)
     self._steps = args.steps
-    self._test_filter = args.test_filter
+    self._test_filter = test_filter.InitializeFilterFromArgs(args)
     self._write_buildbot_json = args.write_buildbot_json
 
   #override
diff --git a/build/android/pylib/utils/test_filter.py b/build/android/pylib/utils/test_filter.py
new file mode 100644
index 0000000..59858fb
--- /dev/null
+++ b/build/android/pylib/utils/test_filter.py
@@ -0,0 +1,80 @@
+# 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)? \{[^\}]*\}')
+
+
+def ParseFilterFile(input_lines):
+  """Converts test filter file contents into --gtest_filter argument.
+
+  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/googletest/docs/AdvancedGuide.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:
+    a string suitable for feeding as an argument of --gtest_filter parameter.
+  """
+  # 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 = ':'.join(l for l in filter_lines if l[0] != '-')
+  negative_patterns = ':'.join(l[1:] for l in filter_lines if l[0] == '-')
+  if negative_patterns:
+    negative_patterns = '-' + negative_patterns
+
+  # Join the filter lines into one, big --gtest_filter argument.
+  return positive_patterns + negative_patterns
+
+
+def AddFilterOptions(parser):
+  """Adds filter command-line options to the provided parser.
+
+  Args:
+    parser: an argparse.ArgumentParser instance.
+  """
+  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(
+      # Deprecated argument.
+      '--gtest-filter-file',
+      # New argument.
+      '--test-launcher-filter-file',
+      dest='test_filter_file', type=os.path.realpath,
+      help='Path to file that contains googletest-style filter strings. '
+           'See also //testing/buildbot/filters/README.md.')
+
+
+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.
+  """
+  parsed_filter = None
+  if args.test_filter:
+    parsed_filter = _CMDLINE_NAME_SEGMENT_RE.sub(
+        '', args.test_filter.replace('#', '.'))
+  elif args.test_filter_file:
+    with open(args.test_filter_file, 'r') as f:
+      parsed_filter = ParseFilterFile(f)
+
+  return parsed_filter
diff --git a/build/android/pylib/utils/test_filter_test.py b/build/android/pylib/utils/test_filter_test.py
new file mode 100755
index 0000000..8ee3d21
--- /dev/null
+++ b/build/android/pylib/utils/test_filter_test.py
@@ -0,0 +1,56 @@
+#!/usr/bin/env vpython
+# 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 sys
+import unittest
+
+from pylib.utils import test_filter
+
+class ParseFilterFileTest(unittest.TestCase):
+
+  def testParseFilterFile_commentsAndBlankLines(self):
+    input_lines = [
+      'positive1',
+      '# comment',
+      'positive2  # Another comment',
+      ''
+      'positive3'
+    ]
+    actual = test_filter.ParseFilterFile(input_lines)
+    expected = 'positive1:positive2:positive3'
+    self.assertEquals(expected, actual)
+
+  def testParseFilterFile_onlyPositive(self):
+    input_lines = [
+      'positive1',
+      'positive2'
+    ]
+    actual = test_filter.ParseFilterFile(input_lines)
+    expected = 'positive1:positive2'
+    self.assertEquals(expected, actual)
+
+  def testParseFilterFile_onlyNegative(self):
+    input_lines = [
+      '-negative1',
+      '-negative2'
+    ]
+    actual = test_filter.ParseFilterFile(input_lines)
+    expected = '-negative1:negative2'
+    self.assertEquals(expected, actual)
+
+  def testParseFilterFile_positiveAndNegative(self):
+    input_lines = [
+      'positive1',
+      'positive2',
+      '-negative1',
+      '-negative2'
+    ]
+    actual = test_filter.ParseFilterFile(input_lines)
+    expected = 'positive1:positive2-negative1:negative2'
+    self.assertEquals(expected, actual)
+
+
+if __name__ == '__main__':
+  sys.exit(unittest.main())
diff --git a/build/android/test_runner.py b/build/android/test_runner.py
index 8512acc..a8f50cb 100755
--- a/build/android/test_runner.py
+++ b/build/android/test_runner.py
@@ -47,6 +47,7 @@
 from pylib.results.presentation import test_results_presentation
 from pylib.utils import logdog_helper
 from pylib.utils import logging_utils
+from pylib.utils import test_filter
 
 from py_utils import contextlib_ext
 
@@ -92,6 +93,8 @@
       type=int, default=os.environ.get('GTEST_TOTAL_SHARDS', 1),
       help='Total number of external shards.')
 
+  test_filter.AddFilterOptions(parser)
+
   return parser
 
 
@@ -357,21 +360,6 @@
       help='Wait for java debugger to attach before running any application '
            'code. Also disables test timeouts and sets retries=0.')
 
-  filter_group = parser.add_mutually_exclusive_group()
-  filter_group.add_argument(
-      '-f', '--gtest_filter', '--gtest-filter',
-      dest='test_filter',
-      help='googletest-style filter string.',
-      default=os.environ.get('GTEST_FILTER'))
-  filter_group.add_argument(
-      # Deprecated argument.
-      '--gtest-filter-file',
-      # New argument.
-      '--test-launcher-filter-file',
-      dest='test_filter_file', type=os.path.realpath,
-      help='Path to file that contains googletest-style filter strings. '
-           'See also //testing/buildbot/filters/README.md.')
-
 
 def AddInstrumentationTestOptions(parser):
   """Adds Instrumentation test options to |parser|."""
@@ -419,11 +407,6 @@
       help='Comma-separated list of annotations. Exclude tests with these '
            'annotations.')
   parser.add_argument(
-      '-f', '--test-filter', '--gtest_filter', '--gtest-filter',
-      dest='test_filter',
-      help='Test filter (if not fully qualified, will run all matches).',
-      default=os.environ.get('GTEST_FILTER'))
-  parser.add_argument(
       '--gtest_also_run_disabled_tests', '--gtest-also-run-disabled-tests',
       dest='run_disabled', action='store_true',
       help='Also run disabled tests if applicable.')
@@ -522,9 +505,6 @@
       '--runner-filter',
       help='Filters tests by runner class. Must be fully qualified.')
   parser.add_argument(
-      '-f', '--test-filter',
-      help='Filters tests googletest-style.')
-  parser.add_argument(
       '-s', '--test-suite', required=True,
       help='JUnit test suite to run.')
   debug_group = parser.add_mutually_exclusive_group()
@@ -558,11 +538,6 @@
   parser.add_argument_group('linker arguments')
 
   parser.add_argument(
-      '-f', '--gtest-filter',
-      dest='test_filter',
-      help='googletest-style filter string.',
-      default=os.environ.get('GTEST_FILTER'))
-  parser.add_argument(
       '--test-apk',
       type=os.path.realpath,
       help='Path to the linker test APK.')
@@ -677,9 +652,6 @@
            'file. Information includes runtime and device affinity for each '
            '--steps.')
   parser.add_argument(
-      '-f', '--test-filter',
-      help='Test filter (will match against the names listed in --steps).')
-  parser.add_argument(
       '--write-buildbot-json',
       action='store_true',
       help='Whether to output buildbot json.')
diff --git a/build/android/test_runner.pydeps b/build/android/test_runner.pydeps
index cedab66..c355993 100644
--- a/build/android/test_runner.pydeps
+++ b/build/android/test_runner.pydeps
@@ -195,6 +195,7 @@
 pylib/utils/proguard.py
 pylib/utils/repo_utils.py
 pylib/utils/shared_preference_utils.py
+pylib/utils/test_filter.py
 pylib/utils/time_profile.py
 pylib/valgrind_tools.py
 test_runner.py