availability_finder.py revision 5f1c94371a64b3196d4be9466099bb892df9b88e
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 posixpath
6
7from api_models import GetNodeCategories
8from api_schema_graph import APISchemaGraph
9from branch_utility import BranchUtility, ChannelInfo
10from compiled_file_system import CompiledFileSystem, SingleFile, Unicode
11from extensions_paths import API_PATHS, JSON_TEMPLATES
12from features_bundle import FeaturesBundle
13from file_system import FileNotFoundError
14from schema_util import ProcessSchema
15from third_party.json_schema_compiler.memoize import memoize
16from third_party.json_schema_compiler.model import UnixName
17
18
19_DEVTOOLS_API = 'devtools_api.json'
20_EXTENSION_API = 'extension_api.json'
21# The version where api_features.json is first available.
22_API_FEATURES_MIN_VERSION = 28
23# The version where permission_ and manifest_features.json are available and
24# presented in the current format.
25_ORIGINAL_FEATURES_MIN_VERSION = 20
26# API schemas are aggregated in extension_api.json up to this version.
27_EXTENSION_API_MAX_VERSION = 17
28# The earliest version for which we have SVN data.
29_SVN_MIN_VERSION = 5
30
31
32def _GetChannelFromFeatures(api_name, features):
33  '''Finds API channel information for |api_name| from |features|.
34  Returns None if channel information for the API cannot be located.
35  '''
36  feature = features.Get().get(api_name)
37  return feature.get('channel') if feature else None
38
39
40def _GetChannelFromAPIFeatures(api_name, features_bundle):
41  return _GetChannelFromFeatures(api_name, features_bundle.GetAPIFeatures())
42
43
44def _GetChannelFromManifestFeatures(api_name, features_bundle):
45  # _manifest_features.json uses unix_style API names.
46  api_name = UnixName(api_name)
47  return _GetChannelFromFeatures(api_name,
48                                 features_bundle.GetManifestFeatures())
49
50
51def _GetChannelFromPermissionFeatures(api_name, features_bundle):
52  return _GetChannelFromFeatures(api_name,
53                                 features_bundle.GetPermissionFeatures())
54
55
56def _GetAPISchemaFilename(api_name, file_system, version):
57  '''Gets the name of the file which may contain the schema for |api_name| in
58  |file_system|, or None if the API is not found. Note that this may be the
59  single _EXTENSION_API file which all APIs share in older versions of Chrome,
60  in which case it is unknown whether the API actually exists there.
61  '''
62  if version == 'trunk' or version > _ORIGINAL_FEATURES_MIN_VERSION:
63    # API schema filenames switch format to unix_hacker_style.
64    api_name = UnixName(api_name)
65
66  # Devtools API names have 'devtools.' prepended to them.
67  # The corresponding filenames do not.
68  if 'devtools_' in api_name:
69    api_name = api_name.replace('devtools_', '')
70
71  for api_path in API_PATHS:
72    try:
73      for base, _, filenames in file_system.Walk(api_path):
74        for ext in ('json', 'idl'):
75          filename = '%s.%s' % (api_name, ext)
76          if filename in filenames:
77            return posixpath.join(api_path, base, filename)
78          if _EXTENSION_API in filenames:
79            return posixpath.join(api_path, base, _EXTENSION_API)
80    except FileNotFoundError:
81      continue
82  return None
83
84
85class AvailabilityInfo(object):
86  '''Represents availability data for an API. |scheduled| is a version number
87  specifying when dev and beta APIs will become stable, or None if that data
88  is unknown.
89  '''
90  def __init__(self, channel_info, scheduled=None):
91    assert isinstance(channel_info, ChannelInfo)
92    assert isinstance(scheduled, int) or scheduled is None
93    self.channel_info = channel_info
94    self.scheduled = scheduled
95
96  def __eq__(self, other):
97    return self.__dict__ == other.__dict__
98
99  def __ne__(self, other):
100    return not (self == other)
101
102  def __repr__(self):
103    return '%s%s' % (type(self).__name__, repr(self.__dict__))
104
105  def __str__(self):
106    return repr(self)
107
108
109class AvailabilityFinder(object):
110  '''Generates availability information for APIs by looking at API schemas and
111  _features files over multiple release versions of Chrome.
112  '''
113
114  def __init__(self,
115               branch_utility,
116               compiled_fs_factory,
117               file_system_iterator,
118               host_file_system,
119               object_store_creator,
120               platform):
121    self._branch_utility = branch_utility
122    self._compiled_fs_factory = compiled_fs_factory
123    self._file_system_iterator = file_system_iterator
124    self._host_file_system = host_file_system
125    self._object_store_creator = object_store_creator
126    def create_object_store(category):
127      return object_store_creator.Create(
128          AvailabilityFinder, category='/'.join((platform, category)))
129    self._top_level_object_store = create_object_store('top_level')
130    self._node_level_object_store = create_object_store('node_level')
131    self._json_fs = compiled_fs_factory.ForJson(self._host_file_system)
132    self._platform = platform
133
134  def _GetPredeterminedAvailability(self, api_name):
135    '''Checks a configuration file for hardcoded (i.e. predetermined)
136    availability information for an API.
137    '''
138    api_info = self._json_fs.GetFromFile(
139        JSON_TEMPLATES + 'api_availabilities.json').Get().get(api_name)
140    if api_info is None:
141      return None
142    if api_info['channel'] == 'stable':
143      return AvailabilityInfo(
144          self._branch_utility.GetStableChannelInfo(api_info['version']))
145    return AvailabilityInfo(
146        self._branch_utility.GetChannelInfo(api_info['channel']))
147
148  @memoize
149  def _CreateAPISchemaFileSystem(self, file_system):
150    '''Creates a CompiledFileSystem for parsing raw JSON or IDL API schema
151    data and formatting it so that it can be used to create APISchemaGraphs.
152    '''
153    # When processing the API schemas, we retain inlined types in the schema
154    # so that there are not missing nodes in the APISchemaGraphs when trying
155    # to lookup availability.
156    def process_schema(path, data):
157      return ProcessSchema(path, data, retain_inlined_types=True)
158    return self._compiled_fs_factory.Create(file_system,
159                                            SingleFile(Unicode(process_schema)),
160                                            CompiledFileSystem,
161                                            category='api-schema')
162
163  def _GetAPISchema(self, api_name, file_system, version):
164    '''Searches |file_system| for |api_name|'s API schema data, and processes
165    and returns it if found.
166    '''
167    api_filename = _GetAPISchemaFilename(api_name, file_system, version)
168    if api_filename is None:
169      # No file for the API could be found in the given |file_system|.
170      return None
171
172    schema_fs = self._CreateAPISchemaFileSystem(file_system)
173    api_schemas = schema_fs.GetFromFile(api_filename).Get()
174    matching_schemas = [api for api in api_schemas
175                        if api['namespace'] == api_name]
176    # There should only be a single matching schema per file, or zero in the
177    # case of no API data being found in _EXTENSION_API.
178    assert len(matching_schemas) <= 1
179    return matching_schemas or None
180
181  def _HasAPISchema(self, api_name, file_system, version):
182    '''Whether or not an API schema for |api_name| exists in the given
183    |file_system|.
184    '''
185    filename = _GetAPISchemaFilename(api_name, file_system, version)
186    if filename is None:
187      return False
188    if filename.endswith(_EXTENSION_API) or filename.endswith(_DEVTOOLS_API):
189      return self._GetAPISchema(api_name, file_system, version) is not None
190    return True
191
192  def _CheckStableAvailability(self,
193                               api_name,
194                               file_system,
195                               version,
196                               earliest_version=None):
197    '''Checks for availability of an API, |api_name|, on the stable channel.
198    Considers several _features.json files, file system existence, and
199    extension_api.json depending on the given |version|.
200    |earliest_version| is the version of Chrome at which |api_name| first became
201    available. It should only be given when checking stable availability for
202    API nodes, so it can be used as an alternative to the check for filesystem
203    existence.
204    '''
205    earliest_version = earliest_version or _SVN_MIN_VERSION
206    if version < earliest_version:
207      # SVN data isn't available below this version.
208      return False
209    features_bundle = self._CreateFeaturesBundle(file_system)
210    available_channel = None
211    if version >= _API_FEATURES_MIN_VERSION:
212      # The _api_features.json file first appears in version 28 and should be
213      # the most reliable for finding API availability.
214      available_channel = _GetChannelFromAPIFeatures(api_name,
215                                                     features_bundle)
216    if version >= _ORIGINAL_FEATURES_MIN_VERSION:
217      # The _permission_features.json and _manifest_features.json files are
218      # present in Chrome 20 and onwards. Use these if no information could be
219      # found using _api_features.json.
220      available_channel = (
221          available_channel or
222          _GetChannelFromPermissionFeatures(api_name, features_bundle) or
223          _GetChannelFromManifestFeatures(api_name, features_bundle))
224      if available_channel is not None:
225        return available_channel == 'stable'
226
227    # |earliest_version| == _SVN_MIN_VERSION implies we're dealing with an API.
228    # Fall back to a check for file system existence if the API is not
229    # stable in any of the _features.json files, or if the _features files
230    # do not exist (version 19 and earlier).
231    if earliest_version == _SVN_MIN_VERSION:
232      return self._HasAPISchema(api_name, file_system, version)
233    # For API nodes, assume it's available if |version| is greater than the
234    # version the node became available (which it is, because of the first
235    # check).
236    return True
237
238  def _CheckChannelAvailability(self, api_name, file_system, channel_info):
239    '''Searches through the _features files in a given |file_system|, falling
240    back to checking the file system for API schema existence, to determine
241    whether or not an API is available on the given channel, |channel_info|.
242    '''
243    features_bundle = self._CreateFeaturesBundle(file_system)
244    available_channel = (
245        _GetChannelFromAPIFeatures(api_name, features_bundle) or
246        _GetChannelFromPermissionFeatures(api_name, features_bundle) or
247        _GetChannelFromManifestFeatures(api_name, features_bundle))
248    if (available_channel is None and
249        self._HasAPISchema(api_name, file_system, channel_info.version)):
250      # If an API is not represented in any of the _features files, but exists
251      # in the filesystem, then assume it is available in this version.
252      # The chrome.windows API is an example of this.
253      available_channel = channel_info.channel
254    # If the channel we're checking is the same as or newer than the
255    # |available_channel| then the API is available at this channel.
256    newest = BranchUtility.NewestChannel((available_channel,
257                                          channel_info.channel))
258    return available_channel is not None and newest == channel_info.channel
259
260  def _CheckChannelAvailabilityForNode(self,
261                                       node_name,
262                                       file_system,
263                                       channel_info,
264                                       earliest_channel_info):
265    '''Searches through the _features files in a given |file_system| to
266    determine whether or not an API node is available on the given channel,
267    |channel_info|. |earliest_channel_info| is the earliest channel the node
268    was introduced.
269    '''
270    features_bundle = self._CreateFeaturesBundle(file_system)
271    available_channel = None
272    # Only API nodes can have their availability overriden on a per-node basis,
273    # so we only need to check _api_features.json.
274    if channel_info.version >= _API_FEATURES_MIN_VERSION:
275      available_channel = _GetChannelFromAPIFeatures(node_name, features_bundle)
276    if (available_channel is None and
277        channel_info.version >= earliest_channel_info.version):
278      # Most API nodes inherit their availabiltity from their parent, so don't
279      # explicitly appear in _api_features.json. For example, "tabs.create"
280      # isn't listed; it inherits from "tabs". Assume these are available at
281      # |channel_info|.
282      available_channel = channel_info.channel
283    newest = BranchUtility.NewestChannel((available_channel,
284                                          channel_info.channel))
285    return available_channel is not None and newest == channel_info.channel
286
287  @memoize
288  def _CreateFeaturesBundle(self, file_system):
289    return FeaturesBundle(file_system,
290                          self._compiled_fs_factory,
291                          self._object_store_creator,
292                          self._platform)
293
294  def _CheckAPIAvailability(self, api_name, file_system, channel_info):
295    '''Determines the availability for an API at a certain version of Chrome.
296    Two branches of logic are used depending on whether or not the API is
297    determined to be 'stable' at the given version.
298    '''
299    if channel_info.channel == 'stable':
300      return self._CheckStableAvailability(api_name,
301                                           file_system,
302                                           channel_info.version)
303    return self._CheckChannelAvailability(api_name,
304                                          file_system,
305                                          channel_info)
306
307  def _FindScheduled(self, api_name, earliest_version=None):
308    '''Determines the earliest version of Chrome where the API is stable.
309    Unlike the code in GetAPIAvailability, this checks if the API is stable
310    even when Chrome is in dev or beta, which shows that the API is scheduled
311    to be stable in that verison of Chrome. |earliest_version| is the version
312    |api_name| became first available. Only use it when finding scheduled
313    availability for nodes.
314    '''
315    def check_scheduled(file_system, channel_info):
316      return self._CheckStableAvailability(api_name,
317                                           file_system,
318                                           channel_info.version,
319                                           earliest_version=earliest_version)
320
321    stable_channel = self._file_system_iterator.Descending(
322        self._branch_utility.GetChannelInfo('dev'), check_scheduled)
323
324    return stable_channel.version if stable_channel else None
325
326  def _CheckAPINodeAvailability(self, node_name, earliest_channel_info):
327    '''Gets availability data for a node by checking _features files.
328    '''
329    def check_node_availability(file_system, channel_info):
330      return self._CheckChannelAvailabilityForNode(node_name,
331                                                   file_system,
332                                                   channel_info,
333                                                   earliest_channel_info)
334    channel_info = (self._file_system_iterator.Descending(
335        self._branch_utility.GetChannelInfo('dev'), check_node_availability) or
336        earliest_channel_info)
337
338    if channel_info.channel == 'stable':
339      scheduled = None
340    else:
341      scheduled = self._FindScheduled(
342          node_name,
343          earliest_version=earliest_channel_info.version)
344
345    return AvailabilityInfo(channel_info, scheduled=scheduled)
346
347  def GetAPIAvailability(self, api_name):
348    '''Performs a search for an API's top-level availability by using a
349    HostFileSystemIterator instance to traverse multiple version of the
350    SVN filesystem.
351    '''
352    availability = self._top_level_object_store.Get(api_name).Get()
353    if availability is not None:
354      return availability
355
356    # Check for predetermined availability and cache this information if found.
357    availability = self._GetPredeterminedAvailability(api_name)
358    if availability is not None:
359      self._top_level_object_store.Set(api_name, availability)
360      return availability
361
362    def check_api_availability(file_system, channel_info):
363      return self._CheckAPIAvailability(api_name, file_system, channel_info)
364
365    channel_info = self._file_system_iterator.Descending(
366        self._branch_utility.GetChannelInfo('dev'),
367        check_api_availability)
368    if channel_info is None:
369      # The API wasn't available on 'dev', so it must be a 'trunk'-only API.
370      channel_info = self._branch_utility.GetChannelInfo('trunk')
371
372    # If the API is not stable, check when it will be scheduled to be stable.
373    if channel_info.channel == 'stable':
374      scheduled = None
375    else:
376      scheduled = self._FindScheduled(api_name)
377
378    availability = AvailabilityInfo(channel_info, scheduled=scheduled)
379
380    self._top_level_object_store.Set(api_name, availability)
381    return availability
382
383  def GetAPINodeAvailability(self, api_name):
384    '''Returns an APISchemaGraph annotated with each node's availability (the
385    ChannelInfo at the oldest channel it's available in).
386    '''
387    availability_graph = self._node_level_object_store.Get(api_name).Get()
388    if availability_graph is not None:
389      return availability_graph
390
391    def assert_not_none(value):
392      assert value is not None
393      return value
394
395    availability_graph = APISchemaGraph()
396    host_fs = self._host_file_system
397    trunk_stat = assert_not_none(host_fs.Stat(_GetAPISchemaFilename(
398        api_name, host_fs, 'trunk')))
399
400    # Weird object thing here because nonlocal is Python 3.
401    previous = type('previous', (object,), {'stat': None, 'graph': None})
402
403    def update_availability_graph(file_system, channel_info):
404      # If we can't find a filename, skip checking at this branch.
405      # For example, something could have a predetermined availability of 23,
406      # but it doesn't show up in the file system until 26.
407      # We know that the file will become available at some point.
408      #
409      # The problem with this is that at the first version where the API file
410      # exists, we'll get a huge chunk of new objects that don't match
411      # the predetermined API availability.
412      version_filename = _GetAPISchemaFilename(api_name,
413                                               file_system,
414                                               channel_info.version)
415      if version_filename is None:
416        # Continue the loop at the next version.
417        return True
418
419      version_stat = assert_not_none(file_system.Stat(version_filename))
420
421      # Important optimisation: only re-parse the graph if the file changed in
422      # the last revision. Parsing the same schema and forming a graph on every
423      # iteration is really expensive.
424      if version_stat == previous.stat:
425        version_graph = previous.graph
426      else:
427        # Keep track of any new schema elements from this version by adding
428        # them to |availability_graph|.
429        #
430        # Calling |availability_graph|.Lookup() on the nodes being updated
431        # will return the |annotation| object -- the current |channel_info|.
432        version_graph = APISchemaGraph(
433            api_schema=self._GetAPISchema(api_name,
434                                          file_system,
435                                          channel_info.version))
436        def annotator(node_name):
437          return self._CheckAPINodeAvailability('%s.%s' % (api_name, node_name),
438                                                channel_info)
439
440        availability_graph.Update(version_graph.Subtract(availability_graph),
441                                  annotator)
442
443      previous.stat = version_stat
444      previous.graph = version_graph
445
446      # Continue looping until there are no longer differences between this
447      # version and trunk.
448      return version_stat != trunk_stat
449
450    self._file_system_iterator.Ascending(
451        self.GetAPIAvailability(api_name).channel_info,
452        update_availability_graph)
453
454    self._node_level_object_store.Set(api_name, availability_graph)
455    return availability_graph
456