availability_finder.py revision 7dbb3d5cf0c15f500944d211057644d6a2f37371
1# Copyright 2013 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import collections
6import os
7
8from branch_utility import BranchUtility
9from compiled_file_system import CompiledFileSystem
10from file_system import FileNotFoundError
11import svn_constants
12from third_party.json_schema_compiler import json_parse, model
13from third_party.json_schema_compiler.memoize import memoize
14
15_API_AVAILABILITIES = svn_constants.JSON_PATH + '/api_availabilities.json'
16_API_FEATURES = svn_constants.API_PATH + '/_api_features.json'
17_EXTENSION_API = svn_constants.API_PATH + '/extension_api.json'
18_MANIFEST_FEATURES = svn_constants.API_PATH + '/_manifest_features.json'
19_PERMISSION_FEATURES = svn_constants.API_PATH + '/_permission_features.json'
20_STABLE = 'stable'
21
22class AvailabilityInfo(object):
23  def __init__(self, channel, version):
24    self.channel = channel
25    self.version = version
26
27def _GetChannelFromFeatures(api_name, file_system, path):
28  '''Finds API channel information within _features.json files at the given
29  |path| for the given |file_system|. Returns None if channel information for
30  the API cannot be located.
31  '''
32  feature = file_system.GetFromFile(path).get(api_name)
33
34  if feature is None:
35    return None
36  if isinstance(feature, collections.Mapping):
37    # The channel information dict is nested within a list for whitelisting
38    # purposes.
39    return feature.get('channel')
40  # Features can contain a list of entries. Take the newest branch.
41  return BranchUtility.NewestChannel(entry.get('channel')
42                                     for entry in feature)
43
44def _GetChannelFromApiFeatures(api_name, file_system):
45  try:
46    return _GetChannelFromFeatures(api_name, file_system, _API_FEATURES)
47  except FileNotFoundError as e:
48    # TODO(epeterson) Remove except block once _api_features is in all channels.
49    return None
50
51def _GetChannelFromPermissionFeatures(api_name, file_system):
52  return _GetChannelFromFeatures(api_name, file_system, _PERMISSION_FEATURES)
53
54def _GetChannelFromManifestFeatures(api_name, file_system):
55  return _GetChannelFromFeatures(#_manifest_features uses unix_style API names
56                                 model.UnixName(api_name),
57                                 file_system,
58                                 _MANIFEST_FEATURES)
59
60def _ExistsInFileSystem(api_name, file_system):
61  '''Checks for existence of |api_name| within the list of files in the api/
62  directory found using the given file system.
63  '''
64  file_names = file_system.GetFromFileListing(svn_constants.API_PATH)
65  # File names switch from unix_hacker_style to camelCase at versions <= 20.
66  return model.UnixName(api_name) in file_names or api_name in file_names
67
68def _ExistsInExtensionApi(api_name, file_system):
69  '''Parses the api/extension_api.json file (available in Chrome versions
70  before 18) for an API namespace. If this is successfully found, then the API
71  is considered to have been 'stable' for the given version.
72  '''
73  try:
74    extension_api_json = file_system.GetFromFile(_EXTENSION_API)
75    api_rows = [row.get('namespace') for row in extension_api_json
76                if 'namespace' in row]
77    return True if api_name in api_rows else False
78  except FileNotFoundError as e:
79    # This should only happen on preview.py since extension_api.json is no
80    # longer present in trunk.
81    return False
82
83class AvailabilityFinder(object):
84  '''Uses API data sources generated by a ChromeVersionDataSource in order to
85  search the filesystem for the earliest existence of a specified API throughout
86  the different versions of Chrome; this constitutes an API's availability.
87  '''
88  class Factory(object):
89    def __init__(self,
90                 object_store_creator,
91                 compiled_host_fs_factory,
92                 branch_utility,
93                 host_file_system_creator):
94      self._object_store_creator = object_store_creator
95      self._compiled_host_fs_factory = compiled_host_fs_factory
96      self._branch_utility = branch_utility
97      self._host_file_system_creator = host_file_system_creator
98
99    def Create(self):
100      return AvailabilityFinder(self._object_store_creator,
101                                self._compiled_host_fs_factory,
102                                self._branch_utility,
103                                self._host_file_system_creator)
104
105  def __init__(self,
106               object_store_creator,
107               compiled_host_fs_factory,
108               branch_utility,
109               host_file_system_creator):
110    self._object_store_creator = object_store_creator
111    self._json_cache = compiled_host_fs_factory.Create(
112        lambda _, json: json_parse.Parse(json),
113        AvailabilityFinder,
114        'json-cache')
115    self._branch_utility = branch_utility
116    self._host_file_system_creator = host_file_system_creator
117    self._object_store = object_store_creator.Create(AvailabilityFinder)
118
119  @memoize
120  def _CreateFeaturesAndNamesFileSystems(self, version):
121    '''The 'features' compiled file system's populate function parses and
122    returns the contents of a _features.json file. The 'names' compiled file
123    system's populate function creates a list of file names with .json or .idl
124    extensions.
125    '''
126    fs_factory = CompiledFileSystem.Factory(
127        self._host_file_system_creator.Create(
128            self._branch_utility.GetBranchForVersion(version)),
129        self._object_store_creator)
130    features_fs = fs_factory.Create(lambda _, json: json_parse.Parse(json),
131                                    AvailabilityFinder,
132                                    category='features')
133    names_fs = fs_factory.Create(self._GetExtNames,
134                                 AvailabilityFinder,
135                                 category='names')
136    return (features_fs, names_fs)
137
138  def _GetExtNames(self, base_path, apis):
139    return [os.path.splitext(api)[0] for api in apis
140            if os.path.splitext(api)[1][1:] in ['json', 'idl']]
141
142  def _FindEarliestStableAvailability(self, api_name, version):
143    '''Searches in descending order through filesystem caches tied to specific
144    chrome version numbers and looks for the availability of an API, |api_name|,
145    on the stable channel. When a version is found where the API is no longer
146    available on stable, returns the previous version number (the last known
147    version where the API was stable).
148    '''
149    available = True
150    while available:
151      if version < 5:
152        # SVN data isn't available below version 5.
153        return version + 1
154      available = False
155      features_fs, names_fs = self._CreateFeaturesAndNamesFileSystems(version)
156      if version >= 28:
157        # The _api_features.json file first appears in version 28 and should be
158        # the most reliable for finding API availabilities, so it gets checked
159        # first. The _permission_features.json and _manifest_features.json files
160        # are present in Chrome 20 and onwards. Fall back to a check for file
161        # system existence if the API is not stable in any of the _features.json
162        # files.
163        available = _GetChannelFromApiFeatures(api_name, features_fs) == _STABLE
164      if version >= 20:
165        # Check other _features.json files/file existence if the API wasn't
166        # found in _api_features.json, or if _api_features.json wasn't present.
167        available = available or (
168            _GetChannelFromPermissionFeatures(api_name, features_fs) == _STABLE
169            or _GetChannelFromManifestFeatures(api_name, features_fs) == _STABLE
170            or _ExistsInFileSystem(api_name, names_fs))
171      elif version >= 18:
172        # These versions are a little troublesome. Version 19 has
173        # _permission_features.json, but it lacks 'channel' information.
174        # Version 18 lacks all of the _features.json files. For now, we're using
175        # a simple check for filesystem existence here.
176        available = _ExistsInFileSystem(api_name, names_fs)
177      elif version >= 5:
178        # Versions 17 and down to 5 have an extension_api.json file which
179        # contains namespaces for each API that was available at the time. We
180        # can use this file to check for API existence.
181        available = _ExistsInExtensionApi(api_name, features_fs)
182
183      if not available:
184        return version + 1
185      version -= 1
186
187  def _GetAvailableChannelForVersion(self, api_name, version):
188    '''Searches through the _features files for a given |version| and returns
189    the channel that the given API is determined to be available on.
190    '''
191    features_fs, names_fs = self._CreateFeaturesAndNamesFileSystems(version)
192    channel = (_GetChannelFromApiFeatures(api_name, features_fs)
193               or _GetChannelFromPermissionFeatures(api_name, features_fs)
194               or _GetChannelFromManifestFeatures(api_name, features_fs))
195    if channel is None and _ExistsInFileSystem(api_name, names_fs):
196      # If an API is not represented in any of the _features files, but exists
197      # in the filesystem, then assume it is available in this version.
198      # The windows API is an example of this.
199      return self._branch_utility.GetChannelForVersion(version)
200
201    return channel
202
203  def GetApiAvailability(self, api_name):
204    '''Determines the availability for an API by testing several scenarios.
205    (i.e. Is the API experimental? Only available on certain development
206    channels? If it's stable, when did it first become stable? etc.)
207    '''
208    availability = self._object_store.Get(api_name).Get()
209    if availability is not None:
210      return availability
211
212    # Check for a predetermined availability for this API.
213    api_info = self._json_cache.GetFromFile(_API_AVAILABILITIES).get(api_name)
214    if api_info is not None:
215      channel = api_info.get('channel')
216      return AvailabilityInfo(
217          channel,
218          api_info.get('version') if channel == _STABLE else None)
219
220    # Check for the API in the development channels.
221    availability = None
222    for channel_info in self._branch_utility.GetAllChannelInfo():
223      available_channel = self._GetAvailableChannelForVersion(
224          api_name,
225          channel_info.version)
226      # If the |available_channel| for the API is the same as, or older than,
227      # the channel we're checking, then the API is available on this channel.
228      if (available_channel is not None and
229          BranchUtility.NewestChannel((available_channel, channel_info.channel))
230              == channel_info.channel):
231        availability = AvailabilityInfo(channel_info.channel,
232                                        channel_info.version)
233        break
234
235    # The API should at least be available on trunk. It's a bug otherwise.
236    assert availability, 'No availability found for %s' % api_name
237
238    # If the API is in stable, find the chrome version in which it became
239    # stable.
240    if availability.channel == _STABLE:
241      availability.version = self._FindEarliestStableAvailability(
242          api_name,
243          availability.version)
244
245    self._object_store.Set(api_name, availability)
246    return availability
247