diff options
author | evan.peterson.EP@gmail.com <evan.peterson.EP@gmail.com@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-08-27 23:55:59 +0000 |
---|---|---|
committer | evan.peterson.EP@gmail.com <evan.peterson.EP@gmail.com@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-08-27 23:55:59 +0000 |
commit | 657c65c6afcc0473050ffcd72c0f72b591a81e30 (patch) | |
tree | ed3db00866d2214aa4f72929ab206e6a33fb8345 | |
parent | f4195b9441438b98f232167e50e0086474d7bc50 (diff) | |
download | chromium_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
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 |