availability_finder.py revision eb525c5499e34cc9c4b825d6d9e75bb07cc06ace
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                 # Takes a |version|, and returns a caching offline or online
94                 # subversion file system for that version.
95                 create_file_system_at_version):
96      self._object_store_creator = object_store_creator
97      self._compiled_host_fs_factory = compiled_host_fs_factory
98      self._branch_utility = branch_utility
99      self._create_file_system_at_version = create_file_system_at_version
100
101    def Create(self):
102      return AvailabilityFinder(self._object_store_creator,
103                                self._compiled_host_fs_factory,
104                                self._branch_utility,
105                                self._create_file_system_at_version)
106
107  def __init__(self,
108               object_store_creator,
109               compiled_host_fs_factory,
110               branch_utility,
111               create_file_system_at_version):
112    self._object_store_creator = object_store_creator
113    self._json_cache = compiled_host_fs_factory.Create(
114        lambda _, json: json_parse.Parse(json),
115        AvailabilityFinder,
116        'json-cache')
117    self._branch_utility = branch_utility
118    self._create_file_system_at_version = create_file_system_at_version
119    self._object_store = object_store_creator.Create(AvailabilityFinder)
120
121  @memoize
122  def _CreateFeaturesAndNamesFileSystems(self, version):
123    '''The 'features' compiled file system's populate function parses and
124    returns the contents of a _features.json file. The 'names' compiled file
125    system's populate function creates a list of file names with .json or .idl
126    extensions.
127    '''
128    fs_factory = CompiledFileSystem.Factory(
129        self._create_file_system_at_version(version),
130        self._object_store_creator)
131    features_fs = fs_factory.Create(lambda _, json: json_parse.Parse(json),
132                                    AvailabilityFinder,
133                                    category='features')
134    names_fs = fs_factory.Create(self._GetExtNames,
135                                 AvailabilityFinder,
136                                 category='names')
137    return (features_fs, names_fs)
138
139  def _GetExtNames(self, base_path, apis):
140    return [os.path.splitext(api)[0] for api in apis
141            if os.path.splitext(api)[1][1:] in ['json', 'idl']]
142
143  def _FindEarliestStableAvailability(self, api_name, version):
144    '''Searches in descending order through filesystem caches tied to specific
145    chrome version numbers and looks for the availability of an API, |api_name|,
146    on the stable channel. When a version is found where the API is no longer
147    available on stable, returns the previous version number (the last known
148    version where the API was stable).
149    '''
150    available = True
151    while available:
152      if version < 5:
153        # SVN data isn't available below version 5.
154        return version + 1
155      available = False
156      features_fs, names_fs = self._CreateFeaturesAndNamesFileSystems(version)
157      if version >= 28:
158        # The _api_features.json file first appears in version 28 and should be
159        # the most reliable for finding API availabilities, so it gets checked
160        # first. The _permission_features.json and _manifest_features.json files
161        # are present in Chrome 20 and onwards. Fall back to a check for file
162        # system existence if the API is not stable in any of the _features.json
163        # files.
164        available = _GetChannelFromApiFeatures(api_name, features_fs) == _STABLE
165      if version >= 20:
166        # Check other _features.json files/file existence if the API wasn't
167        # found in _api_features.json, or if _api_features.json wasn't present.
168        available = available or (
169            _GetChannelFromPermissionFeatures(api_name, features_fs) == _STABLE
170            or _GetChannelFromManifestFeatures(api_name, features_fs) == _STABLE
171            or _ExistsInFileSystem(api_name, names_fs))
172      elif version >= 18:
173        # These versions are a little troublesome. Version 19 has
174        # _permission_features.json, but it lacks 'channel' information.
175        # Version 18 lacks all of the _features.json files. For now, we're using
176        # a simple check for filesystem existence here.
177        available = _ExistsInFileSystem(api_name, names_fs)
178      elif version >= 5:
179        # Versions 17 and down to 5 have an extension_api.json file which
180        # contains namespaces for each API that was available at the time. We
181        # can use this file to check for API existence.
182        available = _ExistsInExtensionApi(api_name, features_fs)
183
184      if not available:
185        return version + 1
186      version -= 1
187
188  def _GetAvailableChannelForVersion(self, api_name, version):
189    '''Searches through the _features files for a given |version| and returns
190    the channel that the given API is determined to be available on.
191    '''
192    features_fs, names_fs = self._CreateFeaturesAndNamesFileSystems(version)
193    channel = (_GetChannelFromApiFeatures(api_name, features_fs)
194               or _GetChannelFromPermissionFeatures(api_name, features_fs)
195               or _GetChannelFromManifestFeatures(api_name, features_fs))
196    if channel is None and _ExistsInFileSystem(api_name, names_fs):
197      # If an API is not represented in any of the _features files, but exists
198      # in the filesystem, then assume it is available in this version.
199      # The windows API is an example of this.
200      return self._branch_utility.GetChannelForVersion(version)
201
202    return channel
203
204  def GetApiAvailability(self, api_name):
205    '''Determines the availability for an API by testing several scenarios.
206    (i.e. Is the API experimental? Only available on certain development
207    channels? If it's stable, when did it first become stable? etc.)
208    '''
209    availability = self._object_store.Get(api_name).Get()
210    if availability is not None:
211      return availability
212
213    # Check for a predetermined availability for this API.
214    api_info = self._json_cache.GetFromFile(_API_AVAILABILITIES).get(api_name)
215    if api_info is not None:
216      channel = api_info.get('channel')
217      return AvailabilityInfo(
218          channel,
219          api_info.get('version') if channel == _STABLE else None)
220
221    # Check for the API in the development channels.
222    availability = None
223    for channel_info in self._branch_utility.GetAllChannelInfo():
224      available_channel = self._GetAvailableChannelForVersion(
225          api_name,
226          channel_info.version)
227      # If the |available_channel| for the API is the same as, or older than,
228      # the channel we're checking, then the API is available on this channel.
229      if (available_channel is not None and
230          BranchUtility.NewestChannel((available_channel, channel_info.channel))
231              == channel_info.channel):
232        availability = AvailabilityInfo(channel_info.channel,
233                                        channel_info.version)
234        break
235
236    # The API should at least be available on trunk. It's a bug otherwise.
237    assert availability, 'No availability found for %s' % api_name
238
239    # If the API is in stable, find the chrome version in which it became
240    # stable.
241    if availability.channel == _STABLE:
242      availability.version = self._FindEarliestStableAvailability(
243          api_name,
244          availability.version)
245
246    self._object_store.Set(api_name, availability)
247    return availability
248