summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorevan.peterson.EP@gmail.com <evan.peterson.EP@gmail.com@0039d316-1c4b-4281-b951-d872f2087c98>2013-08-27 23:55:59 +0000
committerevan.peterson.EP@gmail.com <evan.peterson.EP@gmail.com@0039d316-1c4b-4281-b951-d872f2087c98>2013-08-27 23:55:59 +0000
commit657c65c6afcc0473050ffcd72c0f72b591a81e30 (patch)
treeed3db00866d2214aa4f72929ab206e6a33fb8345
parentf4195b9441438b98f232167e50e0086474d7bc50 (diff)
downloadchromium_src-657c65c6afcc0473050ffcd72c0f72b591a81e30.zip
chromium_src-657c65c6afcc0473050ffcd72c0f72b591a81e30.tar.gz
chromium_src-657c65c6afcc0473050ffcd72c0f72b591a81e30.tar.bz2
Adding HostFileSystemIterator class to the Extensions Docserver.
This patch also includes altering existing availability-related logic to take advantage of the new iterator class. Additionally, this should pave the way for object-level availability code to be implemented in a similar way to top-level API availability. BUG=233982 NOTRY=true Review URL: https://chromiumcodereview.appspot.com/23068026 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@219862 0039d316-1c4b-4281-b951-d872f2087c98
-rw-r--r--chrome/common/extensions/docs/server2/api_data_source.py307
-rwxr-xr-xchrome/common/extensions/docs/server2/api_data_source_test.py58
-rw-r--r--chrome/common/extensions/docs/server2/app.yaml2
-rw-r--r--chrome/common/extensions/docs/server2/availability_finder.py300
-rwxr-xr-xchrome/common/extensions/docs/server2/availability_finder_test.py45
-rw-r--r--chrome/common/extensions/docs/server2/branch_utility.py73
-rwxr-xr-xchrome/common/extensions/docs/server2/branch_utility_test.py80
-rw-r--r--chrome/common/extensions/docs/server2/cron.yaml2
-rw-r--r--chrome/common/extensions/docs/server2/host_file_system_iterator.py55
-rwxr-xr-xchrome/common/extensions/docs/server2/host_file_system_iterator_test.py177
-rw-r--r--chrome/common/extensions/docs/server2/server_instance.py18
-rwxr-xr-xchrome/common/extensions/docs/server2/template_data_source_test.py15
-rw-r--r--chrome/common/extensions/docs/server2/test_branch_utility.py39
-rw-r--r--chrome/common/extensions/docs/server2/test_data/canned_data.py129
-rw-r--r--chrome/common/extensions/docs/templates/json/api_availabilities.json4
15 files changed, 854 insertions, 450 deletions
diff --git a/chrome/common/extensions/docs/server2/api_data_source.py b/chrome/common/extensions/docs/server2/api_data_source.py
index eec18e6..3a9e7ef 100644
--- a/chrome/common/extensions/docs/server2/api_data_source.py
+++ b/chrome/common/extensions/docs/server2/api_data_source.py
@@ -3,17 +3,18 @@
# found in the LICENSE file.
import copy
+import json
import logging
import os
from collections import defaultdict, Mapping
-from branch_utility import BranchUtility
import svn_constants
-from third_party.handlebar import Handlebar
import third_party.json_schema_compiler.json_parse as json_parse
import third_party.json_schema_compiler.model as model
import third_party.json_schema_compiler.idl_schema as idl_schema
import third_party.json_schema_compiler.idl_parser as idl_parser
+from third_party.handlebar import Handlebar
+
def _RemoveNoDocs(item):
if json_parse.IsDict(item):
@@ -31,10 +32,11 @@ def _RemoveNoDocs(item):
item.remove(i)
return False
+
def _DetectInlineableTypes(schema):
- """Look for documents that are only referenced once and mark them as inline.
+ '''Look for documents that are only referenced once and mark them as inline.
Actual inlining is done by _InlineDocs.
- """
+ '''
if not schema.get('types'):
return
@@ -57,9 +59,10 @@ def _DetectInlineableTypes(schema):
if refcounts[type_['id']] == 1:
type_['inline_doc'] = True
+
def _InlineDocs(schema):
- """Replace '$ref's that refer to inline_docs with the json for those docs.
- """
+ '''Replace '$ref's that refer to inline_docs with the json for those docs.
+ '''
types = schema.get('types')
if types is None:
return
@@ -91,21 +94,24 @@ def _InlineDocs(schema):
apply_inline(schema)
+
def _CreateId(node, prefix):
if node.parent is not None and not isinstance(node.parent, model.Namespace):
return '-'.join([prefix, node.parent.simple_name, node.simple_name])
return '-'.join([prefix, node.simple_name])
+
def _FormatValue(value):
- """Inserts commas every three digits for integer values. It is magic.
- """
+ '''Inserts commas every three digits for integer values. It is magic.
+ '''
s = str(value)
return ','.join([s[max(0, i - 3):i] for i in range(len(s), 0, -3)][::-1])
+
def _GetAddRulesDefinitionFromEvents(events):
- """Parses the dictionary |events| to find the definition of the method
+ '''Parses the dictionary |events| to find the definition of the method
addRules among functions of the type Event.
- """
+ '''
assert 'types' in events, \
'The dictionary |events| must contain the key "types".'
event_list = [t for t in events['types']
@@ -119,15 +125,18 @@ def _GetAddRulesDefinitionFromEvents(events):
'Exactly one function must be called "addRules".'
return result_list[0]
+
class _JSCModel(object):
- """Uses a Model from the JSON Schema Compiler and generates a dict that
+ '''Uses a Model from the JSON Schema Compiler and generates a dict that
a Handlebar template can use for a data source.
- """
+ '''
+
def __init__(self,
json,
ref_resolver,
disable_refs,
availability_finder,
+ branch_utility,
parse_cache,
template_data_source,
add_rules_schema_function,
@@ -135,6 +144,9 @@ class _JSCModel(object):
self._ref_resolver = ref_resolver
self._disable_refs = disable_refs
self._availability_finder = availability_finder
+ self._branch_utility = branch_utility
+ self._api_availabilities = parse_cache.GetFromFile(
+ '%s/api_availabilities.json' % svn_constants.JSON_PATH)
self._intro_tables = parse_cache.GetFromFile(
'%s/intro_tables.json' % svn_constants.JSON_PATH)
self._api_features = parse_cache.GetFromFile(
@@ -183,127 +195,14 @@ class _JSCModel(object):
(item['name'], item) for item in as_dict[item_type])
return as_dict
- def _GetIntroTableList(self):
- """Create a generic data structure that can be traversed by the templates
- to create an API intro table.
- """
- intro_rows = [
- self._GetIntroDescriptionRow(),
- self._GetIntroAvailabilityRow()
- ] + self._GetIntroDependencyRows()
-
- # Add rows using data from intro_tables.json, overriding any existing rows
- # if they share the same 'title' attribute.
- row_titles = [row['title'] for row in intro_rows]
- for misc_row in self._GetMiscIntroRows():
- if misc_row['title'] in row_titles:
- intro_rows[row_titles.index(misc_row['title'])] = misc_row
- else:
- intro_rows.append(misc_row)
-
- return intro_rows
-
- def _GetIntroDescriptionRow(self):
- """ Generates the 'Description' row data for an API intro table.
- """
- return {
- 'title': 'Description',
- 'content': [
- { 'text': self._FormatDescription(self._namespace.description) }
- ]
- }
-
- def _GetIntroAvailabilityRow(self):
- """ Generates the 'Availability' row data for an API intro table.
- """
- if self._IsExperimental():
- status = 'experimental'
- version = None
- else:
- availability = self._GetApiAvailability()
- status = availability.channel
- version = availability.version
- return {
- 'title': 'Availability',
- 'content': [{
- 'partial': self._template_data_source.get(
- 'intro_tables/%s_message.html' % status),
- 'version': version
- }]
- }
-
- def _GetIntroDependencyRows(self):
- # Devtools aren't in _api_features. If we're dealing with devtools, bail.
- if 'devtools' in self._namespace.name:
- return []
- feature = self._api_features.get(self._namespace.name)
- assert feature, ('"%s" not found in _api_features.json.'
- % self._namespace.name)
-
- dependencies = feature.get('dependencies')
- if dependencies is None:
- return []
-
- def make_code_node(text):
- return { 'class': 'code', 'text': text }
-
- permissions_content = []
- manifest_content = []
-
- def categorize_dependency(dependency):
- context, name = dependency.split(':', 1)
- if context == 'permission':
- permissions_content.append(make_code_node('"%s"' % name))
- elif context == 'manifest':
- manifest_content.append(make_code_node('"%s": {...}' % name))
- elif context == 'api':
- transitive_dependencies = (
- self._api_features.get(name, {}).get('dependencies', []))
- for transitive_dependency in transitive_dependencies:
- categorize_dependency(transitive_dependency)
- else:
- raise ValueError('Unrecognized dependency for %s: %s' % (
- self._namespace.name, context))
-
- for dependency in dependencies:
- categorize_dependency(dependency)
-
- dependency_rows = []
- if permissions_content:
- dependency_rows.append({
- 'title': 'Permissions',
- 'content': permissions_content
- })
- if manifest_content:
- dependency_rows.append({
- 'title': 'Manifest',
- 'content': manifest_content
- })
- return dependency_rows
-
- def _GetMiscIntroRows(self):
- """ Generates miscellaneous intro table row data, such as 'Permissions',
- 'Samples', and 'Learn More', using intro_tables.json.
- """
- misc_rows = []
- # Look up the API name in intro_tables.json, which is structured
- # similarly to the data structure being created. If the name is found, loop
- # through the attributes and add them to this structure.
- table_info = self._intro_tables.get(self._namespace.name)
- if table_info is None:
- return misc_rows
-
- for category in table_info.keys():
- content = copy.deepcopy(table_info[category])
- for node in content:
- # If there is a 'partial' argument and it hasn't already been
- # converted to a Handlebar object, transform it to a template.
- if 'partial' in node:
- node['partial'] = self._template_data_source.get(node['partial'])
- misc_rows.append({ 'title': category, 'content': content })
- return misc_rows
-
def _GetApiAvailability(self):
+ # Check for a predetermined availability for this API.
+ api_info = self._api_availabilities.get(self._namespace.name)
+ if api_info is not None:
+ channel = api_info['channel']
+ if channel == 'stable':
+ return self._branch_utility.GetStableChannelInfo(api_info['version'])
+ return self._branch_utility.GetChannelInfo(channel)
return self._availability_finder.GetApiAvailability(self._namespace.name)
def _GetChannelWarning(self):
@@ -499,10 +398,132 @@ class _JSCModel(object):
else:
dst_dict['simple_type'] = type_.property_type.name.lower()
+ def _GetIntroTableList(self):
+ '''Create a generic data structure that can be traversed by the templates
+ to create an API intro table.
+ '''
+ intro_rows = [
+ self._GetIntroDescriptionRow(),
+ self._GetIntroAvailabilityRow()
+ ] + self._GetIntroDependencyRows()
+
+ # Add rows using data from intro_tables.json, overriding any existing rows
+ # if they share the same 'title' attribute.
+ row_titles = [row['title'] for row in intro_rows]
+ for misc_row in self._GetMiscIntroRows():
+ if misc_row['title'] in row_titles:
+ intro_rows[row_titles.index(misc_row['title'])] = misc_row
+ else:
+ intro_rows.append(misc_row)
+
+ return intro_rows
+
+ def _GetIntroDescriptionRow(self):
+ ''' Generates the 'Description' row data for an API intro table.
+ '''
+ return {
+ 'title': 'Description',
+ 'content': [
+ { 'text': self._FormatDescription(self._namespace.description) }
+ ]
+ }
+
+ def _GetIntroAvailabilityRow(self):
+ ''' Generates the 'Availability' row data for an API intro table.
+ '''
+ if self._IsExperimental():
+ status = 'experimental'
+ version = None
+ else:
+ availability = self._GetApiAvailability()
+ status = availability.channel
+ version = availability.version
+ return {
+ 'title': 'Availability',
+ 'content': [{
+ 'partial': self._template_data_source.get(
+ 'intro_tables/%s_message.html' % status),
+ 'version': version
+ }]
+ }
+
+ def _GetIntroDependencyRows(self):
+ # Devtools aren't in _api_features. If we're dealing with devtools, bail.
+ if 'devtools' in self._namespace.name:
+ return []
+ feature = self._api_features.get(self._namespace.name)
+ assert feature, ('"%s" not found in _api_features.json.'
+ % self._namespace.name)
+
+ dependencies = feature.get('dependencies')
+ if dependencies is None:
+ return []
+
+ def make_code_node(text):
+ return { 'class': 'code', 'text': text }
+
+ permissions_content = []
+ manifest_content = []
+
+ def categorize_dependency(dependency):
+ context, name = dependency.split(':', 1)
+ if context == 'permission':
+ permissions_content.append(make_code_node('"%s"' % name))
+ elif context == 'manifest':
+ manifest_content.append(make_code_node('"%s": {...}' % name))
+ elif context == 'api':
+ transitive_dependencies = (
+ self._api_features.get(name, {}).get('dependencies', []))
+ for transitive_dependency in transitive_dependencies:
+ categorize_dependency(transitive_dependency)
+ else:
+ raise ValueError('Unrecognized dependency for %s: %s' % (
+ self._namespace.name, context))
+
+ for dependency in dependencies:
+ categorize_dependency(dependency)
+
+ dependency_rows = []
+ if permissions_content:
+ dependency_rows.append({
+ 'title': 'Permissions',
+ 'content': permissions_content
+ })
+ if manifest_content:
+ dependency_rows.append({
+ 'title': 'Manifest',
+ 'content': manifest_content
+ })
+ return dependency_rows
+
+ def _GetMiscIntroRows(self):
+ ''' Generates miscellaneous intro table row data, such as 'Permissions',
+ 'Samples', and 'Learn More', using intro_tables.json.
+ '''
+ misc_rows = []
+ # Look up the API name in intro_tables.json, which is structured
+ # similarly to the data structure being created. If the name is found, loop
+ # through the attributes and add them to this structure.
+ table_info = self._intro_tables.get(self._namespace.name)
+ if table_info is None:
+ return misc_rows
+
+ for category in table_info.keys():
+ content = copy.deepcopy(table_info[category])
+ for node in content:
+ # If there is a 'partial' argument and it hasn't already been
+ # converted to a Handlebar object, transform it to a template.
+ if 'partial' in node:
+ node['partial'] = self._template_data_source.get(node['partial'])
+ misc_rows.append({ 'title': category, 'content': content })
+ return misc_rows
+
+
class _LazySamplesGetter(object):
- """This class is needed so that an extensions API page does not have to fetch
+ '''This class is needed so that an extensions API page does not have to fetch
the apps samples page and vice versa.
- """
+ '''
+
def __init__(self, api_name, samples):
self._api_name = api_name
self._samples = samples
@@ -510,15 +531,18 @@ class _LazySamplesGetter(object):
def get(self, key):
return self._samples.FilterSamples(key, self._api_name)
+
class APIDataSource(object):
- """This class fetches and loads JSON APIs from the FileSystem passed in with
+ '''This class fetches and loads JSON APIs from the FileSystem passed in with
|compiled_fs_factory|, so the APIs can be plugged into templates.
- """
+ '''
+
class Factory(object):
def __init__(self,
compiled_fs_factory,
base_path,
- availability_finder_factory):
+ availability_finder,
+ branch_utility):
def create_compiled_fs(fn, category):
return compiled_fs_factory.Create(fn, APIDataSource, category=category)
@@ -543,7 +567,8 @@ class APIDataSource(object):
self._names_cache = create_compiled_fs(self._GetAllNames, 'names')
self._base_path = base_path
- self._availability_finder = availability_finder_factory.Create()
+ self._availability_finder = availability_finder
+ self._branch_utility = branch_utility
self._parse_cache = create_compiled_fs(
lambda _, json: json_parse.Parse(json),
'intro-cache')
@@ -565,10 +590,10 @@ class APIDataSource(object):
self._template_data_source = template_data_source_factory.Create(None, '')
def Create(self, request, disable_refs=False):
- """Create an APIDataSource. |disable_refs| specifies whether $ref's in
+ '''Create an APIDataSource. |disable_refs| specifies whether $ref's in
APIs being processed by the |ToDict| method of _JSCModel follows $ref's
in the API. This prevents endless recursion in ReferenceResolver.
- """
+ '''
if self._samples_data_source_factory is None:
# Only error if there is a request, which means this APIDataSource is
# actually being used to render a page.
@@ -606,6 +631,7 @@ class APIDataSource(object):
self._ref_resolver_factory.Create() if not disable_refs else None,
disable_refs,
self._availability_finder,
+ self._branch_utility,
self._parse_cache,
self._template_data_source,
self._LoadAddRulesSchema).ToDict()
@@ -617,6 +643,7 @@ class APIDataSource(object):
self._ref_resolver_factory.Create() if not disable_refs else None,
disable_refs,
self._availability_finder,
+ self._branch_utility,
self._parse_cache,
self._template_data_source,
self._LoadAddRulesSchema,
diff --git a/chrome/common/extensions/docs/server2/api_data_source_test.py b/chrome/common/extensions/docs/server2/api_data_source_test.py
index 8f452b9b..9864fb7 100755
--- a/chrome/common/extensions/docs/server2/api_data_source_test.py
+++ b/chrome/common/extensions/docs/server2/api_data_source_test.py
@@ -15,37 +15,42 @@ from api_data_source import (_JSCModel,
_DetectInlineableTypes,
_InlineDocs,
_GetAddRulesDefinitionFromEvents)
+from branch_utility import ChannelInfo
from collections import namedtuple
from compiled_file_system import CompiledFileSystem
from file_system import FileNotFoundError
from object_store_creator import ObjectStoreCreator
from reference_resolver import ReferenceResolver
+from test_branch_utility import TestBranchUtility
from test_data.canned_data import CANNED_TEST_FILE_SYSTEM_DATA
from test_file_system import TestFileSystem
import third_party.json_schema_compiler.json_parse as json_parse
+
def _MakeLink(href, text):
return '<a href="%s">%s</a>' % (href, text)
+
def _GetType(dict_, name):
for type_ in dict_['types']:
if type_['name'] == name:
return type_
+
class FakeAvailabilityFinder(object):
- AvailabilityInfo = namedtuple('AvailabilityInfo', 'channel version')
def GetApiAvailability(self, version):
- return FakeAvailabilityFinder.AvailabilityInfo('trunk', 'trunk')
+ return ChannelInfo('stable', 396, 5)
- def StringifyAvailability(self, availability):
- return availability.channel
class FakeSamplesDataSource(object):
+
def Create(self, request):
return {}
+
class FakeAPIAndListDataSource(object):
+
def __init__(self, json_data):
self._json = json_data
@@ -60,11 +65,15 @@ class FakeAPIAndListDataSource(object):
def GetAllNames(self):
return self._json.keys()
+
class FakeTemplateDataSource(object):
+
def get(self, key):
return 'handlebar %s' % key
+
class APIDataSourceTest(unittest.TestCase):
+
def setUp(self):
self._base_path = os.path.join(sys.path[0], 'test_data', 'test_json')
self._compiled_fs_factory = CompiledFileSystem.Factory(
@@ -96,6 +105,7 @@ class APIDataSourceTest(unittest.TestCase):
self._CreateRefResolver('test_file_data_source.json'),
False,
FakeAvailabilityFinder(),
+ TestBranchUtility.CreateWithCannedData(),
self._json_cache,
FakeTemplateDataSource(),
None).ToDict()
@@ -115,6 +125,7 @@ class APIDataSourceTest(unittest.TestCase):
self._CreateRefResolver('test_file_data_source.json'),
False,
FakeAvailabilityFinder(),
+ TestBranchUtility.CreateWithCannedData(),
self._json_cache,
FakeTemplateDataSource(),
None).ToDict()
@@ -130,6 +141,7 @@ class APIDataSourceTest(unittest.TestCase):
self._CreateRefResolver('ref_test_data_source.json'),
False,
FakeAvailabilityFinder(),
+ TestBranchUtility.CreateWithCannedData(),
self._json_cache,
FakeTemplateDataSource(),
None).ToDict()
@@ -149,11 +161,44 @@ class APIDataSourceTest(unittest.TestCase):
_RemoveNoDocs(d)
self.assertEquals(self._LoadJSON('expected_nodoc.json'), d)
+ def testGetApiAvailability(self):
+ model = _JSCModel(self._LoadJSON('test_file.json')[0],
+ self._CreateRefResolver('test_file_data_source.json'),
+ False,
+ FakeAvailabilityFinder(),
+ TestBranchUtility.CreateWithCannedData(),
+ self._json_cache,
+ FakeTemplateDataSource(),
+ None)
+ # The model namespace is "tester". No predetermined availability is found,
+ # so the FakeAvailabilityFinder instance is used to find availability.
+ self.assertEqual(ChannelInfo('stable', 396, 5),
+ model._GetApiAvailability())
+
+ # These APIs have predetermined availabilities in the
+ # api_availabilities.json file within CANNED_DATA.
+ model._namespace.name = 'trunk_api'
+ self.assertEqual(ChannelInfo('trunk', 'trunk', 'trunk'),
+ model._GetApiAvailability())
+
+ model._namespace.name = 'dev_api'
+ self.assertEqual(ChannelInfo('dev', 1500, 28),
+ model._GetApiAvailability())
+
+ model._namespace.name = 'beta_api'
+ self.assertEqual(ChannelInfo('beta', 1453, 27),
+ model._GetApiAvailability())
+
+ model._namespace.name = 'stable_api'
+ self.assertEqual(ChannelInfo('stable', 1132, 20),
+ model._GetApiAvailability())
+
def testGetIntroList(self):
model = _JSCModel(self._LoadJSON('test_file.json')[0],
self._CreateRefResolver('test_file_data_source.json'),
False,
FakeAvailabilityFinder(),
+ TestBranchUtility.CreateWithCannedData(),
self._json_cache,
FakeTemplateDataSource(),
None)
@@ -165,8 +210,8 @@ class APIDataSourceTest(unittest.TestCase):
},
{ 'title': 'Availability',
'content': [
- { 'partial': 'handlebar intro_tables/trunk_message.html',
- 'version': 'trunk'
+ { 'partial': 'handlebar intro_tables/stable_message.html',
+ 'version': 5
}
]
},
@@ -327,6 +372,7 @@ class APIDataSourceTest(unittest.TestCase):
self._CreateRefResolver('test_file_data_source.json'),
False,
FakeAvailabilityFinder(),
+ TestBranchUtility.CreateWithCannedData(),
self._json_cache,
FakeTemplateDataSource(),
self._FakeLoadAddRulesSchema).ToDict()
diff --git a/chrome/common/extensions/docs/server2/app.yaml b/chrome/common/extensions/docs/server2/app.yaml
index c134739..2fe805b 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-27-1
+version: 2-27-2
runtime: python27
api_version: 1
threadsafe: false
diff --git a/chrome/common/extensions/docs/server2/availability_finder.py b/chrome/common/extensions/docs/server2/availability_finder.py
index a0b6c45..b4ab31a 100644
--- a/chrome/common/extensions/docs/server2/availability_finder.py
+++ b/chrome/common/extensions/docs/server2/availability_finder.py
@@ -5,24 +5,14 @@
import collections
import os
+import svn_constants
from branch_utility import BranchUtility
from compiled_file_system import CompiledFileSystem
from file_system import FileNotFoundError
-import svn_constants
-from third_party.json_schema_compiler import json_parse, model
+from third_party.json_schema_compiler import json_parse
from third_party.json_schema_compiler.memoize import memoize
+from third_party.json_schema_compiler.model import UnixName
-_API_AVAILABILITIES = svn_constants.JSON_PATH + '/api_availabilities.json'
-_API_FEATURES = svn_constants.API_PATH + '/_api_features.json'
-_EXTENSION_API = svn_constants.API_PATH + '/extension_api.json'
-_MANIFEST_FEATURES = svn_constants.API_PATH + '/_manifest_features.json'
-_PERMISSION_FEATURES = svn_constants.API_PATH + '/_permission_features.json'
-_STABLE = 'stable'
-
-class AvailabilityInfo(object):
- def __init__(self, channel, version):
- self.channel = channel
- self.version = version
def _GetChannelFromFeatures(api_name, file_system, path):
'''Finds API channel information within _features.json files at the given
@@ -40,29 +30,27 @@ def _GetChannelFromFeatures(api_name, file_system, path):
# purposes. Take the newest channel out of all of the entries.
return BranchUtility.NewestChannel(entry.get('channel') for entry in feature)
+
def _GetChannelFromApiFeatures(api_name, file_system):
- try:
- return _GetChannelFromFeatures(api_name, file_system, _API_FEATURES)
- except FileNotFoundError:
- # TODO(epeterson) Remove except block once _api_features is in all channels.
- return None
+ return _GetChannelFromFeatures(
+ api_name,
+ file_system,
+ '%s/_api_features.json' % svn_constants.API_PATH)
-def _GetChannelFromPermissionFeatures(api_name, file_system):
- return _GetChannelFromFeatures(api_name, file_system, _PERMISSION_FEATURES)
def _GetChannelFromManifestFeatures(api_name, file_system):
- return _GetChannelFromFeatures(#_manifest_features uses unix_style API names
- model.UnixName(api_name),
- file_system,
- _MANIFEST_FEATURES)
+ return _GetChannelFromFeatures(
+ UnixName(api_name), #_manifest_features uses unix_style API names
+ file_system,
+ '%s/_manifest_features.json' % svn_constants.API_PATH)
+
+
+def _GetChannelFromPermissionFeatures(api_name, file_system):
+ return _GetChannelFromFeatures(
+ api_name,
+ file_system,
+ '%s/_permission_features.json' % svn_constants.API_PATH)
-def _ExistsInFileSystem(api_name, file_system):
- '''Checks for existence of |api_name| within the list of files in the api/
- directory found using the given file system.
- '''
- file_names = file_system.GetFromFileListing(svn_constants.API_PATH)
- # File names switch from unix_hacker_style to camelCase at versions <= 20.
- return model.UnixName(api_name) in file_names or api_name in file_names
def _ExistsInExtensionApi(api_name, file_system):
'''Parses the api/extension_api.json file (available in Chrome versions
@@ -70,195 +58,135 @@ def _ExistsInExtensionApi(api_name, file_system):
is considered to have been 'stable' for the given version.
'''
try:
- extension_api_json = file_system.GetFromFile(_EXTENSION_API)
+ extension_api_json = file_system.GetFromFile(
+ '%s/extension_api.json' % svn_constants.API_PATH)
api_rows = [row.get('namespace') for row in extension_api_json
if 'namespace' in row]
- return True if api_name in api_rows else False
+ return api_name in api_rows
except FileNotFoundError:
# This should only happen on preview.py since extension_api.json is no
# longer present in trunk.
return False
+
class AvailabilityFinder(object):
- '''Uses API data sources generated by a ChromeVersionDataSource in order to
- search the filesystem for the earliest existence of a specified API throughout
- the different versions of Chrome; this constitutes an API's availability.
+ '''Generates availability information for APIs by looking at API schemas and
+ _features files over multiple release versions of Chrome.
'''
- class Factory(object):
- def __init__(self,
- object_store_creator,
- compiled_host_fs_factory,
- branch_utility,
- host_file_system_creator):
- self._object_store_creator = object_store_creator
- self._compiled_host_fs_factory = compiled_host_fs_factory
- self._branch_utility = branch_utility
- self._host_file_system_creator = host_file_system_creator
-
- def Create(self):
- return AvailabilityFinder(self._object_store_creator,
- self._compiled_host_fs_factory,
- self._branch_utility,
- self._host_file_system_creator)
def __init__(self,
+ file_system_iterator,
object_store_creator,
- compiled_host_fs_factory,
- branch_utility,
- host_file_system_creator):
+ branch_utility):
+ self._file_system_iterator = file_system_iterator
self._object_store_creator = object_store_creator
- self._json_cache = compiled_host_fs_factory.Create(
- lambda _, json: json_parse.Parse(json),
- AvailabilityFinder,
- 'json-cache')
+ self._object_store = self._object_store_creator.Create(AvailabilityFinder)
self._branch_utility = branch_utility
- self._host_file_system_creator = host_file_system_creator
- self._object_store = object_store_creator.Create(AvailabilityFinder)
- @memoize
- def _CreateFeaturesAndNamesFileSystems(self, version):
- '''The 'features' compiled file system's populate function parses and
- returns the contents of a _features.json file. The 'names' compiled file
- system's populate function creates a list of file names with .json or .idl
- extensions.
+ def _ExistsInFileSystem(self, api_name, file_system):
+ '''Checks for existence of |api_name| within the list of api files in the
+ api/ directory found using the given |file_system|.
+ '''
+ file_names = file_system.ReadSingle('%s/' % svn_constants.API_PATH)
+ api_names = tuple(os.path.splitext(name)[0] for name in file_names
+ if os.path.splitext(name)[1][1:] in ['json', 'idl'])
+
+ # API file names in api/ are unix_name at every version except for versions
+ # 18, 19, and 20. Since unix_name is the more common format, check it first.
+ return (UnixName(api_name) in api_names) or (api_name in api_names)
+
+ def _CheckStableAvailability(self, api_name, file_system, version):
+ '''Checks for availability of an API, |api_name|, on the stable channel.
+ Considers several _features.json files, file system existence, and
+ extension_api.json depending on the given |version|.
'''
- fs_factory = CompiledFileSystem.Factory(
- self._host_file_system_creator.Create(
- self._branch_utility.GetBranchForVersion(version)),
- self._object_store_creator)
+ if version < 5:
+ # SVN data isn't available below version 5.
+ return False
+ available_channel = None
+ fs_factory = CompiledFileSystem.Factory(file_system,
+ self._object_store_creator)
features_fs = fs_factory.Create(lambda _, json: json_parse.Parse(json),
AvailabilityFinder,
category='features')
- names_fs = fs_factory.Create(self._GetExtNames,
- AvailabilityFinder,
- category='names')
- return (features_fs, names_fs)
-
- def _GetExtNames(self, base_path, apis):
- return [os.path.splitext(api)[0] for api in apis
- if os.path.splitext(api)[1][1:] in ['json', 'idl']]
-
- def _FindEarliestStableAvailability(self, api_name, version):
- '''Searches in descending order through filesystem caches tied to specific
- chrome version numbers and looks for the availability of an API, |api_name|,
- on the stable channel. When a version is found where the API is no longer
- available on stable, returns the previous version number (the last known
- version where the API was stable).
- '''
- available = True
- while available:
- if version < 5:
- # SVN data isn't available below version 5.
- return version + 1
- available = False
- available_channel = None
- features_fs, names_fs = self._CreateFeaturesAndNamesFileSystems(version)
- if version >= 28:
- # The _api_features.json file first appears in version 28 and should be
- # the most reliable for finding API availabilities, so it gets checked
- # first. The _permission_features.json and _manifest_features.json files
- # are present in Chrome 20 and onwards. Fall back to a check for file
- # system existence if the API is not stable in any of the _features.json
- # files.
- available_channel = _GetChannelFromApiFeatures(api_name, features_fs)
- if version >= 20:
- # Check other _features.json files/file existence if the API wasn't
- # found in _api_features.json, or if _api_features.json wasn't present.
- available_channel = available_channel or (
- _GetChannelFromPermissionFeatures(api_name, features_fs)
- or _GetChannelFromManifestFeatures(api_name, features_fs))
- if available_channel is None:
- available = _ExistsInFileSystem(api_name, names_fs)
- else:
- available = available_channel == _STABLE
- elif version >= 18:
- # These versions are a little troublesome. Version 19 has
- # _permission_features.json, but it lacks 'channel' information.
- # Version 18 lacks all of the _features.json files. For now, we're using
- # a simple check for filesystem existence here.
- available = _ExistsInFileSystem(api_name, names_fs)
- elif version >= 5:
- # Versions 17 and down to 5 have an extension_api.json file which
- # contains namespaces for each API that was available at the time. We
- # can use this file to check for API existence.
- available = _ExistsInExtensionApi(api_name, features_fs)
-
- if not available:
- return version + 1
- version -= 1
-
- def _GetAvailableChannelForVersion(self, api_name, version):
- '''Searches through the _features files for a given |version| and returns
- the channel that the given API is determined to be available on.
+ if version >= 28:
+ # The _api_features.json file first appears in version 28 and should be
+ # the most reliable for finding API availability.
+ available_channel = _GetChannelFromApiFeatures(api_name, features_fs)
+ if version >= 20:
+ # The _permission_features.json and _manifest_features.json files are
+ # present in Chrome 20 and onwards. Use these if no information could be
+ # found using _api_features.json.
+ available_channel = available_channel or (
+ _GetChannelFromPermissionFeatures(api_name, features_fs)
+ or _GetChannelFromManifestFeatures(api_name, features_fs))
+ if available_channel is not None:
+ return available_channel == 'stable'
+ if version >= 18:
+ # Fall back to a check for file system existence if the API is not
+ # stable in any of the _features.json files, OR if we're dealing with
+ # version 18 or 19, which don't contain relevant _features information.
+ return self._ExistsInFileSystem(api_name, file_system)
+ if version >= 5:
+ # Versions 17 down to 5 have an extension_api.json file which
+ # contains namespaces for each API that was available at the time.
+ return _ExistsInExtensionApi(api_name, features_fs)
+
+ def _CheckChannelAvailability(self, api_name, file_system, channel_name):
+ '''Searches through the _features files in a given |file_system| and
+ determines whether or not an API is available on the given channel,
+ |channel_name|.
'''
- features_fs, names_fs = self._CreateFeaturesAndNamesFileSystems(version)
+ fs_factory = CompiledFileSystem.Factory(file_system,
+ self._object_store_creator)
+ features_fs = fs_factory.Create(lambda _, json: json_parse.Parse(json),
+ AvailabilityFinder,
+ category='features')
available_channel = (_GetChannelFromApiFeatures(api_name, features_fs)
or _GetChannelFromPermissionFeatures(api_name, features_fs)
or _GetChannelFromManifestFeatures(api_name, features_fs))
- if available_channel is None and _ExistsInFileSystem(api_name, names_fs):
+ if (available_channel is None and
+ self._ExistsInFileSystem(api_name, file_system)):
# If an API is not represented in any of the _features files, but exists
# in the filesystem, then assume it is available in this version.
# The windows API is an example of this.
- return self._branch_utility.GetChannelForVersion(version)
-
- return available_channel
+ available_channel = channel_name
+ # If the channel we're checking is the same as or newer than the
+ # |available_channel| then the API is available at this channel.
+ return (available_channel is not None and
+ BranchUtility.NewestChannel((available_channel, channel_name))
+ == channel_name)
+
+ def _CheckApiAvailability(self, api_name, file_system, channel_info):
+ '''Determines the availability for an API at a certain version of Chrome.
+ Two branches of logic are used depending on whether or not the API is
+ determined to be 'stable' at the given version.
+ '''
+ if channel_info.channel == 'stable':
+ return self._CheckStableAvailability(api_name,
+ file_system,
+ channel_info.version)
+ return self._CheckChannelAvailability(api_name,
+ file_system,
+ channel_info.channel)
def GetApiAvailability(self, api_name):
- '''Determines the availability for an API by testing several scenarios.
- (i.e. Is the API experimental? Only available on certain development
- channels? If it's stable, when did it first become stable? etc.)
+ '''Performs a search for an API's top-level availability by using a
+ HostFileSystemIterator instance to traverse multiple version of the
+ SVN filesystem.
'''
availability = self._object_store.Get(api_name).Get()
if availability is not None:
return availability
- # Check for a predetermined availability for this API.
- api_info = self._json_cache.GetFromFile(_API_AVAILABILITIES).get(api_name)
- if api_info is not None:
- channel = api_info.get('channel')
- if channel == _STABLE:
- version = api_info.get('version')
- else:
- version = self._branch_utility.GetChannelInfo(channel).version
- # The file data for predetermined availabilities is already cached, so
- # skip caching this result.
- return AvailabilityInfo(channel, version)
-
- # Check for the API in the development channels.
- availability = None
-
- for channel_info in self._branch_utility.GetAllChannelInfo():
- if channel_info.channel == 'trunk':
- # Don't check trunk, since (a) there's no point, we know it's going to
- # be available there, and (b) there is a bug with the current
- # architecture and design of HostFileSystemCreator, where creating
- # 'trunk' ignores the pinned revision (in fact, it bypasses every
- # difference including whether the file system is patched).
- # TODO(kalman): Fix HostFileSystemCreator and update this comment.
- break
-
- available_channel = self._GetAvailableChannelForVersion(
- api_name,
- channel_info.version)
- # If the |available_channel| for the API is the same as, or older than,
- # the channel we're checking, then the API is available on this channel.
- if (available_channel is not None and
- BranchUtility.NewestChannel((available_channel, channel_info.channel))
- == channel_info.channel):
- availability = AvailabilityInfo(channel_info.channel,
- channel_info.version)
- break
+ def check_api_availability(file_system, channel_info):
+ return self._CheckApiAvailability(api_name, file_system, channel_info)
+ availability = self._file_system_iterator.Descending(
+ self._branch_utility.GetChannelInfo('dev'),
+ check_api_availability)
if availability is None:
- trunk_info = self._branch_utility.GetChannelInfo('trunk')
- availability = AvailabilityInfo(trunk_info.channel, trunk_info.version)
-
- # If the API is in stable, find the chrome version in which it became
- # stable.
- if availability.channel == _STABLE:
- availability.version = self._FindEarliestStableAvailability(
- api_name,
- availability.version)
-
+ # The API wasn't available on 'dev', so it must be a 'trunk'-only API.
+ availability = self._branch_utility.GetChannelInfo('trunk')
self._object_store.Set(api_name, availability)
return availability
diff --git a/chrome/common/extensions/docs/server2/availability_finder_test.py b/chrome/common/extensions/docs/server2/availability_finder_test.py
index 25cfd14..83ab7c7 100755
--- a/chrome/common/extensions/docs/server2/availability_finder_test.py
+++ b/chrome/common/extensions/docs/server2/availability_finder_test.py
@@ -11,28 +11,34 @@ from availability_finder import AvailabilityFinder
from branch_utility import BranchUtility
from compiled_file_system import CompiledFileSystem
from fake_url_fetcher import FakeUrlFetcher
+from host_file_system_iterator import HostFileSystemIterator
from object_store_creator import ObjectStoreCreator
from test_file_system import TestFileSystem
from test_data.canned_data import (CANNED_API_FILE_SYSTEM_DATA, CANNED_BRANCHES)
+
class FakeHostFileSystemCreator(object):
+
def Create(self, branch):
return TestFileSystem(CANNED_API_FILE_SYSTEM_DATA[str(branch)])
+
class AvailabilityFinderTest(unittest.TestCase):
+
def setUp(self):
- self._avail_finder_factory = AvailabilityFinder.Factory(
- ObjectStoreCreator.ForTest(),
- CompiledFileSystem.Factory(
- TestFileSystem(CANNED_API_FILE_SYSTEM_DATA['trunk']),
- ObjectStoreCreator.ForTest()),
- BranchUtility(
- os.path.join('branch_utility', 'first.json'),
- os.path.join('branch_utility', 'second.json'),
- FakeUrlFetcher(os.path.join(sys.path[0], 'test_data')),
- ObjectStoreCreator.ForTest()),
- FakeHostFileSystemCreator())
- self._avail_finder = self._avail_finder_factory.Create()
+ branch_utility = BranchUtility(
+ os.path.join('branch_utility', 'first.json'),
+ os.path.join('branch_utility', 'second.json'),
+ FakeUrlFetcher(os.path.join(sys.path[0], 'test_data')),
+ ObjectStoreCreator.ForTest())
+ fake_host_file_system_creator = FakeHostFileSystemCreator()
+ file_system_iterator = HostFileSystemIterator(
+ fake_host_file_system_creator,
+ fake_host_file_system_creator.Create('trunk'),
+ branch_utility)
+ self._avail_finder = AvailabilityFinder(file_system_iterator,
+ ObjectStoreCreator.ForTest(),
+ branch_utility)
def testGetApiAvailability(self):
# Key: Using 'channel' (i.e. 'beta') to represent an availability listing
@@ -40,21 +46,6 @@ class AvailabilityFinderTest(unittest.TestCase):
# represent the development channel, or phase of development, where an API's
# availability is being checked.
- # Testing the predetermined APIs found in
- # templates/json/api_availabilities.json.
- self.assertEqual('stable',
- self._avail_finder.GetApiAvailability('jsonAPI1').channel)
- self.assertEqual(10,
- self._avail_finder.GetApiAvailability('jsonAPI1').version)
- self.assertEqual('trunk',
- self._avail_finder.GetApiAvailability('jsonAPI2').channel)
- self.assertEqual('trunk',
- self._avail_finder.GetApiAvailability('jsonAPI2').version)
- self.assertEqual('dev',
- self._avail_finder.GetApiAvailability('jsonAPI3').channel)
- self.assertEqual(28,
- self._avail_finder.GetApiAvailability('jsonAPI3').version)
-
# Testing whitelisted API
self.assertEquals('beta',
self._avail_finder.GetApiAvailability('declarativeWebRequest').channel)
diff --git a/chrome/common/extensions/docs/server2/branch_utility.py b/chrome/common/extensions/docs/server2/branch_utility.py
index d449263..eb1dffc 100644
--- a/chrome/common/extensions/docs/server2/branch_utility.py
+++ b/chrome/common/extensions/docs/server2/branch_utility.py
@@ -9,13 +9,33 @@ import operator
from appengine_url_fetcher import AppEngineUrlFetcher
import url_constants
+
class ChannelInfo(object):
+ '''Represents a Chrome channel with three pieces of information. |channel| is
+ one of 'stable', 'beta', 'dev', or 'trunk'. |branch| and |version| correspond
+ with each other, and represent different releases of Chrome. Note that
+ |branch| and |version| can occasionally be the same for separate channels
+ (i.e. 'beta' and 'dev'), so all three fields are required to uniquely
+ identify a channel.
+ '''
+
def __init__(self, channel, branch, version):
self.channel = channel
self.branch = branch
self.version = version
+ def __eq__(self, other):
+ return self.__dict__ == other.__dict__
+
+ def __ne__(self, other):
+ return not (self == other)
+
+
class BranchUtility(object):
+ '''Provides methods for working with Chrome channel, branch, and version
+ data served from OmahaProxy.
+ '''
+
def __init__(self, fetch_url, history_url, fetcher, object_store_creator):
self._fetcher = fetcher
def create_object_store(category):
@@ -26,6 +46,13 @@ class BranchUtility(object):
self._history_result = self._fetcher.FetchAsync(history_url)
@staticmethod
+ def Create(object_store_creator):
+ return BranchUtility(url_constants.OMAHA_PROXY_URL,
+ url_constants.OMAHA_DEV_HISTORY,
+ AppEngineUrlFetcher(),
+ object_store_creator)
+
+ @staticmethod
def GetAllChannelNames():
return ('stable', 'beta', 'dev', 'trunk')
@@ -36,12 +63,30 @@ class BranchUtility(object):
if channel in channels:
return channel
- @staticmethod
- def Create(object_store_creator):
- return BranchUtility(url_constants.OMAHA_PROXY_URL,
- url_constants.OMAHA_DEV_HISTORY,
- AppEngineUrlFetcher(),
- object_store_creator)
+ def Newer(self, channel_info):
+ '''Given a ChannelInfo object, returns a new ChannelInfo object
+ representing the next most recent Chrome version/branch combination.
+ '''
+ if channel_info.channel == 'trunk':
+ return None
+ if channel_info.channel == 'stable':
+ stable_info = self.GetChannelInfo('stable')
+ if channel_info.version < stable_info.version:
+ return self.GetStableChannelInfo(channel_info.version + 1)
+ names = self.GetAllChannelNames()
+ return self.GetAllChannelInfo()[names.index(channel_info.channel) + 1]
+
+ def Older(self, channel_info):
+ '''Given a ChannelInfo object, returns a new ChannelInfo object
+ representing the previous Chrome version/branch combination.
+ '''
+ if channel_info.channel == 'stable':
+ if channel_info.version <= 5:
+ # BranchUtility can't access branch data from before Chrome version 5.
+ return None
+ return self.GetStableChannelInfo(channel_info.version - 1)
+ names = self.GetAllChannelNames()
+ return self.GetAllChannelInfo()[names.index(channel_info.channel) - 1]
@staticmethod
def SplitChannelNameFromPath(path):
@@ -57,16 +102,16 @@ class BranchUtility(object):
return (first, second)
return (None, path)
- def GetAllBranchNumbers(self):
- return ((channel, self.GetChannelInfo(channel).branch)
+ def GetAllBranches(self):
+ return tuple((channel, self.GetChannelInfo(channel).branch)
for channel in BranchUtility.GetAllChannelNames())
- def GetAllVersionNumbers(self):
- return (self.GetChannelInfo(channel).version
+ def GetAllVersions(self):
+ return tuple(self.GetChannelInfo(channel).version
for channel in BranchUtility.GetAllChannelNames())
def GetAllChannelInfo(self):
- return (self.GetChannelInfo(channel)
+ return tuple(self.GetChannelInfo(channel)
for channel in BranchUtility.GetAllChannelNames())
@@ -75,6 +120,12 @@ class BranchUtility(object):
self._ExtractFromVersionJson(channel, 'branch'),
self._ExtractFromVersionJson(channel, 'version'))
+ def GetStableChannelInfo(self, version):
+ '''Given a |version| corresponding to a 'stable' version of Chrome, returns
+ a ChannelInfo object representing that version.
+ '''
+ return ChannelInfo('stable', self.GetBranchForVersion(version), version)
+
def _ExtractFromVersionJson(self, channel_name, data_type):
'''Returns the branch or version number for a channel name.
'''
diff --git a/chrome/common/extensions/docs/server2/branch_utility_test.py b/chrome/common/extensions/docs/server2/branch_utility_test.py
index 95ee507..a9b76a8 100755
--- a/chrome/common/extensions/docs/server2/branch_utility_test.py
+++ b/chrome/common/extensions/docs/server2/branch_utility_test.py
@@ -7,11 +7,13 @@ import os
import sys
import unittest
-from branch_utility import BranchUtility
+from branch_utility import BranchUtility, ChannelInfo
from fake_url_fetcher import FakeUrlFetcher
from object_store_creator import ObjectStoreCreator
+
class BranchUtilityTest(unittest.TestCase):
+
def setUp(self):
self._branch_util = BranchUtility(
os.path.join('branch_utility', 'first.json'),
@@ -63,31 +65,59 @@ class BranchUtilityTest(unittest.TestCase):
self.assertEquals('dev', self._branch_util.NewestChannel(('dev',)))
self.assertEquals('trunk', self._branch_util.NewestChannel(('trunk',)))
+ def testNewer(self):
+ oldest_stable_info = ChannelInfo('stable', 963, 17)
+ older_stable_info = ChannelInfo('stable', 1025, 18)
+ old_stable_info = ChannelInfo('stable', 1084, 19)
+ sort_of_old_stable_info = ChannelInfo('stable', 1364, 25)
+ stable_info = ChannelInfo('stable', 1410, 26)
+ beta_info = ChannelInfo('beta', 1453, 27)
+ dev_info = ChannelInfo('dev', 1500, 28)
+ trunk_info = ChannelInfo('trunk', 'trunk', 'trunk')
+
+ self.assertEquals(older_stable_info,
+ self._branch_util.Newer(oldest_stable_info))
+ self.assertEquals(old_stable_info,
+ self._branch_util.Newer(older_stable_info))
+ self.assertEquals(stable_info,
+ self._branch_util.Newer(sort_of_old_stable_info))
+ self.assertEquals(beta_info, self._branch_util.Newer(stable_info))
+ self.assertEquals(dev_info, self._branch_util.Newer(beta_info))
+ self.assertEquals(trunk_info, self._branch_util.Newer(dev_info))
+ # Test the upper limit.
+ self.assertEquals(None, self._branch_util.Newer(trunk_info))
+
+
+ def testOlder(self):
+ trunk_info = ChannelInfo('trunk', 'trunk', 'trunk')
+ dev_info = ChannelInfo('dev', 1500, 28)
+ beta_info = ChannelInfo('beta', 1453, 27)
+ stable_info = ChannelInfo('stable', 1410, 26)
+ old_stable_info = ChannelInfo('stable', 1364, 25)
+ older_stable_info = ChannelInfo('stable', 1312, 24)
+ oldest_stable_info = ChannelInfo('stable', 396, 5)
+
+ self.assertEquals(dev_info, self._branch_util.Older(trunk_info))
+ self.assertEquals(beta_info, self._branch_util.Older(dev_info))
+ self.assertEquals(stable_info, self._branch_util.Older(beta_info))
+ self.assertEquals(old_stable_info, self._branch_util.Older(stable_info))
+ self.assertEquals(older_stable_info,
+ self._branch_util.Older(old_stable_info))
+ # Test the lower limit.
+ self.assertEquals(None, self._branch_util.Older(oldest_stable_info))
+
def testGetChannelInfo(self):
- self.assertEquals('trunk',
- self._branch_util.GetChannelInfo('trunk').channel)
- self.assertEquals('trunk',
- self._branch_util.GetChannelInfo('trunk').branch)
- self.assertEquals('trunk',
- self._branch_util.GetChannelInfo('trunk').version)
- self.assertEquals('dev',
- self._branch_util.GetChannelInfo('dev').channel)
- self.assertEquals(1500,
- self._branch_util.GetChannelInfo('dev').branch)
- self.assertEquals(28,
- self._branch_util.GetChannelInfo('dev').version)
- self.assertEquals('beta',
- self._branch_util.GetChannelInfo('beta').channel)
- self.assertEquals(1453,
- self._branch_util.GetChannelInfo('beta').branch)
- self.assertEquals(27,
- self._branch_util.GetChannelInfo('beta').version)
- self.assertEquals('stable',
- self._branch_util.GetChannelInfo('stable').channel)
- self.assertEquals(1410,
- self._branch_util.GetChannelInfo('stable').branch)
- self.assertEquals(26,
- self._branch_util.GetChannelInfo('stable').version)
+ trunk_info = ChannelInfo('trunk', 'trunk', 'trunk')
+ self.assertEquals(trunk_info, self._branch_util.GetChannelInfo('trunk'))
+
+ dev_info = ChannelInfo('dev', 1500, 28)
+ self.assertEquals(dev_info, self._branch_util.GetChannelInfo('dev'))
+
+ beta_info = ChannelInfo('beta', 1453, 27)
+ self.assertEquals(beta_info, self._branch_util.GetChannelInfo('beta'))
+
+ stable_info = ChannelInfo('stable', 1410, 26)
+ self.assertEquals(stable_info, self._branch_util.GetChannelInfo('stable'))
def testGetLatestVersionNumber(self):
self.assertEquals(28, self._branch_util.GetLatestVersionNumber())
diff --git a/chrome/common/extensions/docs/server2/cron.yaml b/chrome/common/extensions/docs/server2/cron.yaml
index 9e07913..364a5ea 100644
--- a/chrome/common/extensions/docs/server2/cron.yaml
+++ b/chrome/common/extensions/docs/server2/cron.yaml
@@ -2,4 +2,4 @@ cron:
- description: Repopulates all cached data.
url: /_cron
schedule: every 5 minutes
- target: 2-27-1
+ target: 2-27-2
diff --git a/chrome/common/extensions/docs/server2/host_file_system_iterator.py b/chrome/common/extensions/docs/server2/host_file_system_iterator.py
new file mode 100644
index 0000000..20c0741
--- /dev/null
+++ b/chrome/common/extensions/docs/server2/host_file_system_iterator.py
@@ -0,0 +1,55 @@
+# 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.
+
+from third_party.json_schema_compiler.memoize import memoize
+
+
+class HostFileSystemIterator(object):
+ '''Provides methods for iterating through host file systems, in both
+ ascending (oldest to newest version) and descending order.
+ '''
+
+ def __init__(self, file_system_creator, host_file_system, branch_utility):
+ self._file_system_creator = file_system_creator
+ self._host_file_system = host_file_system
+ self._branch_utility = branch_utility
+
+ @memoize
+ def _GetFileSystem(self, branch):
+ '''To avoid overwriting the persistent data store entry for the 'trunk'
+ host file system, hold on to a reference of this file system and return it
+ instead of creating a file system for 'trunk'.
+ Also of note: File systems are going to be iterated over multiple times
+ at each call of ForEach, but the data isn't going to change between calls.
+ Use |branch| to memoize the created file systems.
+ '''
+ if branch == 'trunk':
+ # Don't create a new file system for trunk, since there is a bug with the
+ # current architecture and design of HostFileSystemCreator, where
+ # creating 'trunk' ignores the pinned revision (in fact, it bypasses
+ # every difference including whether the file system is patched).
+ # TODO(kalman): Fix HostFileSystemCreator and update this comment.
+ return self._host_file_system
+ return self._file_system_creator.Create(branch)
+
+ def _ForEach(self, channel_info, callback, get_next):
+ '''Iterates through a sequence of file systems defined by |get_next| until
+ |callback| returns False, or until the end of the sequence of file systems
+ is reached. Returns the BranchUtility.ChannelInfo of the last file system
+ for which |callback| returned True.
+ '''
+ last_true = None
+ while channel_info is not None:
+ file_system = self._GetFileSystem(channel_info.branch)
+ if not callback(file_system, channel_info):
+ return last_true
+ last_true = channel_info
+ channel_info = get_next(channel_info)
+ return last_true
+
+ def Ascending(self, channel_info, callback):
+ return self._ForEach(channel_info, callback, self._branch_utility.Newer)
+
+ def Descending(self, channel_info, callback):
+ return self._ForEach(channel_info, callback, self._branch_utility.Older)
diff --git a/chrome/common/extensions/docs/server2/host_file_system_iterator_test.py b/chrome/common/extensions/docs/server2/host_file_system_iterator_test.py
new file mode 100755
index 0000000..22801c9
--- /dev/null
+++ b/chrome/common/extensions/docs/server2/host_file_system_iterator_test.py
@@ -0,0 +1,177 @@
+#!/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 host_file_system_creator import HostFileSystemCreator
+from host_file_system_creator_test import ConstructorForTest
+from host_file_system_iterator import HostFileSystemIterator
+from object_store_creator import ObjectStoreCreator
+from test_branch_utility import TestBranchUtility
+
+
+def _GetIterationTracker(version):
+ '''Adds the ChannelInfo object from each iteration to a list, and signals the
+ loop to stop when |version| is reached.
+ '''
+ iterations = []
+ def callback(file_system, channel_info):
+ if channel_info.version == version:
+ return False
+ iterations.append(channel_info)
+ return True
+ return (iterations, callback)
+
+
+class HostFileSystemIteratorTest(unittest.TestCase):
+
+ def setUp(self):
+ host_file_system_creator = HostFileSystemCreator(
+ ObjectStoreCreator.ForTest(),
+ constructor_for_test=ConstructorForTest)
+ self._branch_utility = TestBranchUtility.CreateWithCannedData()
+ self._iterator = HostFileSystemIterator(
+ host_file_system_creator,
+ host_file_system_creator.Create('trunk'),
+ self._branch_utility)
+
+ def _GetStableChannelInfo(self,version):
+ return self._branch_utility.GetStableChannelInfo(version)
+
+ def _GetChannelInfo(self, channel_name):
+ return self._branch_utility.GetChannelInfo(channel_name)
+
+ def testAscending(self):
+ # Start at |stable| version 5, and move up towards |trunk|.
+ # Total: 25 file systems.
+ iterations, callback = _GetIterationTracker(0)
+ self.assertEqual(
+ self._iterator.Ascending(self._GetStableChannelInfo(5), callback),
+ self._GetChannelInfo('trunk'))
+ self.assertEqual(len(iterations), 25)
+
+ # Start at |stable| version 5, and move up towards |trunk|. The callback
+ # fails at |beta|, so the last successful callback was the latest version
+ # of |stable|. Total: 22 file systems.
+ iterations, callback = _GetIterationTracker(
+ self._GetChannelInfo('beta').version)
+ self.assertEqual(
+ self._iterator.Ascending(self._GetStableChannelInfo(5), callback),
+ self._GetChannelInfo('stable'))
+ self.assertEqual(len(iterations), 22)
+
+ # Start at |stable| version 5, and the callback fails immediately. Since
+ # no file systems are successfully processed, expect a return of None.
+ iterations, callback = _GetIterationTracker(5)
+ self.assertEqual(
+ self._iterator.Ascending(self._GetStableChannelInfo(5), callback),
+ None)
+ self.assertEqual([], iterations)
+
+ # Start at |stable| version 5, and the callback fails at version 6.
+ # The return should represent |stable| version 5.
+ iterations, callback = _GetIterationTracker(6)
+ self.assertEqual(
+ self._iterator.Ascending(self._GetStableChannelInfo(5), callback),
+ self._GetStableChannelInfo(5))
+ self.assertEqual([self._GetStableChannelInfo(5)], iterations)
+
+ # Start at the latest version of |stable|, and the callback fails at
+ # |trunk|. Total: 3 file systems.
+ iterations, callback = _GetIterationTracker('trunk')
+ self.assertEqual(
+ self._iterator.Ascending(self._GetChannelInfo('stable'), callback),
+ self._GetChannelInfo('dev'))
+ self.assertEqual([self._GetChannelInfo('stable'),
+ self._GetChannelInfo('beta'),
+ self._GetChannelInfo('dev')], iterations)
+
+ # Start at |stable| version 10, and the callback fails at |trunk|.
+ iterations, callback = _GetIterationTracker('trunk')
+ self.assertEqual(
+ self._iterator.Ascending(self._GetStableChannelInfo(10), callback),
+ self._GetChannelInfo('dev'))
+ self.assertEqual([self._GetStableChannelInfo(10),
+ self._GetStableChannelInfo(11),
+ self._GetStableChannelInfo(12),
+ self._GetStableChannelInfo(13),
+ self._GetStableChannelInfo(14),
+ self._GetStableChannelInfo(15),
+ self._GetStableChannelInfo(16),
+ self._GetStableChannelInfo(17),
+ self._GetStableChannelInfo(18),
+ self._GetStableChannelInfo(19),
+ self._GetStableChannelInfo(20),
+ self._GetStableChannelInfo(21),
+ self._GetStableChannelInfo(22),
+ self._GetStableChannelInfo(23),
+ self._GetStableChannelInfo(24),
+ self._GetStableChannelInfo(25),
+ self._GetChannelInfo('stable'),
+ self._GetChannelInfo('beta'),
+ self._GetChannelInfo('dev')], iterations)
+
+ def testDescending(self):
+ # Start at |trunk|, and the callback fails immediately. No file systems
+ # are successfully processed, so Descending() will return None.
+ iterations, callback = _GetIterationTracker('trunk')
+ self.assertEqual(
+ self._iterator.Descending(self._GetChannelInfo('trunk'), callback),
+ None)
+ self.assertEqual([], iterations)
+
+ # Start at |trunk|, and the callback fails at |dev|. Last good iteration
+ # should be |trunk|.
+ iterations, callback = _GetIterationTracker(
+ self._GetChannelInfo('dev').version)
+ self.assertEqual(
+ self._iterator.Descending(self._GetChannelInfo('trunk'), callback),
+ self._GetChannelInfo('trunk'))
+ self.assertEqual([self._GetChannelInfo('trunk')], iterations)
+
+ # Start at |trunk|, and then move from |dev| down to |stable| at version 5.
+ # Total: 25 file systems.
+ iterations, callback = _GetIterationTracker(0)
+ self.assertEqual(
+ self._iterator.Descending(self._GetChannelInfo('trunk'), callback),
+ self._GetStableChannelInfo(5))
+ self.assertEqual(len(iterations), 25)
+
+ # Start at the latest version of |stable|, and move down to |stable| at
+ # version 5. Total: 22 file systems.
+ iterations, callback = _GetIterationTracker(0)
+ self.assertEqual(
+ self._iterator.Descending(self._GetChannelInfo('stable'), callback),
+ self._GetStableChannelInfo(5))
+ self.assertEqual(len(iterations), 22)
+
+ # Start at |dev| and iterate down through |stable| versions. The callback
+ # fails at version 10. Total: 18 file systems.
+ iterations, callback = _GetIterationTracker(10)
+ self.assertEqual(
+ self._iterator.Descending(self._GetChannelInfo('dev'), callback),
+ self._GetStableChannelInfo(11))
+ self.assertEqual([self._GetChannelInfo('dev'),
+ self._GetChannelInfo('beta'),
+ self._GetStableChannelInfo(26),
+ self._GetStableChannelInfo(25),
+ self._GetStableChannelInfo(24),
+ self._GetStableChannelInfo(23),
+ self._GetStableChannelInfo(22),
+ self._GetStableChannelInfo(21),
+ self._GetStableChannelInfo(20),
+ self._GetStableChannelInfo(19),
+ self._GetStableChannelInfo(18),
+ self._GetStableChannelInfo(17),
+ self._GetStableChannelInfo(16),
+ self._GetStableChannelInfo(15),
+ self._GetStableChannelInfo(14),
+ self._GetStableChannelInfo(13),
+ self._GetStableChannelInfo(12),
+ self._GetStableChannelInfo(11)], iterations)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/chrome/common/extensions/docs/server2/server_instance.py b/chrome/common/extensions/docs/server2/server_instance.py
index 1f9321d..a2b936a 100644
--- a/chrome/common/extensions/docs/server2/server_instance.py
+++ b/chrome/common/extensions/docs/server2/server_instance.py
@@ -11,6 +11,7 @@ from data_source_registry import CreateDataSources
from empty_dir_file_system import EmptyDirFileSystem
from example_zipper import ExampleZipper
from host_file_system_creator import HostFileSystemCreator
+from host_file_system_iterator import HostFileSystemIterator
from intro_data_source import IntroDataSource
from object_store_creator import ObjectStoreCreator
from path_canonicalizer import PathCanonicalizer
@@ -24,7 +25,9 @@ from template_data_source import TemplateDataSource
from test_branch_utility import TestBranchUtility
from test_object_store import TestObjectStore
+
class ServerInstance(object):
+
def __init__(self,
object_store_creator,
host_file_system,
@@ -43,11 +46,15 @@ class ServerInstance(object):
self.host_file_system_creator = host_file_system_creator
- self.availability_finder_factory = AvailabilityFinder.Factory(
+ self.host_file_system_iterator = HostFileSystemIterator(
+ host_file_system_creator,
+ host_file_system,
+ branch_utility)
+
+ self.availability_finder = AvailabilityFinder(
+ self.host_file_system_iterator,
object_store_creator,
- self.compiled_host_fs_factory,
- branch_utility,
- host_file_system_creator)
+ branch_utility)
self.api_list_data_source_factory = APIListDataSource.Factory(
self.compiled_host_fs_factory,
@@ -58,7 +65,8 @@ class ServerInstance(object):
self.api_data_source_factory = APIDataSource.Factory(
self.compiled_host_fs_factory,
svn_constants.API_PATH,
- self.availability_finder_factory)
+ self.availability_finder,
+ branch_utility)
self.ref_resolver_factory = ReferenceResolver.Factory(
self.api_data_source_factory,
diff --git a/chrome/common/extensions/docs/server2/template_data_source_test.py b/chrome/common/extensions/docs/server2/template_data_source_test.py
index 190d493..a1deacd 100755
--- a/chrome/common/extensions/docs/server2/template_data_source_test.py
+++ b/chrome/common/extensions/docs/server2/template_data_source_test.py
@@ -15,11 +15,14 @@ from object_store_creator import ObjectStoreCreator
from permissions_data_source import PermissionsDataSource
from reference_resolver import ReferenceResolver
from template_data_source import TemplateDataSource
+from test_branch_utility import TestBranchUtility
from test_util import DisableLogging
-from third_party.handlebar import Handlebar
from servlet import Request
+from third_party.handlebar import Handlebar
+
class _FakeFactory(object):
+
def __init__(self, input_dict=None):
if input_dict is None:
self._input_dict = {}
@@ -29,7 +32,9 @@ class _FakeFactory(object):
def Create(self, *args, **optargs):
return self._input_dict
+
class TemplateDataSourceTest(unittest.TestCase):
+
def setUp(self):
self._base_path = os.path.join(sys.path[0],
'test_data',
@@ -54,9 +59,11 @@ class TemplateDataSourceTest(unittest.TestCase):
def _CreateTemplateDataSource(self, compiled_fs_factory, api_data=None):
if api_data is None:
- api_data_factory = APIDataSource.Factory(compiled_fs_factory,
- 'fake_path',
- _FakeFactory())
+ api_data_factory = APIDataSource.Factory(
+ compiled_fs_factory,
+ 'fake_path',
+ _FakeFactory(),
+ TestBranchUtility.CreateWithCannedData())
else:
api_data_factory = _FakeFactory(api_data)
reference_resolver_factory = ReferenceResolver.Factory(
diff --git a/chrome/common/extensions/docs/server2/test_branch_utility.py b/chrome/common/extensions/docs/server2/test_branch_utility.py
index 5dec476..da91f49 100644
--- a/chrome/common/extensions/docs/server2/test_branch_utility.py
+++ b/chrome/common/extensions/docs/server2/test_branch_utility.py
@@ -5,15 +5,17 @@
from branch_utility import BranchUtility, ChannelInfo
from test_data.canned_data import (CANNED_BRANCHES, CANNED_CHANNELS)
+
class TestBranchUtility(object):
'''Mimics BranchUtility to return valid-ish data without needing omahaproxy
data.
'''
- def __init__(self, branches, channels):
- ''' Parameters: |branches| is a mapping of versions to branches, and
+
+ def __init__(self, versions, channels):
+ ''' Parameters: |version| is a mapping of versions to branches, and
|channels| is a mapping of channels to versions.
'''
- self._branches = branches
+ self._versions = versions
self._channels = channels
@staticmethod
@@ -24,17 +26,42 @@ class TestBranchUtility(object):
return TestBranchUtility(CANNED_BRANCHES, CANNED_CHANNELS)
def GetAllChannelInfo(self):
- return [self.GetChannelInfo(channel)
- for channel in BranchUtility.GetAllChannelNames()]
+ return tuple(self.GetChannelInfo(channel)
+ for channel in BranchUtility.GetAllChannelNames())
def GetChannelInfo(self, channel):
version = self._channels[channel]
return ChannelInfo(channel, self.GetBranchForVersion(version), version)
+ def GetStableChannelInfo(self, version):
+ return ChannelInfo('stable', self.GetBranchForVersion(version), version)
+
def GetBranchForVersion(self, version):
- return self._branches[version]
+ return self._versions[version]
def GetChannelForVersion(self, version):
+ if version <= self._channels['stable']:
+ return 'stable'
for channel in self._channels.iterkeys():
if self._channels[channel] == version:
return channel
+
+ def Older(self, channel_info):
+ versions = self._versions.keys()
+ index = versions.index(channel_info.version)
+ if index == len(versions) - 1:
+ return None
+ version = versions[index + 1]
+ return ChannelInfo(self.GetChannelForVersion(version),
+ self.GetBranchForVersion(version),
+ version)
+
+ def Newer(self, channel_info):
+ versions = self._versions.keys()
+ index = versions.index(channel_info.version)
+ if not index:
+ return None
+ version = versions[index - 1]
+ return ChannelInfo(self.GetChannelForVersion(version),
+ self.GetBranchForVersion(version),
+ version)
diff --git a/chrome/common/extensions/docs/server2/test_data/canned_data.py b/chrome/common/extensions/docs/server2/test_data/canned_data.py
index 5301563..b3a0136 100644
--- a/chrome/common/extensions/docs/server2/test_data/canned_data.py
+++ b/chrome/common/extensions/docs/server2/test_data/canned_data.py
@@ -3,41 +3,45 @@
# found in the LICENSE file.
import json
+from third_party.json_schema_compiler.json_parse import OrderedDict
-CANNED_CHANNELS = {
- 'trunk': 'trunk',
- 'dev': 28,
- 'beta': 27,
- 'stable': 26
-}
-CANNED_BRANCHES = {
- 'trunk': 'trunk',
- 28: 1500,
- 27: 1453,
- 26: 1410,
- 25: 1364,
- 24: 1312,
- 23: 1271,
- 22: 1229,
- 21: 1180,
- 20: 1132,
- 19: 1084,
- 18: 1025,
- 17: 963,
- 16: 912,
- 15: 874,
- 14: 835,
- 13: 782,
- 12: 742,
- 11: 696,
- 10: 648,
- 9: 597,
- 8: 552,
- 7: 544,
- 6: 495,
- 5: 396
-}
+CANNED_CHANNELS = OrderedDict([
+ ('trunk', 'trunk'),
+ ('dev', 28),
+ ('beta', 27),
+ ('stable', 26)
+])
+
+
+CANNED_BRANCHES = OrderedDict([
+ ('trunk', 'trunk'),
+ (28, 1500),
+ (27, 1453),
+ (26, 1410),
+ (25, 1364),
+ (24, 1312),
+ (23, 1271),
+ (22, 1229),
+ (21, 1180),
+ (20, 1132),
+ (19, 1084),
+ (18, 1025),
+ (17, 963),
+ (16, 912),
+ (15, 874),
+ (14, 835),
+ (13, 782),
+ (12, 742),
+ (11, 696),
+ (10, 648),
+ ( 9, 597),
+ ( 8, 552),
+ ( 7, 544),
+ ( 6, 495),
+ ( 5, 396)
+])
+
CANNED_TEST_FILE_SYSTEM_DATA = {
'api': {
@@ -59,11 +63,20 @@ CANNED_TEST_FILE_SYSTEM_DATA = {
},
'json': {
'api_availabilities.json': json.dumps({
- 'tester': {
- 'channel': 'stable',
- 'version': 42
- }
- }),
+ 'trunk_api': {
+ 'channel': 'trunk'
+ },
+ 'dev_api': {
+ 'channel': 'dev'
+ },
+ 'beta_api': {
+ 'channel': 'beta'
+ },
+ 'stable_api': {
+ 'channel': 'stable',
+ 'version': 20
+ }
+ }),
'intro_tables.json': json.dumps({
'tester': {
'Permissions': [
@@ -93,6 +106,7 @@ CANNED_TEST_FILE_SYSTEM_DATA = {
}
}
+
CANNED_API_FILE_SYSTEM_DATA = {
'trunk': {
'api': {
@@ -105,12 +119,24 @@ CANNED_API_FILE_SYSTEM_DATA = {
},
'extension': {
'channel': 'stable'
+ },
+ 'systemInfo.cpu': {
+ 'channel': 'stable'
+ },
+ 'systemInfo.stuff': {
+ 'channel': 'dev'
}
}),
'_manifest_features.json': json.dumps({
'history': {
'channel': 'beta'
},
+ 'notifications': {
+ 'channel': 'beta'
+ },
+ 'page_action': {
+ 'channel': 'stable'
+ },
'runtime': {
'channel': 'stable'
},
@@ -119,6 +145,9 @@ CANNED_API_FILE_SYSTEM_DATA = {
},
'sync': {
'channel': 'trunk'
+ },
+ 'web_request': {
+ 'channel': 'stable'
}
}),
'_permission_features.json': json.dumps({
@@ -145,6 +174,9 @@ CANNED_API_FILE_SYSTEM_DATA = {
'falseBetaAPI': {
'channel': 'beta'
},
+ 'systemInfo.display': {
+ 'channel': 'stable'
+ },
'trunkAPI': {
'channel': 'trunk'
}
@@ -190,6 +222,9 @@ CANNED_API_FILE_SYSTEM_DATA = {
'extension': {
'channel': 'stable'
},
+ 'systemInfo.cpu': {
+ 'channel': 'stable'
+ },
'systemInfo.stuff': {
'channel': 'dev'
}
@@ -201,6 +236,9 @@ CANNED_API_FILE_SYSTEM_DATA = {
'notifications': {
'channel': 'beta'
},
+ 'page_action': {
+ 'channel': 'stable'
+ },
'runtime': {
'channel': 'stable'
},
@@ -209,6 +247,12 @@ CANNED_API_FILE_SYSTEM_DATA = {
},
'sync': {
'channel': 'trunk'
+ },
+ 'system_info_display': {
+ 'channel': 'stable'
+ },
+ 'web_request': {
+ 'channel': 'stable'
}
}),
'_permission_features.json': json.dumps({
@@ -263,11 +307,20 @@ CANNED_API_FILE_SYSTEM_DATA = {
'notifications': {
'channel': 'dev'
},
+ 'page_action': {
+ 'channel': 'stable'
+ },
'runtime': {
'channel': 'stable'
},
'storage': {
'channel': 'dev'
+ },
+ 'system_info_display': {
+ 'channel': 'stable'
+ },
+ 'web_request': {
+ 'channel': 'stable'
}
}),
'_permission_features.json': json.dumps({
diff --git a/chrome/common/extensions/docs/templates/json/api_availabilities.json b/chrome/common/extensions/docs/templates/json/api_availabilities.json
index 9dbc4d6..bc196b0 100644
--- a/chrome/common/extensions/docs/templates/json/api_availabilities.json
+++ b/chrome/common/extensions/docs/templates/json/api_availabilities.json
@@ -19,6 +19,10 @@
"channel": "stable",
"version": 21
},
+ "mediaGalleries": {
+ "channel": "stable",
+ "version": 23
+ },
"notifications": {
"channel": "stable",
"version": 28