integration_test.py revision 1320f92c476a1ad9d19dba2a48c72b75566198e9
1#!/usr/bin/env python
2# Copyright 2013 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6# Run build_server so that files needed by tests are copied to the local
7# third_party directory.
8import build_server
9build_server.main()
10
11import json
12import optparse
13import os
14import posixpath
15import sys
16import time
17import unittest
18
19from appengine_wrappers import SetTaskRunnerForTest
20from branch_utility import BranchUtility
21from chroot_file_system import ChrootFileSystem
22from extensions_paths import (
23    CONTENT_PROVIDERS, CHROME_EXTENSIONS, PUBLIC_TEMPLATES)
24from fake_fetchers import ConfigureFakeFetchers
25from special_paths import SITE_VERIFICATION_FILE
26from handler import Handler
27from link_error_detector import LinkErrorDetector, StringifyBrokenLinks
28from local_file_system import LocalFileSystem
29from local_renderer import LocalRenderer
30from path_util import AssertIsValid
31from servlet import Request
32from third_party.json_schema_compiler import json_parse
33from test_util import (
34    ChromiumPath, DisableLogging, EnableLogging, ReadFile, Server2Path)
35
36
37# Arguments set up if __main__ specifies them.
38_EXPLICIT_TEST_FILES = None
39_REBASE = False
40_VERBOSE = False
41
42
43def _ToPosixPath(os_path):
44  return os_path.replace(os.sep, '/')
45
46
47def _FilterHidden(paths):
48  '''Returns a list of the non-hidden paths from |paths|.
49  '''
50  # Hidden files start with a '.' but paths like './foo' and '../foo' are not
51  # hidden.
52  return [path for path in paths if (not path.startswith('.')) or
53                                     path.startswith('./') or
54                                     path.startswith('../')]
55
56
57def _GetPublicFiles():
58  '''Gets all public file paths mapped to their contents.
59  '''
60  def walk(path, prefix=''):
61    path = ChromiumPath(path)
62    public_files = {}
63    for root, dirs, files in os.walk(path, topdown=True):
64      relative_root = root[len(path):].lstrip(os.path.sep)
65      dirs[:] = _FilterHidden(dirs)
66      for filename in _FilterHidden(files):
67        with open(os.path.join(root, filename), 'r') as f:
68          request_path = posixpath.join(prefix, relative_root, filename)
69          public_files[request_path] = f.read()
70    return public_files
71
72  # Public file locations are defined in content_providers.json, sort of. Epic
73  # hack to pull them out; list all the files from the directories that
74  # Chromium content providers ask for.
75  public_files = {}
76  content_providers = json_parse.Parse(ReadFile(CONTENT_PROVIDERS))
77  for content_provider in content_providers.itervalues():
78    if 'chromium' in content_provider:
79      public_files.update(walk(content_provider['chromium']['dir'],
80                               prefix=content_provider['serveFrom']))
81  return public_files
82
83
84class IntegrationTest(unittest.TestCase):
85  def setUp(self):
86    ConfigureFakeFetchers()
87
88  @EnableLogging('info')
89  def testCronAndPublicFiles(self):
90    '''Runs cron then requests every public file. Cron needs to be run first
91    because the public file requests are offline.
92    '''
93    if _EXPLICIT_TEST_FILES is not None:
94      return
95
96
97    def task_runner(url, commit=None):
98      arguments = { 'commit': commit } if commit else {}
99      Handler(Request.ForTest(url, arguments=arguments)).Get()
100
101    SetTaskRunnerForTest(task_runner)
102
103    print('Running cron...')
104    start_time = time.time()
105    try:
106      response = Handler(Request.ForTest('/_cron')).Get()
107      if response:
108        self.assertEqual(200, response.status)
109        self.assertEqual('Success', response.content.ToString())
110      else:
111        self.fail('No response for _cron')
112    finally:
113      print('Took %s seconds' % (time.time() - start_time))
114
115    # TODO(kalman): Re-enable this, but it takes about an hour at the moment,
116    # presumably because every page now has a lot of links on it from the
117    # topnav.
118
119    #print("Checking for broken links...")
120    #start_time = time.time()
121    #link_error_detector = LinkErrorDetector(
122    #    # TODO(kalman): Use of ChrootFileSystem here indicates a hack. Fix.
123    #    ChrootFileSystem(LocalFileSystem.Create(), CHROME_EXTENSIONS),
124    #    lambda path: Handler(Request.ForTest(path)).Get(),
125    #    'templates/public',
126    #    ('extensions/index.html', 'apps/about_apps.html'))
127
128    #broken_links = link_error_detector.GetBrokenLinks()
129    #if broken_links:
130    #  print('Found %d broken links.' % (
131    #    len(broken_links)))
132    #  if _VERBOSE:
133    #    print(StringifyBrokenLinks(broken_links))
134
135    #broken_links_set = set(broken_links)
136
137    #known_broken_links_path = os.path.join(
138    #    Server2Path('known_broken_links.json'))
139    #try:
140    #  with open(known_broken_links_path, 'r') as f:
141    #    # The JSON file converts tuples and sets into lists, and for this
142    #    # set union/difference logic they need to be converted back.
143    #    known_broken_links = set(tuple(item) for item in json.load(f))
144    #except IOError:
145    #  known_broken_links = set()
146
147    #newly_broken_links = broken_links_set - known_broken_links
148    #fixed_links = known_broken_links - broken_links_set
149
150    #print('Took %s seconds.' % (time.time() - start_time))
151
152    #print('Searching for orphaned pages...')
153    #start_time = time.time()
154    #orphaned_pages = link_error_detector.GetOrphanedPages()
155    #if orphaned_pages:
156    #  # TODO(jshumway): Test should fail when orphaned pages are detected.
157    #  print('Found %d orphaned pages:' % len(orphaned_pages))
158    #  for page in orphaned_pages:
159    #    print(page)
160    #print('Took %s seconds.' % (time.time() - start_time))
161
162    public_files = _GetPublicFiles()
163
164    print('Rendering %s public files...' % len(public_files.keys()))
165    start_time = time.time()
166    try:
167      for path, content in public_files.iteritems():
168        AssertIsValid(path)
169        if path.endswith('redirects.json'):
170          continue
171
172        # The non-example html and md files are served without their file
173        # extensions.
174        path_without_ext, ext = posixpath.splitext(path)
175        if (ext in ('.html', '.md') and
176            '/examples/' not in path and
177            path != SITE_VERIFICATION_FILE):
178          path = path_without_ext
179
180        def check_result(response):
181          self.assertEqual(200, response.status,
182              'Got %s when rendering %s' % (response.status, path))
183
184          # This is reaaaaally rough since usually these will be tiny templates
185          # that render large files. At least it'll catch zero-length responses.
186          self.assertTrue(len(response.content) >= len(content),
187              'Rendered content length was %s vs template content length %s '
188              'when rendering %s' % (len(response.content), len(content), path))
189
190        check_result(Handler(Request.ForTest(path)).Get())
191
192        if path.startswith(('apps/', 'extensions/')):
193          # Make sure that adding the .html will temporarily redirect to
194          # the path without the .html for APIs and articles.
195          if '/examples/' not in path:
196            redirect_response = Handler(Request.ForTest(path + '.html')).Get()
197            self.assertEqual(
198                ('/' + path, False), redirect_response.GetRedirect(),
199                '%s.html did not (temporarily) redirect to %s (status %s)' %
200                    (path, path, redirect_response.status))
201
202          # Make sure including a channel will permanently redirect to the same
203          # path without a channel.
204          for channel in BranchUtility.GetAllChannelNames():
205            redirect_response = Handler(
206                Request.ForTest(posixpath.join(channel, path))).Get()
207            self.assertEqual(
208                ('/' + path, True),
209                redirect_response.GetRedirect(),
210                '%s/%s did not (permanently) redirect to %s (status %s)' %
211                    (channel, path, path, redirect_response.status))
212
213        # Samples are internationalized, test some locales.
214        if path.endswith('/samples'):
215          for lang in ('en-US', 'es', 'ar'):
216            check_result(Handler(Request.ForTest(
217                path,
218                headers={'Accept-Language': '%s;q=0.8' % lang})).Get())
219    finally:
220      print('Took %s seconds' % (time.time() - start_time))
221
222    #if _REBASE:
223    #  print('Rebasing broken links with %s newly broken and %s fixed links.' %
224    #        (len(newly_broken_links), len(fixed_links)))
225    #  with open(known_broken_links_path, 'w') as f:
226    #    json.dump(broken_links, f,
227    #              indent=2, separators=(',', ': '), sort_keys=True)
228    #else:
229    #  if fixed_links or newly_broken_links:
230    #    print('**********************************************\n'
231    #          'CHANGE DETECTED IN BROKEN LINKS WITHOUT REBASE\n'
232    #          '**********************************************')
233    #    print('Found %s broken links, and some have changed. '
234    #          'If this is acceptable or expected then run %s with the --rebase '
235    #          'option.' % (len(broken_links), os.path.split(__file__)[-1]))
236    #  elif broken_links:
237    #    print('%s existing broken links' % len(broken_links))
238    #  if fixed_links:
239    #    print('%s broken links have been fixed:' % len(fixed_links))
240    #    print(StringifyBrokenLinks(fixed_links))
241    #  if newly_broken_links:
242    #    print('There are %s new broken links:' % len(newly_broken_links))
243    #    print(StringifyBrokenLinks(newly_broken_links))
244    #    self.fail('See logging for details.')
245
246  # TODO(kalman): Move this test elsewhere, it's not an integration test.
247  # Perhaps like "presubmit_tests" or something.
248  def testExplicitFiles(self):
249    '''Tests just the files in _EXPLICIT_TEST_FILES.
250    '''
251    if _EXPLICIT_TEST_FILES is None:
252      return
253    for filename in _EXPLICIT_TEST_FILES:
254      print('Rendering %s...' % filename)
255      start_time = time.time()
256      try:
257        response = LocalRenderer.Render(_ToPosixPath(filename))
258        self.assertEqual(200, response.status)
259        self.assertTrue(response.content != '')
260      finally:
261        print('Took %s seconds' % (time.time() - start_time))
262
263    # TODO(jshumway): Check page for broken links (currently prohibited by the
264    # time it takes to render the pages).
265
266  @DisableLogging('warning')
267  def testFileNotFound(self):
268    response = LocalRenderer.Render('/extensions/notfound')
269    self.assertEqual(404, response.status)
270
271  def testSiteVerificationFile(self):
272    response = LocalRenderer.Render('/' + SITE_VERIFICATION_FILE)
273    self.assertEqual(200, response.status)
274
275if __name__ == '__main__':
276  parser = optparse.OptionParser()
277  parser.add_option('-a', '--all', action='store_true', default=False,
278                    help='Render all pages, not just the one specified')
279  parser.add_option('-r', '--rebase', action='store_true', default=False,
280                    help='Rewrites the known_broken_links.json file with '
281                         'the current set of broken links')
282  parser.add_option('-v', '--verbose', action='store_true', default=False,
283                    help='Show verbose output like currently broken links')
284  (opts, args) = parser.parse_args()
285  if not opts.all:
286    _EXPLICIT_TEST_FILES = args
287  _REBASE = opts.rebase
288  _VERBOSE = opts.verbose
289  # Kill sys.argv because we have our own flags.
290  sys.argv = [sys.argv[0]]
291  unittest.main()
292