Support mapping multiple local paths under the same url prefix.

This allows setting up development servers that serve mojo apps from the
output directory (native apps) and from the source tree (dart apps in
edit-refresh workflow) alike.

R=qsr@chromium.org

Review URL: https://codereview.chromium.org/1269863004 .

Cr-Mirrored-From: https://github.com/domokit/mojo
Cr-Mirrored-Commit: 1e0809009deb6783afe4997df2b5c0485583c071
diff --git a/devtoolslib/android_shell.py b/devtoolslib/android_shell.py
index 15df9fb..e07ce77 100644
--- a/devtoolslib/android_shell.py
+++ b/devtoolslib/android_shell.py
@@ -375,7 +375,7 @@
   @overrides(Shell)
   def serve_local_directory(self, local_dir_path, port=0):
     assert local_dir_path
-    mappings = [('', local_dir_path)]
+    mappings = [('', [local_dir_path])]
     server_address = start_http_server(mappings, host_port=port)
 
     return 'http://127.0.0.1:%d/' % self._forward_device_port_to_host(
diff --git a/devtoolslib/http_server.py b/devtoolslib/http_server.py
index dc125c8..6afd6a0 100644
--- a/devtoolslib/http_server.py
+++ b/devtoolslib/http_server.py
@@ -53,9 +53,11 @@
   """Creates a handler override for SimpleHTTPServer.
 
   Args:
-    mappings: List of tuples (prefix, local_base_path), mapping path prefixes
-        without the leading slash to local filesystem directory paths. The first
-        matching prefix will be used each time.
+    mappings: List of tuples (prefix, local_base_path_list) mapping URLs that
+        start with |prefix| to one or more local directories enumerated in
+        |local_base_path_list|. The prefixes should skip the leading slash.
+        The first matching prefix and the first location that contains the
+        requested file will be used each time.
   """
   for prefix, _ in mappings:
     assert not prefix.startswith('/'), ('Prefixes for the http server mappings '
@@ -138,23 +140,26 @@
       normalized_path = os.path.relpath(
           SimpleHTTPServer.SimpleHTTPRequestHandler.translate_path(self, path))
 
-      for prefix, local_base_path in mappings:
+      for prefix, local_base_path_list in mappings:
         if normalized_path.startswith(prefix):
-          result = os.path.join(local_base_path, normalized_path[len(prefix):])
-          if os.path.isfile(result):
-            if gzipped:
-              gz_result = result + '.gz'
-              if (not os.path.isfile(gz_result) or
-                  os.path.getmtime(gz_result) <= os.path.getmtime(result)):
-                with open(result, 'rb') as f:
-                  with gzip.open(gz_result, 'wb') as zf:
-                    shutil.copyfileobj(f, zf)
-              result = gz_result
-          return result
-
-      # This class is only used internally, and we're adding a catch-all ''
-      # prefix at the end of |mappings|.
-      assert False
+          for local_base_path in local_base_path_list:
+            candidate = os.path.join(local_base_path,
+                                     normalized_path[len(prefix):])
+            if os.path.isfile(candidate):
+              if gzipped:
+                gz_result = candidate + '.gz'
+                if (not os.path.isfile(gz_result) or
+                    os.path.getmtime(gz_result) <= os.path.getmtime(candidate)):
+                  with open(candidate, 'rb') as f:
+                    with gzip.open(gz_result, 'wb') as zf:
+                      shutil.copyfileobj(f, zf)
+                return gz_result
+              return candidate
+          else:
+            self.send_response(404)
+            return None
+      self.send_response(404)
+      return None
 
     def guess_type(self, path):
       # This is needed so that exploded Sky apps without shebang can still run
@@ -177,10 +182,11 @@
   """Starts an http server serving files from |local_dir_path| on |host_port|.
 
   Args:
-    mappings: List of tuples (prefix, local_base_path) mapping
-        URLs that start with |prefix| to local directory at |local_base_path|.
-        The prefixes should skip the leading slash. The first matching prefix
-        will be used each time.
+    mappings: List of tuples (prefix, local_base_path_list) mapping URLs that
+        start with |prefix| to one or more local directories enumerated in
+        |local_base_path_list|. The prefixes should skip the leading slash.
+        The first matching prefix and the first location that contains the
+        requested file will be used each time.
     host_port: Port on the host machine to run the server on. Pass 0 to use a
         system-assigned port.
 
diff --git a/devtoolslib/http_server_unittest.py b/devtoolslib/http_server_unittest.py
index dec4571..f279712 100644
--- a/devtoolslib/http_server_unittest.py
+++ b/devtoolslib/http_server_unittest.py
@@ -56,8 +56,8 @@
     there.
     """
     mappings = [
-        ('hello/', self.hello_dir),
-        ('other/', self.other_dir),
+        ('hello/', [self.hello_dir]),
+        ('other/', [self.other_dir]),
     ]
     server_address = ('http://%s:%u/' %
                       http_server.start_http_server(mappings))
@@ -72,10 +72,54 @@
                                      other_relpath)
     self.assertEquals(200, other_response.getcode())
 
+  def test_unmapped_path(self):
+    """Verifies that the server returns 404 when a request for unmapped url
+    prefix is made.
+    """
+    mappings = [
+        ('hello/', [self.hello_dir]),
+    ]
+    server_address = ('http://%s:%u/' %
+                      http_server.start_http_server(mappings))
+
+    error_code = None
+    try:
+      urllib2.urlopen(server_address + 'unmapped/abc')
+    except urllib2.HTTPError as error:
+      error_code = error.code
+    self.assertEquals(404, error_code)
+
+  def test_multiple_paths(self):
+    """Verfies mapping multiple local paths under the same url prefix."""
+    mappings = [
+        ('singularity/', [self.hello_dir, self.other_dir]),
+    ]
+    server_address = ('http://%s:%u/' %
+                      http_server.start_http_server(mappings))
+
+    hello_relpath = os.path.relpath(self.hello_file.name, self.hello_dir)
+    hello_response = urllib2.urlopen(server_address + 'singularity/' +
+                                     hello_relpath)
+    self.assertEquals(200, hello_response.getcode())
+
+    other_relpath = os.path.relpath(self.other_file.name, self.other_dir)
+    other_response = urllib2.urlopen(server_address + 'singularity/' +
+                                     other_relpath)
+    self.assertEquals(200, other_response.getcode())
+
+    # Verify that a request for a file not present under any of the mapped
+    # directories results in 404.
+    error_code = None
+    try:
+      urllib2.urlopen(server_address + 'singularity/unavailable')
+    except urllib2.HTTPError as error:
+      error_code = error.code
+    self.assertEquals(404, error_code)
+
   def test_gzip(self):
     """Verifies the gzip content encoding of the files being served."""
     mappings = [
-        ('hello/', self.hello_dir),
+        ('hello/', [self.hello_dir]),
     ]
     server_address = ('http://%s:%u/' %
                       http_server.start_http_server(mappings))
diff --git a/devtoolslib/linux_shell.py b/devtoolslib/linux_shell.py
index d7c3bad..dedee5b 100644
--- a/devtoolslib/linux_shell.py
+++ b/devtoolslib/linux_shell.py
@@ -25,7 +25,7 @@
 
   @overrides(Shell)
   def serve_local_directory(self, local_dir_path, port=0):
-    mappings = [('', local_dir_path)]
+    mappings = [('', [local_dir_path])]
     return 'http://%s:%d/' % http_server.start_http_server(mappings, port)
 
   @overrides(Shell)
diff --git a/devtoolslib/shell.py b/devtoolslib/shell.py
index 0b542a7..c142611 100644
--- a/devtoolslib/shell.py
+++ b/devtoolslib/shell.py
@@ -30,14 +30,15 @@
     call returns immediately.
 
     Args:
-      mappings: List of tuples (prefix, local_base_path) mapping URLs that start
-          with |prefix| to local directory at |local_base_path|. The prefixes
-          should skip the leading slash. The first matching prefix will be used
-          each time.
+      mappings: List of tuples (prefix, local_base_path_list) mapping URLs that
+          start with |prefix| to one or more local directories enumerated in
+          |local_base_path_list|. The prefixes should skip the leading slash.
+          The first matching prefix and the first location that contains the
+          requested file will be used each time.
       port: port at which the server will be available to the shell
 
     Returns:
-      The url that the shell can use to access the content of |local_dir_path|.
+      The url that the shell can use to access the server.
     """
     raise NotImplementedError()