availability_finder.py revision 5c02ac1a9c1b504631c0a3d2b6e737b5d738bae1
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
5from collections import Mapping
6import posixpath
7
8from api_schema_graph import APISchemaGraph
9from branch_utility import BranchUtility, ChannelInfo
10from extensions_paths import API_PATHS, JSON_TEMPLATES
11from features_bundle import FeaturesBundle
12import features_utility
13from file_system import FileNotFoundError
14from third_party.json_schema_compiler.memoize import memoize
15from third_party.json_schema_compiler.model import UnixName
16
17
18_EXTENSION_API = 'extension_api.json'
19
20# The version where api_features.json is first available.
21_API_FEATURES_MIN_VERSION = 28
22# The version where permission_ and manifest_features.json are available and
23# presented in the current format.
24_ORIGINAL_FEATURES_MIN_VERSION = 20
25# API schemas are aggregated in extension_api.json up to this version.
26_EXTENSION_API_MAX_VERSION = 17
27# The earliest version for which we have SVN data.
28_SVN_MIN_VERSION = 5
29
30
31def _GetChannelFromFeatures(api_name, features):
32  '''Finds API channel information for |api_name| from |features|.
33  Returns None if channel information for the API cannot be located.
34  '''
35  feature = features.Get().get(api_name)
36  return feature.get('channel') if feature else None
37
38
39class AvailabilityInfo(object):
40  '''Represents availability data for an API. |scheduled| is a version number
41  specifying when dev and beta APIs will become stable, or None if that data
42  is unknown.
43  '''
44  def __init__(self, channel_info, scheduled=None):
45    assert isinstance(channel_info, ChannelInfo)
46    assert isinstance(scheduled, int) or scheduled is None
47    self.channel_info = channel_info
48    self.scheduled = scheduled
49
50  def __eq__(self, other):
51    return self.__dict__ == other.__dict__
52
53  def __ne__(self, other):
54    return not (self == other)
55
56  def __repr__(self):
57    return '%s%s' % (type(self).__name__, repr(self.__dict__))
58
59  def __str__(self):
60    return repr(self)
61
62
63class AvailabilityFinder(object):
64  '''Generates availability information for APIs by looking at API schemas and
65  _features files over multiple release versions of Chrome.
66  '''
67
68  def __init__(self,
69               branch_utility,
70               compiled_fs_factory,
71               file_system_iterator,
72               host_file_system,
73               object_store_creator):
74    self._branch_utility = branch_utility
75    self._compiled_fs_factory = compiled_fs_factory
76    self._file_system_iterator = file_system_iterator
77    self._host_file_system = host_file_system
78    self._object_store_creator = object_store_creator
79    def create_object_store(category):
80      return object_store_creator.Create(AvailabilityFinder, category=category)
81    self._top_level_object_store = create_object_store('top_level')
82    self._node_level_object_store = create_object_store('node_level')
83    self._json_fs = compiled_fs_factory.ForJson(self._host_file_system)
84
85  def _GetPredeterminedAvailability(self, api_name):
86    '''Checks a configuration file for hardcoded (i.e. predetermined)
87    availability information for an API.
88    '''
89    api_info = self._json_fs.GetFromFile(
90        JSON_TEMPLATES + 'api_availabilities.json').Get().get(api_name)
91    if api_info is None:
92      return None
93    if api_info['channel'] == 'stable':
94      return AvailabilityInfo(
95          self._branch_utility.GetStableChannelInfo(api_info['version']))
96    return AvailabilityInfo(
97        self._branch_utility.GetChannelInfo(api_info['channel']))
98
99  def _GetApiSchemaFilename(self, api_name, file_system, version):
100    '''Gets the name of the file which may contain the schema for |api_name| in
101    |file_system|, or None if the API is not found. Note that this may be the
102    single _EXTENSION_API file which all APIs share in older versions of Chrome,
103    in which case it is unknown whether the API actually exists there.
104    '''
105    if version == 'trunk' or version > _ORIGINAL_FEATURES_MIN_VERSION:
106      # API schema filenames switch format to unix_hacker_style.
107      api_name = UnixName(api_name)
108
109    found_files = file_system.Read(API_PATHS, skip_not_found=True)
110    for path, filenames in found_files.Get().iteritems():
111      try:
112        for ext in ('json', 'idl'):
113          filename = '%s.%s' % (api_name, ext)
114          if filename in filenames:
115            return path + filename
116          if _EXTENSION_API in filenames:
117            return path + _EXTENSION_API
118      except FileNotFoundError:
119        pass
120    return None
121
122  def _GetApiSchema(self, api_name, file_system, version):
123    '''Searches |file_system| for |api_name|'s API schema data, and processes
124    and returns it if found.
125    '''
126    api_filename = self._GetApiSchemaFilename(api_name, file_system, version)
127    if api_filename is None:
128      # No file for the API could be found in the given |file_system|.
129      return None
130
131    schema_fs = self._compiled_fs_factory.ForApiSchema(file_system)
132    api_schemas = schema_fs.GetFromFile(api_filename).Get()
133    matching_schemas = [api for api in api_schemas
134                        if api['namespace'] == api_name]
135    # There should only be a single matching schema per file, or zero in the
136    # case of no API data being found in _EXTENSION_API.
137    assert len(matching_schemas) <= 1
138    return matching_schemas or None
139
140  def _HasApiSchema(self, api_name, file_system, version):
141    '''Whether or not an API schema for |api_name|exists in the given
142    |file_system|.
143    '''
144    filename = self._GetApiSchemaFilename(api_name, file_system, version)
145    if filename is None:
146      return False
147    if filename.endswith(_EXTENSION_API):
148      return self._GetApiSchema(api_name, file_system, version) is not None
149    return True
150
151  def _CheckStableAvailability(self, api_name, file_system, version):
152    '''Checks for availability of an API, |api_name|, on the stable channel.
153    Considers several _features.json files, file system existence, and
154    extension_api.json depending on the given |version|.
155    '''
156    if version < _SVN_MIN_VERSION:
157      # SVN data isn't available below this version.
158      return False
159    features_bundle = self._CreateFeaturesBundle(file_system)
160    available_channel = None
161    if version >= _API_FEATURES_MIN_VERSION:
162      # The _api_features.json file first appears in version 28 and should be
163      # the most reliable for finding API availability.
164      available_channel = self._GetChannelFromApiFeatures(api_name,
165                                                          features_bundle)
166    if version >= _ORIGINAL_FEATURES_MIN_VERSION:
167      # The _permission_features.json and _manifest_features.json files are
168      # present in Chrome 20 and onwards. Use these if no information could be
169      # found using _api_features.json.
170      available_channel = (
171          available_channel or
172          self._GetChannelFromPermissionFeatures(api_name, features_bundle) or
173          self._GetChannelFromManifestFeatures(api_name, features_bundle))
174      if available_channel is not None:
175        return available_channel == 'stable'
176    if version >= _SVN_MIN_VERSION:
177      # Fall back to a check for file system existence if the API is not
178      # stable in any of the _features.json files, or if the _features files
179      # do not exist (version 19 and earlier).
180      return self._HasApiSchema(api_name, file_system, version)
181
182  def _CheckChannelAvailability(self, api_name, file_system, channel_info):
183    '''Searches through the _features files in a given |file_system|, falling
184    back to checking the file system for API schema existence, to determine
185    whether or not an API is available on the given channel, |channel_info|.
186    '''
187    features_bundle = self._CreateFeaturesBundle(file_system)
188    available_channel = (
189        self._GetChannelFromApiFeatures(api_name, features_bundle) or
190        self._GetChannelFromPermissionFeatures(api_name, features_bundle) or
191        self._GetChannelFromManifestFeatures(api_name, features_bundle))
192    if (available_channel is None and
193        self._HasApiSchema(api_name, file_system, channel_info.version)):
194      # If an API is not represented in any of the _features files, but exists
195      # in the filesystem, then assume it is available in this version.
196      # The chrome.windows API is an example of this.
197      available_channel = channel_info.channel
198    # If the channel we're checking is the same as or newer than the
199    # |available_channel| then the API is available at this channel.
200    newest = BranchUtility.NewestChannel((available_channel,
201                                          channel_info.channel))
202    return available_channel is not None and newest == channel_info.channel
203
204  @memoize
205  def _CreateFeaturesBundle(self, file_system):
206    return FeaturesBundle(file_system,
207                          self._compiled_fs_factory,
208                          self._object_store_creator)
209
210  def _GetChannelFromApiFeatures(self, api_name, features_bundle):
211    return _GetChannelFromFeatures(api_name, features_bundle.GetAPIFeatures())
212
213  def _GetChannelFromManifestFeatures(self, api_name, features_bundle):
214    # _manifest_features.json uses unix_style API names.
215    api_name = UnixName(api_name)
216    return _GetChannelFromFeatures(api_name,
217                                   features_bundle.GetManifestFeatures())
218
219  def _GetChannelFromPermissionFeatures(self, api_name, features_bundle):
220    return _GetChannelFromFeatures(api_name,
221                                   features_bundle.GetPermissionFeatures())
222
223  def _CheckApiAvailability(self, api_name, file_system, channel_info):
224    '''Determines the availability for an API at a certain version of Chrome.
225    Two branches of logic are used depending on whether or not the API is
226    determined to be 'stable' at the given version.
227    '''
228    if channel_info.channel == 'stable':
229      return self._CheckStableAvailability(api_name,
230                                           file_system,
231                                           channel_info.version)
232    return self._CheckChannelAvailability(api_name,
233                                          file_system,
234                                          channel_info)
235
236  def _FindScheduled(self, api_name):
237    '''Determines the earliest version of Chrome where the API is stable.
238    Unlike the code in GetApiAvailability, this checks if the API is stable
239    even when Chrome is in dev or beta, which shows that the API is scheduled
240    to be stable in that verison of Chrome.
241    '''
242    def check_scheduled(file_system, channel_info):
243      return self._CheckStableAvailability(
244          api_name, file_system, channel_info.version)
245
246    stable_channel = self._file_system_iterator.Descending(
247        self._branch_utility.GetChannelInfo('dev'), check_scheduled)
248
249    return stable_channel.version if stable_channel else None
250
251  def GetApiAvailability(self, api_name):
252    '''Performs a search for an API's top-level availability by using a
253    HostFileSystemIterator instance to traverse multiple version of the
254    SVN filesystem.
255    '''
256    availability = self._top_level_object_store.Get(api_name).Get()
257    if availability is not None:
258      return availability
259
260    # Check for predetermined availability and cache this information if found.
261    availability = self._GetPredeterminedAvailability(api_name)
262    if availability is not None:
263      self._top_level_object_store.Set(api_name, availability)
264      return availability
265
266    def check_api_availability(file_system, channel_info):
267      return self._CheckApiAvailability(api_name, file_system, channel_info)
268
269    channel_info = self._file_system_iterator.Descending(
270        self._branch_utility.GetChannelInfo('dev'),
271        check_api_availability)
272    if channel_info is None:
273      # The API wasn't available on 'dev', so it must be a 'trunk'-only API.
274      channel_info = self._branch_utility.GetChannelInfo('trunk')
275
276    # If the API is not stable, check when it will be scheduled to be stable.
277    if channel_info.channel == 'stable':
278      scheduled = None
279    else:
280      scheduled = self._FindScheduled(api_name)
281
282    availability = AvailabilityInfo(channel_info, scheduled=scheduled)
283
284    self._top_level_object_store.Set(api_name, availability)
285    return availability
286
287  def GetApiNodeAvailability(self, api_name):
288    '''Returns an APISchemaGraph annotated with each node's availability (the
289    ChannelInfo at the oldest channel it's available in).
290    '''
291    availability_graph = self._node_level_object_store.Get(api_name).Get()
292    if availability_graph is not None:
293      return availability_graph
294
295    def assert_not_none(value):
296      assert value is not None
297      return value
298
299    availability_graph = APISchemaGraph()
300
301    host_fs = self._host_file_system
302    trunk_stat = assert_not_none(host_fs.Stat(self._GetApiSchemaFilename(
303        api_name, host_fs, 'trunk')))
304
305    # Weird object thing here because nonlocal is Python 3.
306    previous = type('previous', (object,), {'stat': None, 'graph': None})
307
308    def update_availability_graph(file_system, channel_info):
309      version_filename = assert_not_none(self._GetApiSchemaFilename(
310          api_name, file_system, channel_info.version))
311      version_stat = assert_not_none(file_system.Stat(version_filename))
312
313      # Important optimisation: only re-parse the graph if the file changed in
314      # the last revision. Parsing the same schema and forming a graph on every
315      # iteration is really expensive.
316      if version_stat == previous.stat:
317        version_graph = previous.graph
318      else:
319        # Keep track of any new schema elements from this version by adding
320        # them to |availability_graph|.
321        #
322        # Calling |availability_graph|.Lookup() on the nodes being updated
323        # will return the |annotation| object -- the current |channel_info|.
324        version_graph = APISchemaGraph(self._GetApiSchema(
325            api_name, file_system, channel_info.version))
326        availability_graph.Update(version_graph.Subtract(availability_graph),
327                                  annotation=channel_info)
328
329      previous.stat = version_stat
330      previous.graph = version_graph
331
332      # Continue looping until there are no longer differences between this
333      # version and trunk.
334      return version_stat != trunk_stat
335
336    self._file_system_iterator.Ascending(
337        self.GetApiAvailability(api_name).channel_info,
338        update_availability_graph)
339
340    self._node_level_object_store.Set(api_name, availability_graph)
341    return availability_graph
342