summaryrefslogtreecommitdiffstats
path: root/chrome/common/extensions
diff options
context:
space:
mode:
authorkalman@chromium.org <kalman@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2013-05-10 17:50:29 +0000
committerkalman@chromium.org <kalman@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2013-05-10 17:50:29 +0000
commit237dd83bf8046c4840ff79254bd371d8fc95568b (patch)
treeb30df85f555feb74131e312bcf65f5563d69a119 /chrome/common/extensions
parentcfde805a760cae184bbd900fc9a2d09c045cebbc (diff)
downloadchromium_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')
-rw-r--r--chrome/common/extensions/docs/server2/PRESUBMIT.py99
-rw-r--r--chrome/common/extensions/docs/server2/app.yaml2
-rw-r--r--chrome/common/extensions/docs/server2/app_yaml_helper.py131
-rwxr-xr-xchrome/common/extensions/docs/server2/app_yaml_helper_test.py176
-rw-r--r--chrome/common/extensions/docs/server2/appengine_wrappers.py11
-rw-r--r--chrome/common/extensions/docs/server2/caching_file_system.py3
-rw-r--r--chrome/common/extensions/docs/server2/cron.yaml8
-rw-r--r--chrome/common/extensions/docs/server2/cron_servlet.py155
-rwxr-xr-xchrome/common/extensions/docs/server2/cron_servlet_test.py167
-rw-r--r--chrome/common/extensions/docs/server2/fake_fetchers.py2
-rw-r--r--chrome/common/extensions/docs/server2/fake_url_fetcher.py2
-rw-r--r--chrome/common/extensions/docs/server2/file_system.py4
-rw-r--r--chrome/common/extensions/docs/server2/instance_servlet.py1
-rw-r--r--chrome/common/extensions/docs/server2/local_file_system.py103
-rw-r--r--chrome/common/extensions/docs/server2/mock_file_system.py75
-rwxr-xr-xchrome/common/extensions/docs/server2/mock_file_system_test.py68
-rw-r--r--chrome/common/extensions/docs/server2/object_store_creator.py1
-rwxr-xr-xchrome/common/extensions/docs/server2/sidenav_data_source_test.py10
-rw-r--r--chrome/common/extensions/docs/server2/subversion_file_system.py26
-rwxr-xr-xchrome/common/extensions/docs/server2/subversion_file_system_test.py36
-rw-r--r--chrome/common/extensions/docs/server2/svn_constants.py2
-rw-r--r--chrome/common/extensions/docs/server2/test_file_system.py1
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