diff options
author | kalman@chromium.org <kalman@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-05-10 17:50:29 +0000 |
---|---|---|
committer | kalman@chromium.org <kalman@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-05-10 17:50:29 +0000 |
commit | 237dd83bf8046c4840ff79254bd371d8fc95568b (patch) | |
tree | b30df85f555feb74131e312bcf65f5563d69a119 /chrome/common/extensions | |
parent | cfde805a760cae184bbd900fc9a2d09c045cebbc (diff) | |
download | chromium_src-237dd83bf8046c4840ff79254bd371d8fc95568b.zip chromium_src-237dd83bf8046c4840ff79254bd371d8fc95568b.tar.gz chromium_src-237dd83bf8046c4840ff79254bd371d8fc95568b.tar.bz2 |
Docserver: don't allow updating subversion beyond when the app itself existed.
In other words, if the currently running app is 2-0-1 and a change is pushed to
bring it to 2-0-2, never fetch any new content from then onwards. This will
allow for parallel content + server pushes.
BUG=236600
R=cduvall@chromium.org
Review URL: https://codereview.chromium.org/14247024
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@199503 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome/common/extensions')
22 files changed, 917 insertions, 166 deletions
diff --git a/chrome/common/extensions/docs/server2/PRESUBMIT.py b/chrome/common/extensions/docs/server2/PRESUBMIT.py index dc3db5d..addfeeb 100644 --- a/chrome/common/extensions/docs/server2/PRESUBMIT.py +++ b/chrome/common/extensions/docs/server2/PRESUBMIT.py @@ -12,35 +12,88 @@ for more details about the presubmit API built into gcl. # third_party directory. import os import sys -SYS_PATH = sys.path[:] -try: - SERVER2_PATH = os.path.join('chrome', - 'common', - 'extensions', - 'docs', - 'server2') - if os.sep + 'src' in os.getcwd(): - # Is 'src' is in the path, we can find the server2/ directory from there. - sys.path.insert(0, os.path.join(os.getcwd().rsplit(os.sep + 'src', 1)[0], - 'src', - SERVER2_PATH)) - else: - # Otherwise, we have to guess we're in the server2/ directory. - sys.path.insert(0, '.') - import build_server - build_server.main() -finally: - sys.path = SYS_PATH WHITELIST = [ r'.+_test.py$' ] # The integration tests are selectively run from the PRESUBMIT in # chrome/common/extensions. BLACKLIST = [ r'integration_test.py$' ] +def _BuildServer(input_api): + try: + sys.path.insert(0, input_api.PresubmitLocalPath()) + import build_server + build_server.main() + finally: + sys.path.pop(0) + +def _ImportAppYamlHelper(input_api): + try: + sys.path.insert(0, input_api.PresubmitLocalPath()) + from app_yaml_helper import AppYamlHelper + return AppYamlHelper + finally: + sys.path.pop(0) + +def _WarnIfAppYamlHasntChanged(input_api, output_api): + app_yaml_path = os.path.join(input_api.PresubmitLocalPath(), 'app.yaml') + if app_yaml_path in input_api.AbsoluteLocalPaths(): + return [] + return [output_api.PresubmitPromptOrNotify(''' +************************************************** +CHANGE DETECTED IN SERVER2 WITHOUT APP.YAML UPDATE +************************************************** +Maybe this is ok? Follow this simple guide: + +Q: Does this change any data that might get stored? + * Did you add/remove/update a field to a data source? + * Did you add/remove/update some data that gets sent to templates? + * Is this change to support a new feature in the templates? + * Does this change include changes to templates? +Yes? Bump the middle version, i.e. 2-5-2 -> 2-6-2. + THIS WILL CAUSE THE CURRENTLY RUNNING SERVER TO STOP UPDATING. + PUSH THE NEW VERSION ASAP. +No? Continue. + +Q: Is this a non-trivial change to the server? +Yes? Bump the end version. + Unlike above, the server will *not* stop updating. +No? Are you sure? How much do you bet? This can't be rolled back... + +Q: Is this a spelling correction? New test? Better comments? +Yes? Ok fine. Ignore this warning. +No? I guess this presubmit check doesn't work. +''')] + +def _CheckYamlConsistency(input_api, output_api): + app_yaml_path = os.path.join(input_api.PresubmitLocalPath(), 'app.yaml') + cron_yaml_path = os.path.join(input_api.PresubmitLocalPath(), 'cron.yaml') + if not (app_yaml_path in input_api.AbsoluteLocalPaths() or + cron_yaml_path in input_api.AbsoluteLocalPaths()): + return [] + + AppYamlHelper = _ImportAppYamlHelper(input_api) + app_yaml_version = AppYamlHelper.ExtractVersion( + input_api.ReadFile(app_yaml_path)) + cron_yaml_version = AppYamlHelper.ExtractVersion( + input_api.ReadFile(cron_yaml_path), key='target') + + if app_yaml_version == cron_yaml_version: + return [] + return [output_api.PresubmitError( + 'Versions of app.yaml (%s) and cron.yaml (%s) must match' % ( + app_yaml_version, cron_yaml_version))] + +def _RunPresubmit(input_api, output_api): + _BuildServer(input_api) + return ( + _WarnIfAppYamlHasntChanged(input_api, output_api) + + _CheckYamlConsistency(input_api, output_api) + + input_api.canned_checks.RunUnitTestsInDirectory( + input_api, output_api, '.', whitelist=WHITELIST, blacklist=BLACKLIST) + ) + def CheckChangeOnUpload(input_api, output_api): - return input_api.canned_checks.RunUnitTestsInDirectory( - input_api, output_api, '.', whitelist=WHITELIST, blacklist=BLACKLIST) + return _RunPresubmit(input_api, output_api) def CheckChangeOnCommit(input_api, output_api): - return input_api.canned_checks.RunUnitTestsInDirectory( - input_api, output_api, '.', whitelist=WHITELIST, blacklist=BLACKLIST) + return _RunPresubmit(input_api, output_api) diff --git a/chrome/common/extensions/docs/server2/app.yaml b/chrome/common/extensions/docs/server2/app.yaml index 9e564f9d..3cfff26 100644 --- a/chrome/common/extensions/docs/server2/app.yaml +++ b/chrome/common/extensions/docs/server2/app.yaml @@ -1,5 +1,5 @@ application: chrome-apps-doc -version: 2-0-23 +version: 2-1-0 runtime: python27 api_version: 1 threadsafe: false diff --git a/chrome/common/extensions/docs/server2/app_yaml_helper.py b/chrome/common/extensions/docs/server2/app_yaml_helper.py new file mode 100644 index 0000000..6dcc156 --- /dev/null +++ b/chrome/common/extensions/docs/server2/app_yaml_helper.py @@ -0,0 +1,131 @@ +# Copyright 2013 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +import logging + +_APP_YAML_CONTAINER = ''' +application: chrome-apps-doc +version: %s +runtime: python27 +api_version: 1 +threadsafe: false +''' + +class AppYamlHelper(object): + '''Parses the app.yaml file, and is able to step back in the host file + system's revision history to find when it changed to some given version. + ''' + class Delegate(object): + def GetHostFileSystemForRevision(self, revision): + '''Revision may not be None. + ''' + raise NotImplementedError() + + def __init__(self, + app_yaml_path, + file_system_at_head, + delegate, + object_store_creator): + self._app_yaml_path = app_yaml_path + self._file_system_at_head = file_system_at_head + self._delegate = delegate + self._store = object_store_creator.Create( + AppYamlHelper, + category=file_system_at_head.GetIdentity(), + start_empty=False) + + @staticmethod + def ExtractVersion(app_yaml, key='version'): + '''Extracts the 'version' key from the contents of an app.yaml file. + Allow overriding the key to parse e.g. the cron file ('target'). + ''' + # We could properly parse this using a yaml library but Python doesn't have + # one built in so whatevs. + key_colon = '%s:' % key + versions = [line.strip()[len(key_colon):].strip() + for line in app_yaml.split('\n') + if line.strip().startswith(key_colon)] + if not versions: + raise ValueError('No versions found for %s in %s' % ( + key, app_yaml)) + if len(set(versions)) > 1: + raise ValueError('Inconsistent versions found for %s in %s: %s' % ( + key, app_yaml, versions)) + return versions[0] + + @staticmethod + def IsGreater(lhs, rhs): + '''Return whether the app.yaml version |lhs| > |rhs|. This is tricky + because versions are typically not numbers but rather 2-0-9, 2-0-12, + 2-1-0, etc - and 2-1-0 > 2-0-10 > 2-0-9. + ''' + lhs_parts = lhs.replace('-', '.').split('.') + rhs_parts = rhs.replace('-', '.').split('.') + while lhs_parts and rhs_parts: + lhs_msb = int(lhs_parts.pop(0)) + rhs_msb = int(rhs_parts.pop(0)) + if lhs_msb != rhs_msb: + return lhs_msb > rhs_msb + return len(lhs) > len(rhs) + + @staticmethod + def GenerateAppYaml(version): + '''Probably only useful for tests. + ''' + return _APP_YAML_CONTAINER % version + + def IsUpToDate(self, app_version): + '''Returns True if the |app_version| is up to date with respect to the one + checked into the host file system. + ''' + checked_in_app_version = AppYamlHelper.ExtractVersion( + self._file_system_at_head.ReadSingle(self._app_yaml_path)) + if app_version == checked_in_app_version: + return True + if AppYamlHelper.IsGreater(app_version, checked_in_app_version): + logging.warning( + 'Server is too new! Checked in %s < currently running %s' % ( + checked_in_app_version, app_version)) + return True + return False + + def GetFirstRevisionGreaterThan(self, app_version): + '''Finds the first revision that the version in app.yaml was greater than + |app_version|. + + WARNING: if there is no such revision (e.g. the app is up to date, or + *oops* the app is even newer) then this will throw a ValueError. Use + IsUpToDate to validate the input before calling this method. + ''' + stored = self._store.Get(app_version).Get() + if stored is None: + stored = self._GetFirstRevisionGreaterThanImpl(app_version) + assert stored is not None + self._store.Set(app_version, stored) + return stored + + def _GetFirstRevisionGreaterThanImpl(self, app_version): + def get_app_yaml_revision(file_system): + return int(file_system.Stat(self._app_yaml_path).version) + + def has_greater_app_version(file_system): + app_version_in_file_system = AppYamlHelper.ExtractVersion( + file_system.ReadSingle(self._app_yaml_path)) + return AppYamlHelper.IsGreater(app_version_in_file_system, app_version) + + found = None + next_file_system = self._file_system_at_head + + while has_greater_app_version(next_file_system): + found = get_app_yaml_revision(next_file_system) + # Back up a revision then find when app.yaml was last updated before then. + if found == 0: + logging.warning('All revisions are greater than %s' % app_version) + return 0 + next_file_system = self._delegate.GetHostFileSystemForRevision( + found - 1) + + if found is None: + raise ValueError('All revisions are less than %s' % app_version) + return found diff --git a/chrome/common/extensions/docs/server2/app_yaml_helper_test.py b/chrome/common/extensions/docs/server2/app_yaml_helper_test.py new file mode 100755 index 0000000..019674a --- /dev/null +++ b/chrome/common/extensions/docs/server2/app_yaml_helper_test.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python +# Copyright 2013 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +import unittest + +from app_yaml_helper import AppYamlHelper +from file_system import FileNotFoundError +from mock_file_system import MockFileSystem +from object_store_creator import ObjectStoreCreator +from test_file_system import TestFileSystem +from test_util import DisableLogging + +_ExtractVersion, _IsGreater, _GenerateAppYaml = ( + AppYamlHelper.ExtractVersion, + AppYamlHelper.IsGreater, + AppYamlHelper.GenerateAppYaml) + +class AppYamlHelperTest(unittest.TestCase): + def testExtractVersion(self): + def run_test(version): + self.assertEqual(version, _ExtractVersion(_GenerateAppYaml(version))) + run_test('0') + run_test('0-0') + run_test('0-0-0') + run_test('1') + run_test('1-0') + run_test('1-0-0') + run_test('1-0-1') + run_test('1-1-0') + run_test('1-1-1') + run_test('2-0-9') + run_test('2-0-12') + run_test('2-1') + run_test('2-1-0') + run_test('2-11-0') + run_test('3-1-0') + run_test('3-1-3') + run_test('3-12-0') + + def testIsGreater(self): + def assert_is_greater(lhs, rhs): + self.assertTrue(_IsGreater(lhs, rhs), '%s is not > %s' % (lhs, rhs)) + self.assertFalse(_IsGreater(rhs, lhs), + '%s should not be > %s' % (rhs, lhs)) + assert_is_greater('0-0', '0') + assert_is_greater('0-0-0', '0') + assert_is_greater('0-0-0', '0-0') + assert_is_greater('1', '0') + assert_is_greater('1', '0-0') + assert_is_greater('1', '0-0-0') + assert_is_greater('1-0', '0-0') + assert_is_greater('1-0-0-0', '0-0-0') + assert_is_greater('2-0-12', '2-0-9') + assert_is_greater('2-0-12', '2-0-9-0') + assert_is_greater('2-0-12-0', '2-0-9') + assert_is_greater('2-0-12-0', '2-0-9-0') + assert_is_greater('2-1', '2-0-9') + assert_is_greater('2-1', '2-0-12') + assert_is_greater('2-1-0', '2-0-9') + assert_is_greater('2-1-0', '2-0-12') + assert_is_greater('3-1-0', '2-1') + assert_is_greater('3-1-0', '2-1-0') + assert_is_greater('3-1-0', '2-11-0') + assert_is_greater('3-1-3', '3-1-0') + assert_is_greater('3-12-0', '3-1-0') + assert_is_greater('3-12-0', '3-1-3') + assert_is_greater('3-12-0', '3-1-3-0') + + @DisableLogging('warning') + def testInstanceMethods(self): + test_data = { + 'server2': { + 'app.yaml': _GenerateAppYaml('1-0'), + 'app_yaml_helper.py': 'Copyright notice etc' + } + } + + updates = [] + + file_system_at_head = MockFileSystem(TestFileSystem(test_data)) + + def apply_update(update): + file_system_at_head.Update(update) + updates.append(update) + + assert_true = self.assertTrue + class TestDelegate(AppYamlHelper.Delegate): + def GetHostFileSystemForRevision(self, revision): + assert_true(revision is not None) + assert_true(revision >= 0) + return MockFileSystem.Create(TestFileSystem(test_data), + updates[:revision]) + + helper = AppYamlHelper('server2/app.yaml', + file_system_at_head, + TestDelegate(), + ObjectStoreCreator.ForTest(disable_wrappers=False)) + + def assert_is_up_to_date(version): + self.assertTrue(helper.IsUpToDate(version), + '%s is not up to date' % version) + self.assertRaises(ValueError, + helper.GetFirstRevisionGreaterThan, version) + + self.assertEqual(0, helper.GetFirstRevisionGreaterThan('0-5-0')) + assert_is_up_to_date('1-0-0') + assert_is_up_to_date('1-5-0') + + # Revision 1. + apply_update({'server2': { + 'app.yaml': _GenerateAppYaml('1-5-0') + }}) + + self.assertEqual(0, helper.GetFirstRevisionGreaterThan('0-5-0')) + self.assertEqual(1, helper.GetFirstRevisionGreaterThan('1-0-0')) + assert_is_up_to_date('1-5-0') + assert_is_up_to_date('2-5-0') + + # Revision 2. + apply_update({'server2': { + 'app_yaml_helper.py': 'fixed a bug' + }}) + + self.assertEqual(0, helper.GetFirstRevisionGreaterThan('0-5-0')) + self.assertEqual(1, helper.GetFirstRevisionGreaterThan('1-0-0')) + assert_is_up_to_date('1-5-0') + assert_is_up_to_date('2-5-0') + + # Revision 3. + apply_update({'server2': { + 'app.yaml': _GenerateAppYaml('1-6-0') + }}) + + self.assertEqual(0, helper.GetFirstRevisionGreaterThan('0-5-0')) + self.assertEqual(1, helper.GetFirstRevisionGreaterThan('1-0-0')) + self.assertEqual(3, helper.GetFirstRevisionGreaterThan('1-5-0')) + assert_is_up_to_date('2-5-0') + + # Revision 4. + apply_update({'server2': { + 'app.yaml': _GenerateAppYaml('1-8-0') + }}) + # Revision 5. + apply_update({'server2': { + 'app.yaml': _GenerateAppYaml('2-0-0') + }}) + # Revision 6. + apply_update({'server2': { + 'app.yaml': _GenerateAppYaml('2-2-0') + }}) + # Revision 7. + apply_update({'server2': { + 'app.yaml': _GenerateAppYaml('2-4-0') + }}) + # Revision 8. + apply_update({'server2': { + 'app.yaml': _GenerateAppYaml('2-6-0') + }}) + + self.assertEqual(0, helper.GetFirstRevisionGreaterThan('0-5-0')) + self.assertEqual(1, helper.GetFirstRevisionGreaterThan('1-0-0')) + self.assertEqual(3, helper.GetFirstRevisionGreaterThan('1-5-0')) + self.assertEqual(5, helper.GetFirstRevisionGreaterThan('1-8-0')) + self.assertEqual(6, helper.GetFirstRevisionGreaterThan('2-0-0')) + self.assertEqual(6, helper.GetFirstRevisionGreaterThan('2-1-0')) + self.assertEqual(7, helper.GetFirstRevisionGreaterThan('2-2-0')) + self.assertEqual(7, helper.GetFirstRevisionGreaterThan('2-3-0')) + self.assertEqual(8, helper.GetFirstRevisionGreaterThan('2-4-0')) + self.assertEqual(8, helper.GetFirstRevisionGreaterThan('2-5-0')) + assert_is_up_to_date('2-6-0') + assert_is_up_to_date('2-7-0') + +if __name__ == '__main__': + unittest.main() diff --git a/chrome/common/extensions/docs/server2/appengine_wrappers.py b/chrome/common/extensions/docs/server2/appengine_wrappers.py index 745ec35..86a43e4 100644 --- a/chrome/common/extensions/docs/server2/appengine_wrappers.py +++ b/chrome/common/extensions/docs/server2/appengine_wrappers.py @@ -4,19 +4,16 @@ import os +from app_yaml_helper import AppYamlHelper + def GetAppVersion(): if 'CURRENT_VERSION_ID' in os.environ: # The version ID looks like 2-0-25.36712548, we only want the 2-0-25. return os.environ['CURRENT_VERSION_ID'].split('.', 1)[0] - # Not running on appengine, get it from the app.yaml file ourselves. We - # could properly parse this using a yaml library but Python doesn't have - # one built in so whatevs. - version_key = 'version:' + # Not running on appengine, get it from the app.yaml file ourselves. app_yaml_path = os.path.join(os.path.split(__file__)[0], 'app.yaml') with open(app_yaml_path, 'r') as app_yaml: - version_line = [line for line in app_yaml.read().split('\n') - if line.startswith(version_key)][0] - return version_line[len(version_key):].strip() + return AppYamlHelper.ExtractVersion(app_yaml.read()) def IsDevServer(): return os.environ.get('SERVER_SOFTWARE', '').find('Development') == 0 diff --git a/chrome/common/extensions/docs/server2/caching_file_system.py b/chrome/common/extensions/docs/server2/caching_file_system.py index 05c6282..322258f 100644 --- a/chrome/common/extensions/docs/server2/caching_file_system.py +++ b/chrome/common/extensions/docs/server2/caching_file_system.py @@ -113,3 +113,6 @@ class CachingFileSystem(FileSystem): results, self, read_object_store)) + + def GetIdentity(self): + return self._file_system.GetIdentity() diff --git a/chrome/common/extensions/docs/server2/cron.yaml b/chrome/common/extensions/docs/server2/cron.yaml index b28ad3b..24ab579 100644 --- a/chrome/common/extensions/docs/server2/cron.yaml +++ b/chrome/common/extensions/docs/server2/cron.yaml @@ -2,19 +2,19 @@ cron: - description: Load everything for trunk. url: /_cron/trunk schedule: every 5 minutes - target: 2-0-23 + target: 2-1-0 - description: Load everything for dev. url: /_cron/dev schedule: every 5 minutes - target: 2-0-23 + target: 2-1-0 - description: Load everything for beta. url: /_cron/beta schedule: every 5 minutes - target: 2-0-23 + target: 2-1-0 - description: Load everything for stable. url: /_cron/stable schedule: every 5 minutes - target: 2-0-23 + target: 2-1-0 diff --git a/chrome/common/extensions/docs/server2/cron_servlet.py b/chrome/common/extensions/docs/server2/cron_servlet.py index e76f074..81dac00c 100644 --- a/chrome/common/extensions/docs/server2/cron_servlet.py +++ b/chrome/common/extensions/docs/server2/cron_servlet.py @@ -6,9 +6,12 @@ import logging import time import traceback -from appengine_wrappers import DeadlineExceededError, IsDevServer, logservice +from app_yaml_helper import AppYamlHelper +from appengine_wrappers import ( + GetAppVersion, DeadlineExceededError, IsDevServer, logservice) from branch_utility import BranchUtility from caching_file_system import CachingFileSystem +from empty_dir_file_system import EmptyDirFileSystem from github_file_system import GithubFileSystem from object_store_creator import ObjectStoreCreator from render_servlet import RenderServlet @@ -18,20 +21,6 @@ from subversion_file_system import SubversionFileSystem import svn_constants from third_party.json_schema_compiler.memoize import memoize -def _CreateServerInstanceForChannel(channel, delegate): - object_store_creator = ObjectStoreCreator(channel, start_empty=True) - branch = (delegate.CreateBranchUtility(object_store_creator) - .GetBranchForChannel(channel)) - host_file_system = CachingFileSystem( - delegate.CreateHostFileSystemForBranch(branch), - object_store_creator) - app_samples_file_system = delegate.CreateAppSamplesFileSystem( - object_store_creator) - return ServerInstance(channel, - object_store_creator, - host_file_system, - app_samples_file_system) - class _SingletonRenderServletDelegate(RenderServlet.Delegate): def __init__(self, server_instance): self._server_instance = server_instance @@ -44,16 +33,17 @@ class CronServlet(Servlet): ''' def __init__(self, request, delegate_for_test=None): Servlet.__init__(self, request) + self._channel = request.path.strip('/') self._delegate = delegate_for_test or CronServlet.Delegate() class Delegate(object): - '''Allow runtime dependencies to be overridden for testing. + '''CronServlet's runtime dependencies. Override for testing. ''' def CreateBranchUtility(self, object_store_creator): return BranchUtility.Create(object_store_creator) - def CreateHostFileSystemForBranch(self, branch): - return SubversionFileSystem.Create(branch) + def CreateHostFileSystemForBranchAndRevision(self, branch, revision): + return SubversionFileSystem.Create(branch, revision=revision) def CreateAppSamplesFileSystem(self, object_store_creator): # TODO(kalman): CachingFileSystem wrapper for GithubFileSystem, but it's @@ -61,6 +51,9 @@ class CronServlet(Servlet): return (EmptyDirFileSystem() if IsDevServer() else GithubFileSystem.Create(object_store_creator)) + def GetAppVersion(self): + return GetAppVersion() + def Get(self): # Crons often time out, and when they do *and* then eventually try to # flush logs they die. Turn off autoflush and manually do so at the end. @@ -77,12 +70,12 @@ class CronServlet(Servlet): # the time these won't have changed since the last cron run, so it's a # little wasteful, but hopefully rendering is really fast (if it isn't we # have a problem). - channel = self._request.path.strip('/') + channel = self._channel logging.info('cron/%s: starting' % channel) # This is returned every time RenderServlet wants to create a new # ServerInstance. - server_instance = _CreateServerInstanceForChannel(channel, self._delegate) + server_instance = self._GetSafeServerInstance() def get_via_render_servlet(path): return RenderServlet( @@ -123,18 +116,23 @@ class CronServlet(Servlet): success = True try: # Render all of the publicly accessible files. - for path, path_prefix in ( - # Note: rendering the public templates will pull in all of the private - # templates. - (svn_constants.PUBLIC_TEMPLATE_PATH, ''), - # Note: rendering the public templates will have pulled in the .js - # and manifest.json files (for listing examples on the API reference - # pages), but there are still images, CSS, etc. - (svn_constants.STATIC_PATH, 'static/'), - (svn_constants.EXAMPLES_PATH, 'extensions/examples/')): - # Note: don't try to short circuit any of this stuff. We want to run - # the cron for all the directories regardless of intermediate - # failures. + cron_runs = [ + # Note: rendering the public templates will pull in all of the private + # templates. + (svn_constants.PUBLIC_TEMPLATE_PATH, ''), + # Note: rendering the public templates will have pulled in the .js + # and manifest.json files (for listing examples on the API reference + # pages), but there are still images, CSS, etc. + (svn_constants.STATIC_PATH, 'static/'), + ] + if not IsDevServer(): + cron_runs.append( + (svn_constants.EXAMPLES_PATH, 'extensions/examples/')) + + # Note: don't try to short circuit any of this stuff. We want to run + # the cron for all the directories regardless of intermediate + # failures. + for path, path_prefix in cron_runs: success = run_cron_for_dir(path, path_prefix=path_prefix) and success # TODO(kalman): Generic way for classes to request cron access. The next @@ -143,22 +141,23 @@ class CronServlet(Servlet): # Extension examples have zip files too. Well, so do apps, but the app # file system doesn't get the Offline treatment so they don't need cron. - manifest_json = '/manifest.json' - example_zips = [ - '%s.zip' % filename[:-len(manifest_json)] - for filename in server_instance.content_cache.GetFromFileListing( - svn_constants.EXAMPLES_PATH) - if filename.endswith(manifest_json)] - logging.info('cron/%s: rendering %s example zips...' % ( - channel, len(example_zips))) - start_time = time.time() - try: - success = success and all( - get_via_render_servlet('extensions/examples/%s' % z).status == 200 - for z in example_zips) - finally: - logging.info('cron/%s: rendering %s example zips took %s seconds' % ( - channel, len(example_zips), time.time() - start_time)) + if not IsDevServer(): + manifest_json = '/manifest.json' + example_zips = [ + '%s.zip' % filename[:-len(manifest_json)] + for filename in server_instance.content_cache.GetFromFileListing( + svn_constants.EXAMPLES_PATH) + if filename.endswith(manifest_json)] + logging.info('cron/%s: rendering %s example zips...' % ( + channel, len(example_zips))) + start_time = time.time() + try: + success = success and all( + get_via_render_servlet('extensions/examples/%s' % z).status == 200 + for z in example_zips) + finally: + logging.info('cron/%s: rendering %s example zips took %s seconds' % ( + channel, len(example_zips), time.time() - start_time)) # Also trigger a redirect so that PathCanonicalizer has an opportunity to # cache file listings. @@ -172,3 +171,63 @@ class CronServlet(Servlet): return (Response.Ok('Success') if success else Response.InternalError('Failure')) + + def _GetSafeServerInstance(self): + '''Returns a ServerInstance with a host file system at a safe revision, + meaning the last revision that the current running version of the server + existed. + ''' + channel = self._channel + delegate = self._delegate + + server_instance_at_head = self._CreateServerInstance(channel, None) + + get_branch_for_channel = self._GetBranchForChannel + class AppYamlHelperDelegate(AppYamlHelper.Delegate): + def GetHostFileSystemForRevision(self, revision): + return delegate.CreateHostFileSystemForBranchAndRevision( + get_branch_for_channel(channel), + revision) + + app_yaml_handler = AppYamlHelper( + svn_constants.APP_YAML_PATH, + server_instance_at_head.host_file_system, + AppYamlHelperDelegate(), + server_instance_at_head.object_store_creator) + + if app_yaml_handler.IsUpToDate(delegate.GetAppVersion()): + # TODO(kalman): return a new ServerInstance at an explicit revision in + # case the HEAD version changes underneath us. + return server_instance_at_head + + # The version in app.yaml is greater than the currently running app's. + # The safe version is the one before it changed. + safe_revision = app_yaml_handler.GetFirstRevisionGreaterThan( + delegate.GetAppVersion()) - 1 + + logging.info('cron/%s: app version %s is out of date, safe is %s' % ( + channel, delegate.GetAppVersion(), safe_revision)) + + return self._CreateServerInstance(channel, safe_revision) + + def _CreateObjectStoreCreator(self, channel): + return ObjectStoreCreator(channel, start_empty=True) + + def _GetBranchForChannel(self, channel): + object_store_creator = self._CreateObjectStoreCreator(channel) + return (self._delegate.CreateBranchUtility(object_store_creator) + .GetBranchForChannel(channel)) + + def _CreateServerInstance(self, channel, revision): + object_store_creator = self._CreateObjectStoreCreator(channel) + host_file_system = CachingFileSystem( + self._delegate.CreateHostFileSystemForBranchAndRevision( + self._GetBranchForChannel(channel), + revision), + object_store_creator) + app_samples_file_system = self._delegate.CreateAppSamplesFileSystem( + object_store_creator) + return ServerInstance(channel, + object_store_creator, + host_file_system, + app_samples_file_system) diff --git a/chrome/common/extensions/docs/server2/cron_servlet_test.py b/chrome/common/extensions/docs/server2/cron_servlet_test.py index 16f41b6..0ee14a3 100755 --- a/chrome/common/extensions/docs/server2/cron_servlet_test.py +++ b/chrome/common/extensions/docs/server2/cron_servlet_test.py @@ -5,6 +5,8 @@ import unittest +from appengine_wrappers import GetAppVersion +from app_yaml_helper import AppYamlHelper from cron_servlet import CronServlet from empty_dir_file_system import EmptyDirFileSystem from local_file_system import LocalFileSystem @@ -15,33 +17,44 @@ from test_file_system import TestFileSystem from test_util import EnableLogging # NOTE(kalman): The ObjectStore created by the CronServlet is backed onto our -# fake AppEngine memcache/datastore, so the tests aren't isolated. +# fake AppEngine memcache/datastore, so the tests aren't isolated. Of course, +# if the host file systems have different identities, they will be, sort of. class _TestDelegate(CronServlet.Delegate): - def __init__(self): - self.host_file_systems = [] + def __init__(self, create_file_system): + self.file_systems = [] + # A callback taking a revision and returning a file system. + self._create_file_system = create_file_system + self._app_version = GetAppVersion() def CreateBranchUtility(self, object_store_creator): return TestBranchUtility() - def CreateHostFileSystemForBranch(self, branch): - host_file_system = MockFileSystem(LocalFileSystem.Create()) - self.host_file_systems.append(host_file_system) - return host_file_system + def CreateHostFileSystemForBranchAndRevision(self, branch, revision): + file_system = self._create_file_system(revision) + self.file_systems.append(file_system) + return file_system def CreateAppSamplesFileSystem(self, object_store_creator): return EmptyDirFileSystem() + def GetAppVersion(self): + return self._app_version + + # (non-Delegate method). + def SetAppVersion(self, app_version): + self._app_version = app_version + class CronServletTest(unittest.TestCase): @EnableLogging('info') def testEverything(self): # All these tests are dependent (see above comment) so lump everything in # the one test. - delegate = _TestDelegate() + delegate = _TestDelegate(lambda _: MockFileSystem(LocalFileSystem.Create())) # Test that the cron runs successfully. response = CronServlet(Request.ForTest('trunk'), delegate_for_test=delegate).Get() - self.assertEqual(1, len(delegate.host_file_systems)) + self.assertEqual(1, len(delegate.file_systems)) self.assertEqual(200, response.status) # When re-running, all file systems should be Stat()d the same number of @@ -49,11 +62,141 @@ class CronServletTest(unittest.TestCase): # Stats haven't changed. response = CronServlet(Request.ForTest('trunk'), delegate_for_test=delegate).Get() - self.assertEqual(2, len(delegate.host_file_systems)) - self.assertTrue(*delegate.host_file_systems[1].CheckAndReset( + self.assertEqual(2, len(delegate.file_systems)) + self.assertTrue(*delegate.file_systems[1].CheckAndReset( read_count=0, - stat_count=delegate.host_file_systems[0].GetStatCount())) + stat_count=delegate.file_systems[0].GetStatCount())) + + def testSafeRevision(self): + test_data = { + 'docs': { + 'examples': { + 'examples.txt': 'examples.txt contents' + }, + 'server2': { + 'app.yaml': AppYamlHelper.GenerateAppYaml('2-0-8') + }, + 'static': { + 'static.txt': 'static.txt contents' + }, + 'templates': { + 'public': { + 'apps': { + 'storage.html': 'storage.html contents' + }, + 'extensions': { + 'storage.html': 'storage.html contents' + }, + } + } + } + } + + updates = [] + + def app_yaml_update(version): + return {'docs': {'server2': { + 'app.yaml': AppYamlHelper.GenerateAppYaml(version) + }}} + def storage_html_update(update): + return {'docs': {'templates': {'public': {'apps': { + 'storage.html': update + }}}}} + def static_txt_update(update): + return {'docs': {'static': { + 'static.txt': update + }}} + + app_yaml_path = 'docs/server2/app.yaml' + storage_html_path = 'docs/templates/public/apps/storage.html' + static_txt_path = 'docs/static/static.txt' + + def create_file_system(revision): + '''Creates a MockFileSystem at |revision| by applying that many |updates| + to it. + ''' + mock_file_system = MockFileSystem(TestFileSystem(test_data)) + for update in updates[:revision]: + mock_file_system.Update(update) + return mock_file_system + + delegate = _TestDelegate(create_file_system) + delegate.SetAppVersion('2-0-8') + + file_systems = delegate.file_systems + + # No updates applied yet. + CronServlet(Request.ForTest('trunk'), delegate_for_test=delegate).Get() + self.assertEqual(AppYamlHelper.GenerateAppYaml('2-0-8'), + file_systems[-1].ReadSingle(app_yaml_path)) + self.assertEqual('storage.html contents', + file_systems[-1].ReadSingle(storage_html_path)) + + # Apply updates to storage.html. + updates.append(storage_html_update('interim contents')) + updates.append(storage_html_update('new contents')) + + CronServlet(Request.ForTest('trunk'), delegate_for_test=delegate).Get() + self.assertEqual(AppYamlHelper.GenerateAppYaml('2-0-8'), + file_systems[-1].ReadSingle(app_yaml_path)) + self.assertEqual('new contents', + file_systems[-1].ReadSingle(storage_html_path)) + + # Apply several updates to storage.html and app.yaml. The file system + # should be pinned at the version before app.yaml changed. + updates.append(storage_html_update('stuck here contents')) + + double_update = storage_html_update('newer contents') + double_update.update(app_yaml_update('2-0-10')) + updates.append(double_update) + + updates.append(storage_html_update('never gonna reach here')) + + CronServlet(Request.ForTest('trunk'), delegate_for_test=delegate).Get() + self.assertEqual(AppYamlHelper.GenerateAppYaml('2-0-8'), + file_systems[-1].ReadSingle(app_yaml_path)) + self.assertEqual('stuck here contents', + file_systems[-1].ReadSingle(storage_html_path)) + + # Further pushes to storage.html will keep it pinned. + updates.append(storage_html_update('y u not update!')) + + CronServlet(Request.ForTest('trunk'), delegate_for_test=delegate).Get() + self.assertEqual(AppYamlHelper.GenerateAppYaml('2-0-8'), + file_systems[-1].ReadSingle(app_yaml_path)) + self.assertEqual('stuck here contents', + file_systems[-1].ReadSingle(storage_html_path)) + + # Likewise app.yaml. + updates.append(app_yaml_update('2-1-0')) + + CronServlet(Request.ForTest('trunk'), delegate_for_test=delegate).Get() + self.assertEqual(AppYamlHelper.GenerateAppYaml('2-0-8'), + file_systems[-1].ReadSingle(app_yaml_path)) + self.assertEqual('stuck here contents', + file_systems[-1].ReadSingle(storage_html_path)) + + # And updates to other content won't happen either. + updates.append(static_txt_update('important content!')) + + CronServlet(Request.ForTest('trunk'), delegate_for_test=delegate).Get() + self.assertEqual(AppYamlHelper.GenerateAppYaml('2-0-8'), + file_systems[-1].ReadSingle(app_yaml_path)) + self.assertEqual('stuck here contents', + file_systems[-1].ReadSingle(storage_html_path)) + self.assertEqual('static.txt contents', + file_systems[-1].ReadSingle(static_txt_path)) + # Lastly - when the app version changes, everything should no longer be + # pinned. + delegate.SetAppVersion('2-1-0') + CronServlet(Request.ForTest('trunk'), delegate_for_test=delegate).Get() + self.assertEqual(AppYamlHelper.GenerateAppYaml('2-1-0'), + file_systems[-1].ReadSingle(app_yaml_path)) + self.assertEqual('y u not update!', + file_systems[-1].ReadSingle(storage_html_path)) + self.assertEqual('important content!', + file_systems[-1].ReadSingle(static_txt_path)) if __name__ == '__main__': unittest.main() diff --git a/chrome/common/extensions/docs/server2/fake_fetchers.py b/chrome/common/extensions/docs/server2/fake_fetchers.py index 3a61192..4e66e4a 100644 --- a/chrome/common/extensions/docs/server2/fake_fetchers.py +++ b/chrome/common/extensions/docs/server2/fake_fetchers.py @@ -44,6 +44,7 @@ class FakeSubversionServer(_FakeFetcher): self._base_pattern = re.compile(r'.*chrome/common/extensions/(.*)') def fetch(self, url): + url = url.rsplit('?', 1)[0] path = os.path.join(os.pardir, self._base_pattern.match(url).group(1)) if self._IsDir(path): html = ['<html>Revision 000000'] @@ -70,6 +71,7 @@ class FakeViewvcServer(_FakeFetcher): self._base_pattern = re.compile(r'.*chrome/common/extensions/(.*)') def fetch(self, url): + url = url.rsplit('?', 1)[0] path = os.path.join(os.pardir, self._base_pattern.match(url).group(1)) if self._IsDir(path): html = ['<table><tbody><tr>...</tr>'] diff --git a/chrome/common/extensions/docs/server2/fake_url_fetcher.py b/chrome/common/extensions/docs/server2/fake_url_fetcher.py index ea4f523..b6d943b 100644 --- a/chrome/common/extensions/docs/server2/fake_url_fetcher.py +++ b/chrome/common/extensions/docs/server2/fake_url_fetcher.py @@ -38,9 +38,11 @@ class FakeUrlFetcher(object): return html def FetchAsync(self, url): + url = url.rsplit('?', 1)[0] return Future(value=self.Fetch(url)) def Fetch(self, url): + url = url.rsplit('?', 1)[0] result = _Response() if url.endswith('/'): result.content = self._ListDir(url) diff --git a/chrome/common/extensions/docs/server2/file_system.py b/chrome/common/extensions/docs/server2/file_system.py index ab2248f..72bb599 100644 --- a/chrome/common/extensions/docs/server2/file_system.py +++ b/chrome/common/extensions/docs/server2/file_system.py @@ -16,7 +16,8 @@ class StatInfo(object): self.child_versions = child_versions def __eq__(self, other): - return (self.version == other.version and + return (isinstance(other, StatInfo) and + self.version == other.version and self.child_versions == other.child_versions) def __ne__(self, other): @@ -41,7 +42,6 @@ def ToUnicode(data): class FileSystem(object): '''A FileSystem interface that can read files and directories. ''' - def Read(self, paths, binary=False): '''Reads each file in paths and returns a dictionary mapping the path to the contents. If a path in paths ends with a '/', it is assumed to be a diff --git a/chrome/common/extensions/docs/server2/instance_servlet.py b/chrome/common/extensions/docs/server2/instance_servlet.py index c5c161b..5a091fa 100644 --- a/chrome/common/extensions/docs/server2/instance_servlet.py +++ b/chrome/common/extensions/docs/server2/instance_servlet.py @@ -5,6 +5,7 @@ from appengine_wrappers import IsDevServer from branch_utility import BranchUtility from caching_file_system import CachingFileSystem +from empty_dir_file_system import EmptyDirFileSystem from github_file_system import GithubFileSystem from third_party.json_schema_compiler.memoize import memoize from offline_file_system import OfflineFileSystem diff --git a/chrome/common/extensions/docs/server2/local_file_system.py b/chrome/common/extensions/docs/server2/local_file_system.py index 8a6aae4..2e9667e 100644 --- a/chrome/common/extensions/docs/server2/local_file_system.py +++ b/chrome/common/extensions/docs/server2/local_file_system.py @@ -12,6 +12,54 @@ from future import Future def _ConvertToFilepath(path): return path.replace('/', os.sep) +def _ConvertFromFilepath(path): + return path.replace(os.sep, '/') + +def _ReadFile(filename, binary): + try: + mode = 'rb' if binary else 'r' + with open(filename, mode) as f: + contents = f.read() + if binary: + return contents + return ToUnicode(contents) + except IOError as e: + raise FileNotFoundError('Read failed for %s: %s' % (filename, e)) + +def _ListDir(dir_name): + all_files = [] + try: + files = os.listdir(dir_name) + except OSError as e: + raise FileNotFoundError('os.listdir failed for %s: %s' % (dir_name, e)) + for os_path in files: + posix_path = _ConvertFromFilepath(os_path) + if os_path.startswith('.'): + continue + if os.path.isdir(os.path.join(dir_name, os_path)): + all_files.append(posix_path + '/') + else: + all_files.append(posix_path) + return all_files + +def _CreateStatInfo(path): + try: + path_mtime = os.stat(path).st_mtime + if path.endswith('/'): + child_versions = dict((_ConvertFromFilepath(filename), + os.stat(os.path.join(path, filename)).st_mtime) + for filename in os.listdir(path)) + # This file system stat mimics subversion, where the stat of directories + # is max(file stats). That means we need to recursively check the whole + # file system tree :\ so approximate that by just checking this dir. + version = max([path_mtime] + child_versions.values()) + else: + child_versions = None + version = path_mtime + return StatInfo(version, child_versions) + except OSError as e: + raise FileNotFoundError('os.stat failed for %s: %s' % (path, e)) + class LocalFileSystem(FileSystem): '''FileSystem implementation which fetches resources from the local filesystem. @@ -23,62 +71,21 @@ class LocalFileSystem(FileSystem): def Create(): return LocalFileSystem(os.path.join(sys.path[0], os.pardir, os.pardir)) - def _ReadFile(self, filename, binary): - try: - mode = 'rb' if binary else 'r' - with open(os.path.join(self._base_path, filename), mode) as f: - contents = f.read() - if binary: - return contents - return ToUnicode(contents) - except IOError as e: - raise FileNotFoundError('Read failed for %s: %s' % (filename, e)) - - def _ListDir(self, dir_name): - all_files = [] - full_path = os.path.join(self._base_path, dir_name) - try: - files = os.listdir(full_path) - except OSError as e: - raise FileNotFoundError('os.listdir failed for %s: %s' % (dir_name, e)) - for path in files: - if path.startswith('.'): - continue - if os.path.isdir(os.path.join(full_path, path)): - all_files.append(path + '/') - else: - all_files.append(path) - return all_files - def Read(self, paths, binary=False): result = {} for path in paths: + full_path = os.path.join(self._base_path, + _ConvertToFilepath(path).lstrip(os.sep)) if path.endswith('/'): - result[path] = self._ListDir(_ConvertToFilepath(path)) + result[path] = _ListDir(full_path) else: - result[path] = self._ReadFile(_ConvertToFilepath(path), binary) + result[path] = _ReadFile(full_path, binary) return Future(value=result) - def _CreateStatInfo(self, path): - try: - path_mtime = os.stat(path).st_mtime - if path.endswith('/'): - child_versions = dict( - (filename, os.stat(os.path.join(path, filename)).st_mtime) - for filename in os.listdir(path)) - # This file system stat mimics subversion, where the stat of directories - # is max(file stats). That means we need to recursively check the whole - # file system tree :\ so approximate that by just checking this dir. - version = max([path_mtime] + child_versions.values()) - else: - child_versions = None - version = path_mtime - return StatInfo(version, child_versions) - except OSError as e: - raise FileNotFoundError('os.stat failed for %s: %s' % (path, e)) - def Stat(self, path): - return self._CreateStatInfo(os.path.join(self._base_path, path)) + full_path = os.path.join(self._base_path, + _ConvertToFilepath(path).lstrip(os.sep)) + return _CreateStatInfo(full_path) def GetIdentity(self): return '@'.join((self.__class__.__name__, StringIdentity(self._base_path))) diff --git a/chrome/common/extensions/docs/server2/mock_file_system.py b/chrome/common/extensions/docs/server2/mock_file_system.py index 70e37c1..507d391 100644 --- a/chrome/common/extensions/docs/server2/mock_file_system.py +++ b/chrome/common/extensions/docs/server2/mock_file_system.py @@ -2,29 +2,85 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -from file_system import FileSystem +from file_system import FileSystem, FileNotFoundError +from future import Future +from test_file_system import TestFileSystem class MockFileSystem(FileSystem): - '''Wraps a FileSystem to add simple mock behaviour - asserting how often - Stat/Read calls are being made to it. The Read/Stat implementations - themselves are provided by a delegate FileSystem. + '''Wraps FileSystems to add a selection of mock behaviour: + + - asserting how often Stat/Read calls are being made to it. + - primitive changes/versioning via applying object "diffs", mapping paths to + new content (similar to how TestFileSystem works). ''' def __init__(self, file_system): self._file_system = file_system + # Updates are modelled are stored as TestFileSystems because they've + # implemented a bunch of logic to interpret paths into dictionaries. + self._updates = [] self._read_count = 0 self._stat_count = 0 + @staticmethod + def Create(file_system, updates): + mock_file_system = MockFileSystem(file_system) + for update in updates: + mock_file_system.Update(update) + return mock_file_system + # # FileSystem implementation. # def Read(self, paths, binary=False): + '''Reads |paths| from |_file_system|, then applies the most recent update + from |_updates|, if any. + ''' self._read_count += 1 - return self._file_system.Read(paths, binary=binary) + future_result = self._file_system.Read(paths, binary=binary) + try: + result = future_result.Get() + except: + return future_result + for path in result.iterkeys(): + _, update = self._GetMostRecentUpdate(path) + if update is not None: + result[path] = update + return Future(value=result) + + def _GetMostRecentUpdate(self, path): + for revision, update in reversed(list(enumerate(self._updates))): + try: + return (revision + 1, update.ReadSingle(path)) + except FileNotFoundError: + pass + return (0, None) def Stat(self, path): self._stat_count += 1 - return self._file_system.Stat(path) + return self._StatImpl(path) + + def _StatImpl(self, path): + result = self._file_system.Stat(path) + result.version = self._UpdateStat(result.version, path) + child_versions = result.child_versions + if child_versions is not None: + for child_path in child_versions.iterkeys(): + child_versions[child_path] = self._UpdateStat( + child_versions[child_path], + '%s%s' % (path, child_path)) + return result + + def _UpdateStat(self, version, path): + if not path.endswith('/'): + return str(int(version) + self._GetMostRecentUpdate(path)[0]) + # Bleh, it's a directory, need to recursively search all the children. + child_paths = self._file_system.ReadSingle(path) + if not child_paths: + return version + return str(max([int(version)] + + [int(self._StatImpl('%s%s' % (path, child_path)).version) + for child_path in child_paths])) def GetIdentity(self): return self._file_system.GetIdentity() @@ -33,8 +89,8 @@ class MockFileSystem(FileSystem): return repr(self) def __repr__(self): - return 'MockFileSystem(read_count=%s, stat_count=%s)' % ( - self._read_count, self._stat_count) + return 'MockFileSystem(read_count=%s, stat_count=%s, updates=%s)' % ( + self._read_count, self._stat_count, len(self._updates)) # # Testing methods. @@ -64,3 +120,6 @@ class MockFileSystem(FileSystem): def Reset(self): self._read_count = 0 self._stat_count = 0 + + def Update(self, update): + self._updates.append(TestFileSystem(update)) diff --git a/chrome/common/extensions/docs/server2/mock_file_system_test.py b/chrome/common/extensions/docs/server2/mock_file_system_test.py index 961128b..187462b 100755 --- a/chrome/common/extensions/docs/server2/mock_file_system_test.py +++ b/chrome/common/extensions/docs/server2/mock_file_system_test.py @@ -52,8 +52,9 @@ class MockFileSystemTest(unittest.TestCase): self.assertTrue(*fs.CheckAndReset()) fs.ReadSingle('404.html') - fs.Read(['notfound.html', 'apps/']) + future = fs.Read(['notfound.html', 'apps/']) self.assertTrue(*fs.CheckAndReset(read_count=2)) + self.assertRaises(FileNotFoundError, future.Get) fs.Stat('404.html') fs.Stat('404.html') @@ -68,5 +69,70 @@ class MockFileSystemTest(unittest.TestCase): self.assertTrue(*fs.CheckAndReset(read_count=1, stat_count=2)) self.assertTrue(*fs.CheckAndReset()) + def testUpdates(self): + fs = MockFileSystem(TestFileSystem(deepcopy(_TEST_DATA))) + + self.assertEqual(StatInfo('0', child_versions={ + '404.html': '0', + 'apps/': '0', + 'extensions/': '0' + }), fs.Stat('/')) + self.assertEqual(StatInfo('0'), fs.Stat('404.html')) + self.assertEqual(StatInfo('0', child_versions={ + 'a11y.html': '0', + 'about_apps.html': '0', + 'fakedir/': '0', + }), fs.Stat('apps/')) + self.assertEqual('404.html contents', fs.ReadSingle('404.html')) + + fs.Update({ + '404.html': 'New version!' + }) + + self.assertEqual(StatInfo('1', child_versions={ + '404.html': '1', + 'apps/': '0', + 'extensions/': '0' + }), fs.Stat('/')) + self.assertEqual(StatInfo('1'), fs.Stat('404.html')) + self.assertEqual(StatInfo('0', child_versions={ + 'a11y.html': '0', + 'about_apps.html': '0', + 'fakedir/': '0', + }), fs.Stat('apps/')) + self.assertEqual('New version!', fs.ReadSingle('404.html')) + + fs.Update({ + '404.html': 'Newer version!', + 'apps': { + 'fakedir': { + 'file.html': 'yo' + } + } + }) + + self.assertEqual(StatInfo('2', child_versions={ + '404.html': '2', + 'apps/': '2', + 'extensions/': '0' + }), fs.Stat('/')) + self.assertEqual(StatInfo('2'), fs.Stat('404.html')) + self.assertEqual(StatInfo('2', child_versions={ + 'a11y.html': '0', + 'about_apps.html': '0', + 'fakedir/': '2', + }), fs.Stat('apps/')) + self.assertEqual(StatInfo('0'), fs.Stat('apps/a11y.html')) + self.assertEqual(StatInfo('2', child_versions={ + 'file.html': '2' + }), fs.Stat('apps/fakedir/')) + self.assertEqual(StatInfo('2'), fs.Stat('apps/fakedir/file.html')) + self.assertEqual(StatInfo('0', child_versions={ + 'activeTab.html': '0', + 'alarms.html': '0' + }), fs.Stat('extensions/')) + self.assertEqual('Newer version!', fs.ReadSingle('404.html')) + self.assertEqual('yo', fs.ReadSingle('apps/fakedir/file.html')) + if __name__ == '__main__': unittest.main() diff --git a/chrome/common/extensions/docs/server2/object_store_creator.py b/chrome/common/extensions/docs/server2/object_store_creator.py index 2f797c5..5bf37f6 100644 --- a/chrome/common/extensions/docs/server2/object_store_creator.py +++ b/chrome/common/extensions/docs/server2/object_store_creator.py @@ -19,6 +19,7 @@ class ObjectStoreCreator(object): ''' def __init__(self, channel, + # TODO(kalman): rename start_dirty? start_empty=_unspecified, # Override for testing. A custom ObjectStore type to construct # on Create(). Useful with TestObjectStore, for example. diff --git a/chrome/common/extensions/docs/server2/sidenav_data_source_test.py b/chrome/common/extensions/docs/server2/sidenav_data_source_test.py index 52a1df7..ce5b609 100755 --- a/chrome/common/extensions/docs/server2/sidenav_data_source_test.py +++ b/chrome/common/extensions/docs/server2/sidenav_data_source_test.py @@ -14,11 +14,9 @@ from sidenav_data_source import SidenavDataSource class SamplesDataSourceTest(unittest.TestCase): def setUp(self): - self._base_path = os.path.join(sys.path[0], - 'test_data', - 'sidenav_data_source') + self._json_path = 'docs/server2/test_data/sidenav_data_source' self._compiled_fs_factory = CompiledFileSystem.Factory( - LocalFileSystem(self._base_path), + LocalFileSystem.Create(), ObjectStoreCreator.ForTest()) def _CheckLevels(self, items, level=2): @@ -29,14 +27,14 @@ class SamplesDataSourceTest(unittest.TestCase): def testLevels(self): sidenav_data_source = SidenavDataSource.Factory(self._compiled_fs_factory, - self._base_path).Create('') + self._json_path).Create('') sidenav_json = sidenav_data_source.get('test') self._CheckLevels(sidenav_json) def testSelected(self): sidenav_data_source = SidenavDataSource.Factory( self._compiled_fs_factory, - self._base_path).Create('www.b.com') + self._json_path).Create('www.b.com') sidenav_json = sidenav_data_source.get('test') # This will be prettier once JSON is loaded with an OrderedDict. for item in sidenav_json: diff --git a/chrome/common/extensions/docs/server2/subversion_file_system.py b/chrome/common/extensions/docs/server2/subversion_file_system.py index 41b4d2f..d35a16d 100644 --- a/chrome/common/extensions/docs/server2/subversion_file_system.py +++ b/chrome/common/extensions/docs/server2/subversion_file_system.py @@ -16,9 +16,11 @@ import svn_constants import url_constants class _AsyncFetchFuture(object): - def __init__(self, paths, fetcher, binary): + def __init__(self, paths, fetcher, binary, args=None): + def apply_args(path): + return path if args is None else '%s?%s' % (path, args) # A list of tuples of the form (path, Future). - self._fetches = [(path, fetcher.FetchAsync(path)) + self._fetches = [(path, fetcher.FetchAsync(apply_args(path))) for path in paths] self._value = {} self._error = None @@ -54,7 +56,7 @@ class SubversionFileSystem(FileSystem): '''Class to fetch resources from src.chromium.org. ''' @staticmethod - def Create(branch): + def Create(branch, revision=None): if branch == 'trunk': svn_path = 'trunk/src/%s' % svn_constants.EXTENSIONS_PATH else: @@ -63,17 +65,24 @@ class SubversionFileSystem(FileSystem): return SubversionFileSystem( AppEngineUrlFetcher('%s/%s' % (url_constants.SVN_URL, svn_path)), AppEngineUrlFetcher('%s/%s' % (url_constants.VIEWVC_URL, svn_path)), - svn_path) + svn_path, + revision=revision) - def __init__(self, file_fetcher, stat_fetcher, svn_path): + def __init__(self, file_fetcher, stat_fetcher, svn_path, revision=None): self._file_fetcher = file_fetcher self._stat_fetcher = stat_fetcher self._svn_path = svn_path + self._revision = revision def Read(self, paths, binary=False): + args = None + if self._revision is not None: + # |fetcher| gets from svn.chromium.org which uses p= for version. + args = 'p=%s' % self._revision return Future(delegate=_AsyncFetchFuture(paths, self._file_fetcher, - binary)) + binary, + args=args)) def _ParseHTML(self, html): '''Unfortunately, the viewvc page has a stray </div> tag, so this takes care @@ -142,6 +151,9 @@ class SubversionFileSystem(FileSystem): def Stat(self, path): directory, filename = posixpath.split(path) directory += '/' + if self._revision is not None: + # |stat_fetch| uses viewvc which uses pathrev= for version. + directory += '?pathrev=%s' % self._revision result = self._stat_fetcher.Fetch(directory) if result.status_code == 404: raise FileNotFoundError( @@ -154,4 +166,6 @@ class SubversionFileSystem(FileSystem): return StatInfo(stat_info.child_versions[filename]) def GetIdentity(self): + # NOTE: no revision here, consider it just an implementation detail of the + # file version that is handled by Stat. return '@'.join((self.__class__.__name__, StringIdentity(self._svn_path))) diff --git a/chrome/common/extensions/docs/server2/subversion_file_system_test.py b/chrome/common/extensions/docs/server2/subversion_file_system_test.py index 8fd702f..7281908 100755 --- a/chrome/common/extensions/docs/server2/subversion_file_system_test.py +++ b/chrome/common/extensions/docs/server2/subversion_file_system_test.py @@ -10,6 +10,7 @@ import unittest from fake_url_fetcher import FakeUrlFetcher from file_system import StatInfo +from future import Future from subversion_file_system import SubversionFileSystem class SubversionFileSystemTest(unittest.TestCase): @@ -56,5 +57,40 @@ class SubversionFileSystemTest(unittest.TestCase): stat_info = file_system.Stat('stat/extension_api.h') self.assertEquals(StatInfo('146163'), stat_info) + def testRevisions(self): + # This is a super hacky test. Record the path that was fetched then exit the + # test. Compare. + class ValueErrorFetcher(object): + def __init__(self): + self.last_fetched = None + + def FetchAsync(self, path): + self.last_fetched = path + raise ValueError() + + def Fetch(self, path): + self.last_fetched = path + raise ValueError() + + file_fetcher = ValueErrorFetcher() + stat_fetcher = ValueErrorFetcher() + svn_path = 'svn:' + + svn_file_system = SubversionFileSystem(file_fetcher, + stat_fetcher, + svn_path, + revision=42) + + self.assertRaises(ValueError, svn_file_system.ReadSingle, 'dir/file') + self.assertEqual('dir/file?p=42', file_fetcher.last_fetched) + # Stat() will always stat directories. + self.assertRaises(ValueError, svn_file_system.Stat, 'dir/file') + self.assertEqual('dir/?pathrev=42', stat_fetcher.last_fetched) + + self.assertRaises(ValueError, svn_file_system.ReadSingle, 'dir/') + self.assertEqual('dir/?p=42', file_fetcher.last_fetched) + self.assertRaises(ValueError, svn_file_system.Stat, 'dir/') + self.assertEqual('dir/?pathrev=42', stat_fetcher.last_fetched) + if __name__ == '__main__': unittest.main() diff --git a/chrome/common/extensions/docs/server2/svn_constants.py b/chrome/common/extensions/docs/server2/svn_constants.py index eed66ac..66162f7 100644 --- a/chrome/common/extensions/docs/server2/svn_constants.py +++ b/chrome/common/extensions/docs/server2/svn_constants.py @@ -13,3 +13,5 @@ PRIVATE_TEMPLATE_PATH = TEMPLATE_PATH + '/private' EXAMPLES_PATH = DOCS_PATH + '/examples' JSON_PATH = TEMPLATE_PATH + '/json' STATIC_PATH = DOCS_PATH + '/static' +SERVER2_PATH = DOCS_PATH + '/server2' +APP_YAML_PATH = SERVER2_PATH + '/app.yaml' diff --git a/chrome/common/extensions/docs/server2/test_file_system.py b/chrome/common/extensions/docs/server2/test_file_system.py index 4a638bc..da9d378 100644 --- a/chrome/common/extensions/docs/server2/test_file_system.py +++ b/chrome/common/extensions/docs/server2/test_file_system.py @@ -28,6 +28,7 @@ class TestFileSystem(FileSystem): return result def __init__(self, obj): + assert obj is not None self._obj = obj self._path_stats = {} self._global_stat = 0 |