Home | History | Annotate | Download | only in catapult_build
      1 # Copyright (c) 2015 The Chromium Authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 import argparse
      6 import json
      7 import os
      8 import sys
      9 import urlparse
     10 
     11 from hooks import install
     12 
     13 from paste import fileapp
     14 from paste import httpserver
     15 
     16 import webapp2
     17 from webapp2 import Route, RedirectHandler
     18 
     19 from dashboard_build import dashboard_dev_server_config
     20 from perf_insights_build import perf_insights_dev_server_config
     21 from tracing_build import tracing_dev_server_config
     22 
     23 _MAIN_HTML = """<html><body>
     24 <h1>Run Unit Tests</h1>
     25 <ul>
     26 %s
     27 </ul>
     28 <h1>Quick links</h1>
     29 <ul>
     30 %s
     31 </ul>
     32 </body></html>
     33 """
     34 
     35 _QUICK_LINKS = [
     36     ('Trace File Viewer',
     37      '/tracing_examples/trace_viewer.html'),
     38     ('Perf Insights Viewer',
     39      '/perf_insights_examples/perf_insights_viewer.html')
     40 ]
     41 
     42 _LINK_ITEM = '<li><a href="%s">%s</a></li>'
     43 
     44 def _GetFilesIn(basedir):
     45   data_files = []
     46   for dirpath, dirnames, filenames in os.walk(basedir, followlinks=True):
     47     new_dirnames = [d for d in dirnames if not d.startswith('.')]
     48     del dirnames[:]
     49     dirnames += new_dirnames
     50 
     51     for f in filenames:
     52       if f.startswith('.'):
     53         continue
     54       if f == 'README.md':
     55         continue
     56       full_f = os.path.join(dirpath, f)
     57       rel_f = os.path.relpath(full_f, basedir)
     58       data_files.append(rel_f)
     59 
     60   data_files.sort()
     61   return data_files
     62 
     63 
     64 def _RelPathToUnixPath(p):
     65   return p.replace(os.sep, '/')
     66 
     67 
     68 class TestResultHandler(webapp2.RequestHandler):
     69   def post(self, *args, **kwargs):  # pylint: disable=unused-argument
     70     msg = self.request.body
     71     ostream = sys.stdout if 'PASSED' in msg else sys.stderr
     72     ostream.write(msg + '\n')
     73     return self.response.write('')
     74 
     75 
     76 class TestsCompletedHandler(webapp2.RequestHandler):
     77   def post(self, *args, **kwargs):  # pylint: disable=unused-argument
     78     msg = self.request.body
     79     sys.stdout.write(msg + '\n')
     80     exit_code = 0 if 'ALL_PASSED' in msg else 1
     81     if hasattr(self.app.server, 'please_exit'):
     82       self.app.server.please_exit(exit_code)
     83     return self.response.write('')
     84 
     85 
     86 class DirectoryListingHandler(webapp2.RequestHandler):
     87   def get(self, *args, **kwargs):  # pylint: disable=unused-argument
     88     source_path = kwargs.pop('_source_path', None)
     89     mapped_path = kwargs.pop('_mapped_path', None)
     90     assert mapped_path.endswith('/')
     91 
     92     data_files_relative_to_top = _GetFilesIn(source_path)
     93     data_files = [mapped_path + x
     94                   for x in data_files_relative_to_top]
     95 
     96     files_as_json = json.dumps(data_files)
     97     self.response.content_type = 'application/json'
     98     return self.response.write(files_as_json)
     99 
    100 
    101 class FileAppWithGZipHandling(fileapp.FileApp):
    102   def guess_type(self):
    103     content_type, content_encoding = \
    104         super(FileAppWithGZipHandling, self).guess_type()
    105     if not self.filename.endswith('.gz'):
    106       return content_type, content_encoding
    107     # By default, FileApp serves gzip files as their underlying type with
    108     # Content-Encoding of gzip. That causes them to show up on the client
    109     # decompressed. That ends up being surprising to our xhr.html system.
    110     return None, None
    111 
    112 class SourcePathsHandler(webapp2.RequestHandler):
    113   def get(self, *args, **kwargs):  # pylint: disable=unused-argument
    114     source_paths = kwargs.pop('_source_paths', [])
    115 
    116     path = self.request.path
    117     # This is how we do it. Its... strange, but its what we've done since
    118     # the dawn of time. Aka 4 years ago, lol.
    119     for mapped_path in source_paths:
    120       rel = os.path.relpath(path, '/')
    121       candidate = os.path.join(mapped_path, rel)
    122       if os.path.exists(candidate):
    123         app = FileAppWithGZipHandling(candidate)
    124         app.cache_control(no_cache=True)
    125         return app
    126     self.abort(404)
    127 
    128   @staticmethod
    129   def GetServingPathForAbsFilename(source_paths, filename):
    130     if not os.path.isabs(filename):
    131       raise Exception('filename must be an absolute path')
    132 
    133     for mapped_path in source_paths:
    134       if not filename.startswith(mapped_path):
    135         continue
    136       rel = os.path.relpath(filename, mapped_path)
    137       unix_rel = _RelPathToUnixPath(rel)
    138       return unix_rel
    139     return None
    140 
    141 
    142 class SimpleDirectoryHandler(webapp2.RequestHandler):
    143   def get(self, *args, **kwargs):  # pylint: disable=unused-argument
    144     top_path = os.path.abspath(kwargs.pop('_top_path', None))
    145     if not top_path.endswith(os.path.sep):
    146       top_path += os.path.sep
    147 
    148     joined_path = os.path.abspath(
    149         os.path.join(top_path, kwargs.pop('rest_of_path')))
    150     if not joined_path.startswith(top_path):
    151       self.response.set_status(403)
    152       return
    153     app = FileAppWithGZipHandling(joined_path)
    154     app.cache_control(no_cache=True)
    155     return app
    156 
    157 
    158 class TestOverviewHandler(webapp2.RequestHandler):
    159   def get(self, *args, **kwargs):  # pylint: disable=unused-argument
    160     test_links = []
    161     for name, path in kwargs.pop('pds').iteritems():
    162       test_links.append(_LINK_ITEM % (path, name))
    163     quick_links = []
    164     for name, path in _QUICK_LINKS:
    165       quick_links.append(_LINK_ITEM % (path, name))
    166     self.response.out.write(_MAIN_HTML % ('\n'.join(test_links),
    167                                           '\n'.join(quick_links)))
    168 
    169 class DevServerApp(webapp2.WSGIApplication):
    170   def __init__(self, pds, args):
    171     super(DevServerApp, self).__init__(debug=True)
    172     self.pds = pds
    173     self._server = None
    174     self._all_source_paths = []
    175     self._all_mapped_test_data_paths = []
    176     self._InitFromArgs(args)
    177 
    178   @property
    179   def server(self):
    180     return self._server
    181 
    182   @server.setter
    183   def server(self, server):
    184     self._server = server
    185 
    186   def _InitFromArgs(self, args):
    187     default_tests = dict((pd.GetName(), pd.GetRunUnitTestsUrl())
    188                          for pd in self.pds)
    189     routes = [
    190         Route('/tests.html', TestOverviewHandler,
    191               defaults={'pds': default_tests}),
    192         Route('', RedirectHandler, defaults={'_uri': '/tests.html'}),
    193         Route('/', RedirectHandler, defaults={'_uri': '/tests.html'}),
    194     ]
    195     for pd in self.pds:
    196       routes += pd.GetRoutes(args)
    197       routes += [
    198           Route('/%s/notify_test_result' % pd.GetName(),
    199                 TestResultHandler),
    200           Route('/%s/notify_tests_completed' % pd.GetName(),
    201                 TestsCompletedHandler)
    202       ]
    203 
    204     for pd in self.pds:
    205       # Test data system.
    206       for mapped_path, source_path in pd.GetTestDataPaths(args):
    207         self._all_mapped_test_data_paths.append((mapped_path, source_path))
    208         routes.append(Route('%s__file_list__' % mapped_path,
    209                             DirectoryListingHandler,
    210                             defaults={
    211                                 '_source_path': source_path,
    212                                 '_mapped_path': mapped_path
    213                             }))
    214         routes.append(Route('%s<rest_of_path:.+>' % mapped_path,
    215                             SimpleDirectoryHandler,
    216                             defaults={'_top_path': source_path}))
    217 
    218     # This must go last, because its catch-all.
    219     #
    220     # Its funky that we have to add in the root path. The long term fix is to
    221     # stop with the crazy multi-source-pathing thing.
    222     for pd in self.pds:
    223       self._all_source_paths += pd.GetSourcePaths(args)
    224     routes.append(
    225         Route('/<:.+>', SourcePathsHandler,
    226               defaults={'_source_paths': self._all_source_paths}))
    227 
    228     for route in routes:
    229       self.router.add(route)
    230 
    231   def GetAbsFilenameForHref(self, href):
    232     for source_path in self._all_source_paths:
    233       full_source_path = os.path.abspath(source_path)
    234       expanded_href_path = os.path.abspath(os.path.join(full_source_path,
    235                                                         href.lstrip('/')))
    236       if (os.path.exists(expanded_href_path) and
    237           os.path.commonprefix([full_source_path,
    238                                 expanded_href_path]) == full_source_path):
    239         return expanded_href_path
    240     return None
    241 
    242   def GetURLForAbsFilename(self, filename):
    243     assert self.server is not None
    244     for mapped_path, source_path in self._all_mapped_test_data_paths:
    245       if not filename.startswith(source_path):
    246         continue
    247       rel = os.path.relpath(filename, source_path)
    248       unix_rel = _RelPathToUnixPath(rel)
    249       url = urlparse.urljoin(mapped_path, unix_rel)
    250       return url
    251 
    252     path = SourcePathsHandler.GetServingPathForAbsFilename(
    253         self._all_source_paths, filename)
    254     if path is None:
    255       return None
    256     return urlparse.urljoin('/', path)
    257 
    258 
    259 def _AddPleaseExitMixinToServer(server):
    260   # Shutting down httpserver gracefully and yielding a return code requires
    261   # a bit of mixin code.
    262 
    263   exit_code_attempt = []
    264   def PleaseExit(exit_code):
    265     if len(exit_code_attempt) > 0:
    266       return
    267     exit_code_attempt.append(exit_code)
    268     server.running = False
    269 
    270   real_serve_forever = server.serve_forever
    271 
    272   def ServeForever():
    273     try:
    274       real_serve_forever()
    275     except KeyboardInterrupt:
    276       # allow CTRL+C to shutdown
    277       return 255
    278 
    279     if len(exit_code_attempt) == 1:
    280       return exit_code_attempt[0]
    281     # The serve_forever returned for some reason separate from
    282     # exit_please.
    283     return 0
    284 
    285   server.please_exit = PleaseExit
    286   server.serve_forever = ServeForever
    287 
    288 
    289 def _AddCommandLineArguments(pds, argv):
    290   parser = argparse.ArgumentParser(description='Run development server')
    291   parser.add_argument(
    292       '--no-install-hooks', dest='install_hooks', action='store_false')
    293   parser.add_argument('-p', '--port', default=8003, type=int)
    294   for pd in pds:
    295     g = parser.add_argument_group(pd.GetName())
    296     pd.AddOptionstToArgParseGroup(g)
    297   args = parser.parse_args(args=argv[1:])
    298   return args
    299 
    300 
    301 def Main(argv):
    302   pds = [
    303       dashboard_dev_server_config.DashboardDevServerConfig(),
    304       perf_insights_dev_server_config.PerfInsightsDevServerConfig(),
    305       tracing_dev_server_config.TracingDevServerConfig(),
    306   ]
    307 
    308   args = _AddCommandLineArguments(pds, argv)
    309 
    310   if args.install_hooks:
    311     install.InstallHooks()
    312 
    313   app = DevServerApp(pds, args=args)
    314 
    315   server = httpserver.serve(app, host='127.0.0.1', port=args.port,
    316                             start_loop=False)
    317   _AddPleaseExitMixinToServer(server)
    318   # pylint: disable=no-member
    319   server.urlbase = 'http://127.0.0.1:%i' % server.server_port
    320   app.server = server
    321 
    322   sys.stderr.write('Now running on %s\n' % server.urlbase)
    323 
    324   return server.serve_forever()
    325