Devserver unittest wait for start instead of sleep

Check devserver's health to wait for server startup instead of sleeping
a fixed interval while we wait for the devserver to start up.

+ refactoring

BUG=chromium:218020
TEST=devserver_unittest.py

Change-Id: Ie815e18a9350315a13b3839ecfe336561b51d820
Reviewed-on: https://gerrit.chromium.org/gerrit/61688
Commit-Queue: Joy Chen <joychen@chromium.org>
Reviewed-by: Joy Chen <joychen@chromium.org>
Tested-by: Joy Chen <joychen@chromium.org>
diff --git a/devserver_unittest.py b/devserver_unittest.py
index d730316..302d521 100755
--- a/devserver_unittest.py
+++ b/devserver_unittest.py
@@ -50,6 +50,7 @@
 # TODO(girts): use a random available port.
 UPDATE_URL = 'http://127.0.0.1:8080/update'
 STATIC_URL = 'http://127.0.0.1:8080/static/archive/'
+CHECK_HEALTH_URL = 'http://127.0.0.1:8080/check_health'
 
 API_HOST_INFO_BAD_URL = 'http://127.0.0.1:8080/api/hostinfo/'
 API_HOST_INFO_URL = API_HOST_INFO_BAD_URL + '127.0.0.1'
@@ -58,8 +59,8 @@
 API_SET_UPDATE_URL = API_SET_UPDATE_BAD_URL + '127.0.0.1'
 
 API_SET_UPDATE_REQUEST = 'new_update-test/the-new-update'
-DEVSERVER_STARTUP_DELAY = 1
 
+DEVSERVER_START_TIMEOUT = 15
 
 class DevserverTest(unittest.TestCase):
   """Regressions tests for devserver."""
@@ -75,10 +76,12 @@
     self.image_src = os.path.join(self.src_dir, TEST_IMAGE)
     self.image = os.path.join(self.test_data_path, TEST_IMAGE_NAME)
     shutil.copy(self.image_src, self.image)
+    self.devserver_process = self._StartServer()
 
   def tearDown(self):
     """Removes testing files."""
     shutil.rmtree(self.test_data_path)
+    os.kill(self.devserver_process.pid, signal.SIGKILL)
 
   # Helper methods begin here.
 
@@ -92,36 +95,45 @@
         self.test_data_path,
         ]
 
-    process = subprocess.Popen(cmd)
-    # Wait for the server to start up.
-    time.sleep(DEVSERVER_STARTUP_DELAY)
-    return process.pid
+    process = subprocess.Popen(cmd,
+                               stderr=subprocess.PIPE)
+
+    # wait for devserver to start
+    current_time = time.time()
+    deadline = current_time + DEVSERVER_START_TIMEOUT
+    while current_time < deadline:
+      current_time = time.time()
+      try:
+        urllib2.urlopen(CHECK_HEALTH_URL, timeout=0.05)
+        break
+      except Exception:
+        continue
+    else:
+      self.fail('Devserver failed to start within timeout.')
+
+    return process
 
   def VerifyHandleUpdate(self, protocol):
     """Tests running the server and getting an update for the given protocol."""
-    pid = self._StartServer()
-    try:
-      request = urllib2.Request(UPDATE_URL, UPDATE_REQUEST[protocol])
-      connection = urllib2.urlopen(request)
-      response = connection.read()
-      connection.close()
-      self.assertNotEqual('', response)
+    request = urllib2.Request(UPDATE_URL, UPDATE_REQUEST[protocol])
+    connection = urllib2.urlopen(request)
+    response = connection.read()
+    connection.close()
+    self.assertNotEqual('', response)
 
-      # Parse the response and check if it contains the right result.
-      dom = minidom.parseString(response)
-      update = dom.getElementsByTagName('updatecheck')[0]
-      if protocol == '2.0':
-        url = self.VerifyV2Response(update)
-      else:
-        url = self.VerifyV3Response(update)
+    # Parse the response and check if it contains the right result.
+    dom = minidom.parseString(response)
+    update = dom.getElementsByTagName('updatecheck')[0]
+    if protocol == '2.0':
+      url = self.VerifyV2Response(update)
+    else:
+      url = self.VerifyV3Response(update)
 
-      # Try to fetch the image.
-      connection = urllib2.urlopen(url)
-      contents = connection.read()
-      connection.close()
-      self.assertEqual('Developers, developers, developers!\n', contents)
-    finally:
-      os.kill(pid, signal.SIGKILL)
+    # Try to fetch the image.
+    connection = urllib2.urlopen(url)
+    contents = connection.read()
+    connection.close()
+    self.assertEqual('Developers, developers, developers!\n', contents)
 
   def VerifyV2Response(self, update):
     """Verifies the update DOM from a v2 response and returns the url."""
@@ -162,69 +174,53 @@
 
   def testApiBadSetNextUpdateRequest(self):
     """Tests sending a bad setnextupdate request."""
-    pid = self._StartServer()
+    # Send bad request and ensure it fails...
     try:
-      # Send bad request and ensure it fails...
-      try:
-        request = urllib2.Request(API_SET_UPDATE_URL, '')
-        connection = urllib2.urlopen(request)
-        connection.read()
-        connection.close()
-        self.fail('Invalid setnextupdate request did not fail!')
-      except urllib2.URLError:
-        pass
-    finally:
-      os.kill(pid, signal.SIGKILL)
+      request = urllib2.Request(API_SET_UPDATE_URL, '')
+      connection = urllib2.urlopen(request)
+      connection.read()
+      connection.close()
+      self.fail('Invalid setnextupdate request did not fail!')
+    except urllib2.URLError:
+      pass
 
   def testApiBadSetNextUpdateURL(self):
     """Tests contacting a bad setnextupdate url."""
-    pid = self._StartServer()
+    # Send bad request and ensure it fails...
     try:
-      # Send bad request and ensure it fails...
-      try:
-        connection = urllib2.urlopen(API_SET_UPDATE_BAD_URL)
-        connection.read()
-        connection.close()
-        self.fail('Invalid setnextupdate url did not fail!')
-      except urllib2.URLError:
-        pass
-    finally:
-      os.kill(pid, signal.SIGKILL)
+      connection = urllib2.urlopen(API_SET_UPDATE_BAD_URL)
+      connection.read()
+      connection.close()
+      self.fail('Invalid setnextupdate url did not fail!')
+    except urllib2.URLError:
+      pass
 
   def testApiBadHostInfoURL(self):
     """Tests contacting a bad hostinfo url."""
-    pid = self._StartServer()
+    # Send bad request and ensure it fails...
     try:
-      # Send bad request and ensure it fails...
-      try:
-        connection = urllib2.urlopen(API_HOST_INFO_BAD_URL)
-        connection.read()
-        connection.close()
-        self.fail('Invalid hostinfo url did not fail!')
-      except urllib2.URLError:
-        pass
-    finally:
-      os.kill(pid, signal.SIGKILL)
+      connection = urllib2.urlopen(API_HOST_INFO_BAD_URL)
+      connection.read()
+      connection.close()
+      self.fail('Invalid hostinfo url did not fail!')
+    except urllib2.URLError:
+      pass
 
   def testApiHostInfoAndSetNextUpdate(self):
     """Tests using the setnextupdate and hostinfo api commands."""
-    pid = self._StartServer()
-    try:
-      # Send setnextupdate command.
-      request = urllib2.Request(API_SET_UPDATE_URL, API_SET_UPDATE_REQUEST)
-      connection = urllib2.urlopen(request)
-      response = connection.read()
-      connection.close()
+    # Send setnextupdate command.
+    request = urllib2.Request(API_SET_UPDATE_URL, API_SET_UPDATE_REQUEST)
+    connection = urllib2.urlopen(request)
+    response = connection.read()
+    connection.close()
 
-      # Send hostinfo command and verify the setnextupdate worked.
-      connection = urllib2.urlopen(API_HOST_INFO_URL)
-      response = connection.read()
-      connection.close()
+    # Send hostinfo command and verify the setnextupdate worked.
+    connection = urllib2.urlopen(API_HOST_INFO_URL)
+    response = connection.read()
+    connection.close()
 
-      self.assertEqual(
-          json.loads(response)['forced_update_label'], API_SET_UPDATE_REQUEST)
-    finally:
-      os.kill(pid, signal.SIGKILL)
+    self.assertEqual(
+        json.loads(response)['forced_update_label'], API_SET_UPDATE_REQUEST)
 
 
 if __name__ == '__main__':