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