Add "iterations" option to run a test multiple times.

BUG=None
TEST=goofy_unittest.py, manual on device

Change-Id: I84c0fdd9989272412a68f815dd90d068ef2ba39e
Reviewed-on: https://gerrit.chromium.org/gerrit/27631
Commit-Ready: Jon Salz <jsalz@chromium.org>
Reviewed-by: Jon Salz <jsalz@chromium.org>
Tested-by: Jon Salz <jsalz@chromium.org>
diff --git a/py/goofy/goofy.py b/py/goofy/goofy.py
index 83e2ace..6bc1a14 100755
--- a/py/goofy/goofy.py
+++ b/py/goofy/goofy.py
@@ -609,12 +609,20 @@
               Event.Type.PENDING_SHUTDOWN))
           continue
 
-      invoc = TestInvocation(self, test, on_completion=self.run_next_test)
-      self.invocations[test] = invoc
-      if self.visible_test is None and test.has_ui:
-        self.set_visible_test(test)
-      self.check_connection_manager()
-      invoc.start()
+      self._run_test(test, test.iterations)
+
+  def _run_test(self, test, iterations_left=None):
+    invoc = TestInvocation(self, test, on_completion=self.run_next_test)
+    new_state = test.update_state(
+      status=TestState.ACTIVE, increment_count=1, error_msg='',
+      invocation=invoc.uuid, iterations_left=iterations_left)
+    invoc.count = new_state.count
+
+    self.invocations[test] = invoc
+    if self.visible_test is None and test.has_ui:
+      self.set_visible_test(test)
+    self.check_connection_manager()
+    invoc.start()
 
   def check_connection_manager(self):
     exclusive_tests = [
@@ -705,8 +713,13 @@
     '''
     for t, v in dict(self.invocations).iteritems():
       if v.is_completed():
+        new_state = t.update_state(**v.update_state_on_completion)
         del self.invocations[t]
 
+        if new_state.iterations_left and new_state.status == TestState.PASSED:
+          # Play it again, Sam!
+          self._run_test(t)
+
     if (self.visible_test is None or
         self.visible_test not in self.invocations):
       self.set_visible_test(None)
@@ -733,7 +746,9 @@
       factory.console.info('Killing active test %s...' % test.path)
       invoc.abort_and_join()
       factory.console.info('Killed %s' % test.path)
+      test.update_state(**invoc.update_state_on_completion)
       del self.invocations[test]
+
       if not abort:
         test.update_state(status=TestState.UNTESTED)
     self.reap_completed_tests()
@@ -1200,9 +1215,11 @@
 
     Useful for testing.
     '''
-    for k, v in self.invocations.iteritems():
-      logging.info('Waiting for %s to complete...', k)
-      v.thread.join()
+    while self.invocations:
+      for k, v in self.invocations.iteritems():
+        logging.info('Waiting for %s to complete...', k)
+        v.thread.join()
+      self.reap_completed_tests()
 
   def check_exceptions(self):
     '''Raises an error if any exceptions have occurred in