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