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