Add (some) tests for gtest_parallel.find_tests. (#49)

diff --git a/gtest_parallel.py b/gtest_parallel.py
index 27ae7f0..c5089bf 100755
--- a/gtest_parallel.py
+++ b/gtest_parallel.py
@@ -588,16 +588,7 @@
       timeout.cancel()
 
 
-def main():
-  # Remove additional arguments (anything after --).
-  additional_args = []
-
-  for i in range(len(sys.argv)):
-    if sys.argv[i] == '--':
-      additional_args = sys.argv[i+1:]
-      sys.argv = sys.argv[:i]
-      break
-
+def default_options_parser():
   parser = optparse.OptionParser(
       usage = 'usage: %prog [options] binary [binary ...] -- [additional args]')
 
@@ -640,7 +631,20 @@
   parser.add_option('--serialize_test_cases', action='store_true',
                     default=False, help='Do not run tests from the same test '
                                         'case in parallel.')
+  return parser
 
+
+def main():
+  # Remove additional arguments (anything after --).
+  additional_args = []
+
+  for i in range(len(sys.argv)):
+    if sys.argv[i] == '--':
+      additional_args = sys.argv[i+1:]
+      sys.argv = sys.argv[:i]
+      break
+
+  parser = default_options_parser()
   (options, binaries) = parser.parse_args()
   # Append gtest-parallel-logs to log output, this is to avoid deleting user
   # data if an user passes a directory where files are already present. If a
diff --git a/gtest_parallel_mocks.py b/gtest_parallel_mocks.py
index 891f192..b00ed11 100644
--- a/gtest_parallel_mocks.py
+++ b/gtest_parallel_mocks.py
@@ -48,14 +48,20 @@
 
 
 class TestTimesMock(object):
-  def __init__(self, test_lib):
+  def __init__(self, test_lib, test_data=None):
     self.test_lib = test_lib
+    self.test_data = test_data or {}
     self.last_execution_times = collections.defaultdict(list)
 
   def record_test_time(self, test_binary, test_name, last_execution_time):
     test_id = (test_binary, test_name)
     self.last_execution_times[test_id].append(last_execution_time)
 
+  def get_test_time(self, test_binary, test_name):
+    test_group, test = test_name.split('.')
+    return self.test_data.get(
+        test_binary, {}).get(test_group, {}).get(test, None)
+
   def assertRecorded(self, test_id, expected, retries):
     self.test_lib.assertIn(test_id, self.last_execution_times)
     self.test_lib.assertListEqual(expected['last_execution_time'][:retries],
@@ -141,3 +147,27 @@
 
   def run(self):
     pass
+
+
+class PopenMock(object):
+  class _SubprocessMock(object):
+    def __init__(self, stdout=""):
+      self.stdout = stdout
+
+    def communicate(self):
+      return [self.stdout]
+
+  def __init__(self, test_data=None):
+    self._test_data = test_data
+    self.last_invocation = None
+
+  def __call__(self, command, stdout=None):
+    self.last_invocation = command
+    binary = command[0]
+    test_list = []
+    tests_for_binary = sorted(self._test_data.get(binary, {}).iteritems())
+    for test_group, tests in tests_for_binary:
+      test_list.append(test_group + ".")
+      for test in sorted(tests):
+        test_list.append("  " + test)
+    return self._SubprocessMock('\n'.join(test_list))
diff --git a/gtest_parallel_tests.py b/gtest_parallel_tests.py
index 5725f35..f5c6d47 100755
--- a/gtest_parallel_tests.py
+++ b/gtest_parallel_tests.py
@@ -25,6 +25,7 @@
 import unittest
 
 from gtest_parallel_mocks import LoggerMock
+from gtest_parallel_mocks import PopenMock
 from gtest_parallel_mocks import TestTimesMock
 from gtest_parallel_mocks import TestResultsMock
 from gtest_parallel_mocks import TaskManagerMock
@@ -335,5 +336,140 @@
                                    'Test.case', 1))
 
 
+class TestFindTests(unittest.TestCase):
+  ONE_DISABLED_ONE_ENABLED_TEST = {
+      "fake_unittests": {
+          "FakeTest": {
+              "Test1": None,
+              "DISABLED_Test2": None,
+          }
+      }
+  }
+  ONE_FAILED_ONE_PASSED_TEST = {
+      "fake_unittests": {
+          "FakeTest": {
+              # Failed (and new) tests have no recorded runtime.
+              "FailedTest": None,
+              "Test": 1,
+          }
+      }
+  }
+  ONE_TEST = {
+      "fake_unittests": {
+          "FakeTest": {
+              "TestSomething": None,
+          }
+      }
+  }
+  MULTIPLE_BINARIES_MULTIPLE_TESTS_ONE_FAILURE = {
+      "fake_unittests": {
+          "FakeTest": {
+              "TestSomething": None,
+              "TestSomethingElse": 2,
+          },
+          "SomeOtherTest": {
+              "YetAnotherTest": 3,
+          },
+      },
+      "fake_tests": {
+          "Foo": {
+              "Bar": 4,
+              "Baz": 4,
+          }
+      }
+  }
+
+  def _process_options(self, options):
+    parser = gtest_parallel.default_options_parser()
+    options, binaries = parser.parse_args(options)
+    self.assertEqual(len(binaries), 0)
+    return options
+
+  def _call_find_tests(self, test_data, options=None):
+    popen_mock = PopenMock(test_data)
+    options = self._process_options(options or [])
+    with guard_patch_module('subprocess.Popen', popen_mock):
+      tasks = gtest_parallel.find_tests(
+        test_data.keys(), [], options, TestTimesMock(self, test_data))
+    return tasks, popen_mock
+
+  def test_tasks_are_sorted(self):
+    tasks, _ = self._call_find_tests(
+        self.MULTIPLE_BINARIES_MULTIPLE_TESTS_ONE_FAILURE)
+    self.assertEqual(tasks, sorted(tasks))
+
+  def test_does_not_run_disabled_tests_by_default(self):
+    tasks, popen_mock = self._call_find_tests(
+        self.ONE_DISABLED_ONE_ENABLED_TEST)
+    self.assertEqual(len(tasks), 1)
+    self.assertFalse("DISABLED_" in tasks[0].test_name)
+    self.assertNotIn("--gtest_also_run_disabled_tests",
+                     popen_mock.last_invocation)
+
+  def test_runs_disabled_tests_when_asked(self):
+    tasks, popen_mock = self._call_find_tests(
+        self.ONE_DISABLED_ONE_ENABLED_TEST,
+        ['--gtest_also_run_disabled_tests'])
+    self.assertEqual(len(tasks), 2)
+    self.assertEqual(sorted([task.test_name for task in tasks]),
+                     ["FakeTest.DISABLED_Test2", "FakeTest.Test1"])
+    self.assertIn("--gtest_also_run_disabled_tests",
+                  popen_mock.last_invocation)
+
+  def test_runs_failed_tests_by_default(self):
+    tasks, _ = self._call_find_tests(self.ONE_FAILED_ONE_PASSED_TEST)
+    self.assertEqual(len(tasks), 2)
+    self.assertEqual(sorted([task.test_name for task in tasks]),
+                     ["FakeTest.FailedTest", "FakeTest.Test"])
+    self.assertEqual(sorted([task.last_execution_time for task in tasks]),
+                     [None, 1])
+
+  def test_runs_only_failed_tests_when_asked(self):
+    tasks, _ = self._call_find_tests(
+        self.ONE_FAILED_ONE_PASSED_TEST, ['--failed'])
+    self.assertEqual(len(tasks), 1)
+    self.assertEqual(tasks[0].test_binary, "fake_unittests")
+    self.assertEqual(tasks[0].test_name, "FakeTest.FailedTest")
+    self.assertIsNone(tasks[0].last_execution_time)
+
+  def test_does_not_apply_gtest_filter_by_default(self):
+    _, popen_mock = self._call_find_tests(self.ONE_TEST)
+    self.assertFalse(any(
+        arg.startswith('--gtest_filter=SomeFilter')
+        for arg in popen_mock.last_invocation
+    ))
+  def test_applies_gtest_filter(self):
+    _, popen_mock = self._call_find_tests(
+        self.ONE_TEST, ['--gtest_filter=SomeFilter'])
+    self.assertIn('--gtest_filter=SomeFilter', popen_mock.last_invocation)
+
+  def test_applies_gtest_color_by_default(self):
+    tasks, _ = self._call_find_tests(self.ONE_TEST)
+    self.assertEqual(len(tasks), 1)
+    self.assertIn('--gtest_color=yes', tasks[0].test_command)
+
+  def test_applies_gtest_color(self):
+    tasks, _ = self._call_find_tests(self.ONE_TEST, ['--gtest_color=Lemur'])
+    self.assertEqual(len(tasks), 1)
+    self.assertIn('--gtest_color=Lemur', tasks[0].test_command)
+
+  def test_repeats_tasks_once_by_default(self):
+    tasks, _ = self._call_find_tests(self.ONE_TEST)
+    self.assertEqual(len(tasks), 1)
+
+  def test_repeats_tasks_multiple_times(self):
+    tasks, _ = self._call_find_tests(self.ONE_TEST, ['--repeat=3'])
+    self.assertEqual(len(tasks), 3)
+    # Test all tasks have the same test_name, test_binary and test_command
+    all_tasks_set = set(
+        (task.test_name, task.test_binary, tuple(task.test_command))
+        for task in tasks
+    )
+    self.assertEqual(len(all_tasks_set), 1)
+    # Test tasks have consecutive execution_numbers starting from 1
+    self.assertEqual(sorted(task.execution_number for task in tasks),
+                     [1, 2, 3])
+
+
 if __name__ == '__main__':
   unittest.main()