Add option to parallelize full test cases. (#37)

Adds --serialize_test_cases to run tests from the same test case
sequentially. This adds a compromise that permits running binaries where
at least the test cases are independent.

It should also be significantly easier to get binaries into a state
where test cases are fully independent than all individual tests.
diff --git a/README.md b/README.md
index 3a50bce..9ef0051 100644
--- a/README.md
+++ b/README.md
@@ -56,3 +56,16 @@
 as such they have problem writing to hard-coded files, even if they are only
 used by that single test. `tmpfile()` and similar library functions are often
 your friends here.
+
+## Running Tests Within Test Cases Sequentially
+
+Sometimes tests within a single test case use globally-shared resources
+(hard-coded file paths, sockets, etc.) and cannot be run in parallel. Running
+such tests in parallel will either fail or be flaky (if they happen to not
+overlap during execution, they pass). So long as these resources are only shared
+within the same test case `gtest-parallel` can still provide some parallelism.
+
+For such binaries where test cases are independent, `gtest-parallel` provides
+`--serialize_test_cases` that runs tests within the same test case sequentially.
+While generally not providing as much speedup as fully parallel test execution,
+this permits such binaries to partially benefit from parallel execution.
diff --git a/gtest_parallel.py b/gtest_parallel.py
index e9f23d2..a059bf8 100755
--- a/gtest_parallel.py
+++ b/gtest_parallel.py
@@ -458,23 +458,41 @@
   return tasks
 
 
-def execute_tasks(tasks, pool_size, task_manager, timeout):
+def execute_tasks(tasks, pool_size, task_manager,
+                  timeout, serialize_test_cases):
   class WorkerFn(object):
-    def __init__(self, tasks):
-      self.task_id = 0
+    def __init__(self, tasks, running_groups):
       self.tasks = tasks
+      self.running_groups = running_groups
       self.task_lock = threading.Lock()
 
     def __call__(self):
       while True:
         with self.task_lock:
-          if self.task_id < len(self.tasks):
-            task = self.tasks[self.task_id]
-            self.task_id += 1
+          for task_id in range(len(self.tasks)):
+            task = self.tasks[task_id]
+
+            if self.running_groups is not None:
+              test_group = task.test_name.split('.')[0]
+              if test_group in self.running_groups:
+                # Try to find other non-running test group.
+                continue
+              else:
+                self.running_groups.add(test_group)
+
+            del self.tasks[task_id]
+            break
           else:
+            # Either there is no tasks left or number or remaining test
+            # cases (groups) is less than number or running threads.
             return
+
         task_manager.run_task(task)
 
+        if self.running_groups is not None:
+          with self.task_lock:
+            self.running_groups.remove(test_group)
+
   def start_daemon(func):
     t = threading.Thread(target=func)
     t.daemon = True
@@ -484,7 +502,8 @@
   try:
     if timeout:
       timeout.start()
-    worker_fn = WorkerFn(tasks)
+    running_groups = set() if serialize_test_cases else None
+    worker_fn = WorkerFn(tasks, running_groups)
     workers = [start_daemon(worker_fn) for _ in range(pool_size)]
     for worker in workers:
       worker.join()
@@ -539,6 +558,9 @@
   parser.add_option('--timeout', type='int', default=None,
                     help='Interrupt all remaining processes after the given '
                          'time (in seconds).')
+  parser.add_option('--serialize_test_cases', action='store_true',
+                    default=False, help='Do not run tests from the same test '
+                                        'case in parallel.')
 
   (options, binaries) = parser.parse_args()
 
@@ -590,7 +612,8 @@
 
   tasks = find_tests(binaries, additional_args, options, times)
   logger.log_tasks(len(tasks))
-  execute_tasks(tasks, options.workers, task_manager, timeout)
+  execute_tasks(tasks, options.workers, task_manager,
+                timeout, options.serialize_test_cases)
 
   print_try_number = options.retry_failed > 0 or options.repeat > 1
   if task_manager.passed:
diff --git a/gtest_parallel_tests.py b/gtest_parallel_tests.py
index 409a381..127cb5d 100755
--- a/gtest_parallel_tests.py
+++ b/gtest_parallel_tests.py
@@ -16,9 +16,12 @@
 import contextlib
 import gtest_parallel
 import os.path
+import random
 import shutil
 import sys
 import tempfile
+import threading
+import time
 import unittest
 
 
@@ -300,5 +303,77 @@
                          gtest_parallel.get_save_file_path())
 
 
+class TestSerializeTestCases(unittest.TestCase):
+  class TaskManagerMock(object):
+    def __init__(self):
+      self.running_groups = []
+      self.check_lock = threading.Lock()
+
+      self.had_running_parallel_groups = False
+      self.total_tasks_run = 0
+
+    def run_task(self, task):
+      test_group = task.test_name.split('.')[0]
+
+      with self.check_lock:
+        self.total_tasks_run += 1
+        if test_group in self.running_groups:
+          self.had_running_parallel_groups = True
+        self.running_groups.append(test_group)
+
+      # Delay as if real test were run.
+      time.sleep(0.001)
+
+      with self.check_lock:
+        self.running_groups.remove(test_group)
+
+  def _execute_tasks(self, max_number_of_test_cases,
+                     max_number_of_tests_per_test_case,
+                     max_number_of_repeats, max_number_of_workers,
+                     serialize_test_cases):
+    tasks = []
+    for test_case in range(max_number_of_test_cases):
+      for test_name in range(max_number_of_tests_per_test_case):
+        # All arguments for gtest_parallel.Task except for test_name are fake.
+        test_name = 'TestCase{}.test{}'.format(test_case, test_name)
+
+        for execution_number in range(random.randint(1, max_number_of_repeats)):
+          tasks.append(gtest_parallel.Task(
+            'path/to/binary', test_name, ['path/to/binary', '--gtest_filter=*'],
+            execution_number + 1, None, 'path/to/output'))
+
+    expected_tasks_number = len(tasks)
+
+    task_manager = TestSerializeTestCases.TaskManagerMock()
+
+    gtest_parallel.execute_tasks(tasks, max_number_of_workers,
+                                 task_manager, None, serialize_test_cases)
+
+    self.assertEqual(serialize_test_cases,
+                     not task_manager.had_running_parallel_groups)
+    self.assertEqual(expected_tasks_number, task_manager.total_tasks_run)
+
+  def test_running_parallel_test_cases_without_repeats(self):
+    self._execute_tasks(max_number_of_test_cases=4,
+                        max_number_of_tests_per_test_case=32,
+                        max_number_of_repeats=1,
+                        max_number_of_workers=16,
+                        serialize_test_cases=True)
+
+  def test_running_parallel_test_cases_with_repeats(self):
+    self._execute_tasks(max_number_of_test_cases=4,
+                        max_number_of_tests_per_test_case=32,
+                        max_number_of_repeats=4,
+                        max_number_of_workers=16,
+                        serialize_test_cases=True)
+
+  def test_running_parallel_tests(self):
+    self._execute_tasks(max_number_of_test_cases=4,
+                        max_number_of_tests_per_test_case=128,
+                        max_number_of_repeats=1,
+                        max_number_of_workers=16,
+                        serialize_test_cases=False)
+
+
 if __name__ == '__main__':
   unittest.main()