Adding AvailabilityFinder to Doc Server.

This is a large part of automatically generating API intro tables.
This patch does not connect AvailabilityFinder with the rest of the server; a future patch will take care of that.

BUG=233968

Review URL: https://chromiumcodereview.appspot.com/17397010

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@208075 0039d316-1c4b-4281-b951-d872f2087c98
parent 15fe3270
application: chrome-apps-doc
version: 2-7-1
version: 2-7-2
runtime: python27
api_version: 1
threadsafe: false
......
# Copyright 2013 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import collections
import os
from branch_utility import BranchUtility
from compiled_file_system import CompiledFileSystem
from file_system import FileNotFoundError
import svn_constants
from third_party.json_schema_compiler import json_parse, model
from third_party.json_schema_compiler.memoize import memoize
_API_AVAILABILITIES = svn_constants.JSON_PATH + '/api_availabilities.json'
_API_FEATURES = svn_constants.API_PATH + '/_api_features.json'
_EXTENSION_API = svn_constants.API_PATH + '/extension_api.json'
_MANIFEST_FEATURES = svn_constants.API_PATH + '/_manifest_features.json'
_PERMISSION_FEATURES = svn_constants.API_PATH + '/_permission_features.json'
_STABLE = 'stable'
class AvailabilityInfo(object):
def __init__(self, channel, version):
self.channel = channel
self.version = version
def _GetChannelFromFeatures(api_name, file_system, path):
'''Finds API channel information within _features.json files at the given
|path| for the given |file_system|. Returns None if channel information for
the API cannot be located.
'''
feature = file_system.GetFromFile(path).get(api_name)
if feature is None:
return None
if isinstance(feature, collections.Mapping):
# The channel information dict is nested within a list for whitelisting
# purposes.
return feature.get('channel')
# Features can contain a list of entries. Take the newest branch.
return BranchUtility.NewestChannel(entry.get('channel')
for entry in feature)
def _GetChannelFromApiFeatures(api_name, file_system):
try:
return _GetChannelFromFeatures(api_name, file_system, _API_FEATURES)
except FileNotFoundError as e:
# TODO(epeterson) Remove except block once _api_features is in all channels.
return None
def _GetChannelFromPermissionFeatures(api_name, file_system):
return _GetChannelFromFeatures(api_name, file_system, _PERMISSION_FEATURES)
def _GetChannelFromManifestFeatures(api_name, file_system):
return _GetChannelFromFeatures(#_manifest_features uses unix_style API names
model.UnixName(api_name),
file_system,
_MANIFEST_FEATURES)
def _ExistsInFileSystem(api_name, file_system):
'''Checks for existence of |api_name| within the list of files in the api/
directory found using the given file system.
'''
file_names = file_system.GetFromFileListing(svn_constants.API_PATH)
# File names switch from unix_hacker_style to camelCase at versions <= 20.
return model.UnixName(api_name) in file_names or api_name in file_names
def _ExistsInExtensionApi(api_name, file_system):
'''Parses the api/extension_api.json file (available in Chrome versions
before 18) for an API namespace. If this is successfully found, then the API
is considered to have been 'stable' for the given version.
'''
try:
extension_api_json = file_system.GetFromFile(_EXTENSION_API)
api_rows = [row.get('namespace') for row in extension_api_json
if 'namespace' in row]
return True if api_name in api_rows else False
except FileNotFoundError as e:
# This should only happen on preview.py since extension_api.json is no
# longer present in trunk.
return False
class AvailabilityFinder(object):
'''Uses API data sources generated by a ChromeVersionDataSource in order to
search the filesystem for the earliest existence of a specified API throughout
the different versions of Chrome; this constitutes an API's availability.
'''
class Factory(object):
def __init__(self,
object_store_creator,
compiled_host_fs_factory,
branch_utility,
# Takes a |version|, and returns a caching offline or online
# subversion file system for that version.
create_file_system_at_version):
self._object_store_creator = object_store_creator
self._compiled_host_fs_factory = compiled_host_fs_factory
self._branch_utility = branch_utility
self._create_file_system_at_version = create_file_system_at_version
def Create(self):
return AvailabilityFinder(self._object_store_creator,
self._compiled_host_fs_factory,
self._branch_utility,
self._create_file_system_at_version)
def __init__(self,
object_store_creator,
compiled_host_fs_factory,
branch_utility,
create_file_system_at_version):
self._object_store_creator = object_store_creator
self._json_cache = compiled_host_fs_factory.Create(
lambda _, json: json_parse.Parse(json),
AvailabilityFinder,
'json-cache')
self._branch_utility = branch_utility
self._create_file_system_at_version = create_file_system_at_version
self._object_store = object_store_creator.Create(AvailabilityFinder)
@memoize
def _CreateFeaturesAndNamesFileSystems(self, version):
'''The 'features' compiled file system's populate function parses and
returns the contents of a _features.json file. The 'names' compiled file
system's populate function creates a list of file names with .json or .idl
extensions.
'''
fs_factory = CompiledFileSystem.Factory(
self._create_file_system_at_version(version),
self._object_store_creator)
features_fs = fs_factory.Create(lambda _, json: json_parse.Parse(json),
AvailabilityFinder,
category='features')
names_fs = fs_factory.Create(self._GetExtNames,
AvailabilityFinder,
category='names')
return (features_fs, names_fs)
def _GetExtNames(self, base_path, apis):
return [os.path.splitext(api)[0] for api in apis
if os.path.splitext(api)[1][1:] in ['json', 'idl']]
def _FindEarliestStableAvailability(self, api_name, version):
'''Searches in descending order through filesystem caches tied to specific
chrome version numbers and looks for the availability of an API, |api_name|,
on the stable channel. When a version is found where the API is no longer
available on stable, returns the previous version number (the last known
version where the API was stable).
'''
available = True
while available:
if version < 5:
# SVN data isn't available below version 5.
return version + 1
available = False
features_fs, names_fs = self._CreateFeaturesAndNamesFileSystems(version)
if version >= 28:
# The _api_features.json file first appears in version 28 and should be
# the most reliable for finding API availabilities, so it gets checked
# first. The _permission_features.json and _manifest_features.json files
# are present in Chrome 20 and onwards. Fall back to a check for file
# system existence if the API is not stable in any of the _features.json
# files.
available = _GetChannelFromApiFeatures(api_name, features_fs) == _STABLE
if version >= 20:
# Check other _features.json files/file existence if the API wasn't
# found in _api_features.json, or if _api_features.json wasn't present.
available = available or (
_GetChannelFromPermissionFeatures(api_name, features_fs) == _STABLE
or _GetChannelFromManifestFeatures(api_name, features_fs) == _STABLE
or _ExistsInFileSystem(api_name, names_fs))
elif version >= 18:
# These versions are a little troublesome. Version 19 has
# _permission_features.json, but it lacks 'channel' information.
# Version 18 lacks all of the _features.json files. For now, we're using
# a simple check for filesystem existence here.
available = _ExistsInFileSystem(api_name, names_fs)
elif version >= 5:
# Versions 17 and down to 5 have an extension_api.json file which
# contains namespaces for each API that was available at the time. We
# can use this file to check for API existence.
available = _ExistsInExtensionApi(api_name, features_fs)
if not available:
return version + 1
version -= 1
def _GetAvailableChannelForVersion(self, api_name, version):
'''Searches through the _features files for a given |version| and returns
the channel that the given API is determined to be available on.
'''
features_fs, names_fs = self._CreateFeaturesAndNamesFileSystems(version)
channel = (_GetChannelFromApiFeatures(api_name, features_fs)
or _GetChannelFromPermissionFeatures(api_name, features_fs)
or _GetChannelFromManifestFeatures(api_name, features_fs))
if channel is None and _ExistsInFileSystem(api_name, names_fs):
# If an API is not represented in any of the _features files, but exists
# in the filesystem, then assume it is available in this version.
# The windows API is an example of this.
return self._branch_utility.GetChannelForVersion(version)
return channel
def GetApiAvailability(self, api_name):
'''Determines the availability for an API by testing several scenarios.
(i.e. Is the API experimental? Only available on certain development
channels? If it's stable, when did it first become stable? etc.)
'''
availability = self._object_store.Get(api_name).Get()
if availability is not None:
return availability
# Check for a predetermined availability for this API.
api_info = self._json_cache.GetFromFile(_API_AVAILABILITIES).get(api_name)
if api_info is not None:
channel = api_info.get('channel')
return AvailabilityInfo(
channel,
api_info.get('version') if channel == _STABLE else None)
# Check for the API in the development channels.
availability = None
for channel_info in self._branch_utility.GetAllChannelInfo():
available_channel = self._GetAvailableChannelForVersion(
api_name,
channel_info.version)
# If the |available_channel| for the API is the same as, or older than,
# the channel we're checking, then the API is available on this channel.
if (available_channel is not None and
BranchUtility.NewestChannel((available_channel, channel_info.channel))
== channel_info.channel):
availability = AvailabilityInfo(channel_info.channel,
channel_info.version)
break
# The API should at least be available on trunk. It's a bug otherwise.
assert availability, 'No availability found for %s' % api_name
# If the API is in stable, find the chrome version in which it became
# stable.
if availability.channel == _STABLE:
availability.version = self._FindEarliestStableAvailability(
api_name,
availability.version)
self._object_store.Set(api_name, availability)
return availability
#!/usr/bin/env python
# Copyright 2013 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import os
import sys
import unittest
from availability_finder import AvailabilityFinder
from branch_utility import BranchUtility
from compiled_file_system import CompiledFileSystem
from fake_url_fetcher import FakeUrlFetcher
from object_store_creator import ObjectStoreCreator
from test_file_system import TestFileSystem
from test_data.canned_data import (CANNED_API_FILE_SYSTEM_DATA, CANNED_BRANCHES)
def _CreateCannedFileSystem(version):
branch = CANNED_BRANCHES[version]
return TestFileSystem(CANNED_API_FILE_SYSTEM_DATA[str(branch)])
class AvailabilityFinderTest(unittest.TestCase):
def setUp(self):
self._avail_ds_factory = AvailabilityFinder.Factory(
ObjectStoreCreator.ForTest(),
CompiledFileSystem.Factory(
TestFileSystem(CANNED_API_FILE_SYSTEM_DATA['trunk']),
ObjectStoreCreator.ForTest()),
BranchUtility(
os.path.join('branch_utility', 'first.json'),
os.path.join('branch_utility', 'second.json'),
FakeUrlFetcher(os.path.join(sys.path[0], 'test_data')),
ObjectStoreCreator.ForTest()),
_CreateCannedFileSystem)
self._avail_ds = self._avail_ds_factory.Create()
def testGetApiAvailability(self):
# Key: Using 'channel' (i.e. 'beta') to represent an availability listing
# for an API in a _features.json file, and using |channel| (i.e. |dev|) to
# represent the development channel, or phase of development, where an API's
# availability is being checked.
# Testing the predetermined APIs found in
# templates/json/api_availabilities.json.
self.assertEqual('stable',
self._avail_ds.GetApiAvailability('jsonAPI1').channel)
self.assertEqual(10,
self._avail_ds.GetApiAvailability('jsonAPI1').version)
self.assertEqual('trunk',
self._avail_ds.GetApiAvailability('jsonAPI2').channel)
self.assertEqual(None,
self._avail_ds.GetApiAvailability('jsonAPI2').version)
self.assertEqual('dev',
self._avail_ds.GetApiAvailability('jsonAPI3').channel)
self.assertEqual(None,
self._avail_ds.GetApiAvailability('jsonAPI3').version)
# Testing APIs found only by checking file system existence.
self.assertEquals('stable',
self._avail_ds.GetApiAvailability('windows').channel)
self.assertEquals(23,
self._avail_ds.GetApiAvailability('windows').version)
self.assertEquals('stable',
self._avail_ds.GetApiAvailability('tabs').channel)
self.assertEquals(18,
self._avail_ds.GetApiAvailability('tabs').version)
self.assertEquals('stable',
self._avail_ds.GetApiAvailability('input.ime').channel)
self.assertEquals(18,
self._avail_ds.GetApiAvailability('input.ime').version)
# Testing API channel existence for _api_features.json.
# Listed as 'dev' on |beta|, 'dev' on |dev|.
self.assertEquals('dev',
self._avail_ds.GetApiAvailability('systemInfo.stuff').channel)
self.assertEquals(28,
self._avail_ds.GetApiAvailability('systemInfo.stuff').version)
# Listed as 'stable' on |beta|.
self.assertEquals('beta',
self._avail_ds.GetApiAvailability('systemInfo.cpu').channel)
self.assertEquals(27,
self._avail_ds.GetApiAvailability('systemInfo.cpu').version)
# Testing API channel existence for _manifest_features.json.
# Listed as 'trunk' on all channels.
self.assertEquals('trunk',
self._avail_ds.GetApiAvailability('sync').channel)
self.assertEquals('trunk',
self._avail_ds.GetApiAvailability('sync').version)
# No records of API until |trunk|.
self.assertEquals('trunk',
self._avail_ds.GetApiAvailability('history').channel)
self.assertEquals('trunk',
self._avail_ds.GetApiAvailability('history').version)
# Listed as 'dev' on |dev|.
self.assertEquals('dev',
self._avail_ds.GetApiAvailability('storage').channel)
self.assertEquals(28,
self._avail_ds.GetApiAvailability('storage').version)
# Stable in _manifest_features and into pre-18 versions.
self.assertEquals('stable',
self._avail_ds.GetApiAvailability('pageAction').channel)
self.assertEquals(8,
self._avail_ds.GetApiAvailability('pageAction').version)
# Testing API channel existence for _permission_features.json.
# Listed as 'beta' on |trunk|.
self.assertEquals('trunk',
self._avail_ds.GetApiAvailability('falseBetaAPI').version)
self.assertEquals('trunk',
self._avail_ds.GetApiAvailability('falseBetaAPI').version)
# Listed as 'trunk' on |trunk|.
self.assertEquals('trunk',
self._avail_ds.GetApiAvailability('trunkAPI').channel)
self.assertEquals('trunk',
self._avail_ds.GetApiAvailability('trunkAPI').version)
# Listed as 'trunk' on all development channels.
self.assertEquals('trunk',
self._avail_ds.GetApiAvailability('declarativeContent').channel)
self.assertEquals('trunk',
self._avail_ds.GetApiAvailability('declarativeContent').version)
# Listed as 'dev' on all development channels.
self.assertEquals('dev',
self._avail_ds.GetApiAvailability('bluetooth').channel)
self.assertEquals(28,
self._avail_ds.GetApiAvailability('bluetooth').version)
# Listed as 'dev' on |dev|.
self.assertEquals('dev',
self._avail_ds.GetApiAvailability('cookies').channel)
self.assertEquals(28,
self._avail_ds.GetApiAvailability('cookies').version)
# Treated as 'stable' APIs.
self.assertEquals('stable',
self._avail_ds.GetApiAvailability('alarms').channel)
self.assertEquals(24,
self._avail_ds.GetApiAvailability('alarms').version)
self.assertEquals('stable',
self._avail_ds.GetApiAvailability('bookmarks').channel)
self.assertEquals(21,
self._avail_ds.GetApiAvailability('bookmarks').version)
# Testing older API existence using extension_api.json.
self.assertEquals('stable',
self._avail_ds.GetApiAvailability('menus').channel)
self.assertEquals(6,
self._avail_ds.GetApiAvailability('menus').version)
self.assertEquals('stable',
self._avail_ds.GetApiAvailability('idle').channel)
self.assertEquals(5,
self._avail_ds.GetApiAvailability('idle').version)
# Switches between _features.json files across branches.
# Listed as 'trunk' on all channels, in _api, _permission, or _manifest.
self.assertEquals('trunk',
self._avail_ds.GetApiAvailability('contextMenus').channel)
self.assertEquals('trunk',
self._avail_ds.GetApiAvailability('contextMenus').version)
# Moves between _permission and _manifest as file system is traversed.
self.assertEquals('stable',
self._avail_ds.GetApiAvailability('systemInfo.display').channel)
self.assertEquals(23,
self._avail_ds.GetApiAvailability('systemInfo.display').version)
self.assertEquals('stable',
self._avail_ds.GetApiAvailability('webRequest').channel)
self.assertEquals(17,
self._avail_ds.GetApiAvailability('webRequest').version)
# Mid-upgrade cases:
# Listed as 'dev' on |beta| and 'beta' on |dev|.
self.assertEquals('dev',
self._avail_ds.GetApiAvailability('notifications').channel)
self.assertEquals(28,
self._avail_ds.GetApiAvailability('notifications').version)
# Listed as 'beta' on |stable|, 'dev' on |beta| ... until |stable| on trunk.
self.assertEquals('trunk',
self._avail_ds.GetApiAvailability('events').channel)
self.assertEquals('trunk',
self._avail_ds.GetApiAvailability('events').version)
if __name__ == '__main__':
unittest.main()
......@@ -137,14 +137,23 @@ class BranchUtility(object):
version_json = json.loads(self._history_result.Get().content)
for entry in version_json['events']:
# Here, entry['title'] looks like: 'title - version#.#.branch#.#'
# Here, entry['title'] looks like: '<title> - <version>.##.<branch>.##'
version_title = entry['title'].split(' - ')[1].split('.')
if version_title[0] == str(version):
self._branch_object_store.Set(str(version), version_title[2])
return int(version_title[2])
raise ValueError(
'The branch for %s could not be found.' % version)
raise ValueError('The branch for %s could not be found.' % version)
def GetChannelForVersion(self, version):
'''Returns the name of the development channel corresponding to a given
version number.
'''
for channel_info in self.GetAllChannelInfo():
if channel_info.channel == 'stable' and version <= channel_info.version:
return channel_info.channel
if version == channel_info.version:
return channel_info.channel
def GetLatestVersionNumber(self):
'''Returns the most recent version number found using data stored on
......
......@@ -120,5 +120,26 @@ class BranchUtilityTest(unittest.TestCase):
self.assertEquals(396,
self._branch_util.GetBranchForVersion(5))
def testGetChannelForVersion(self):
self.assertEquals('trunk',
self._branch_util.GetChannelForVersion('trunk'))
self.assertEquals('dev',
self._branch_util.GetChannelForVersion(28))
self.assertEquals('beta',
self._branch_util.GetChannelForVersion(27))
self.assertEquals('stable',
self._branch_util.GetChannelForVersion(26))
self.assertEquals('stable',
self._branch_util.GetChannelForVersion(22))
self.assertEquals('stable',
self._branch_util.GetChannelForVersion(18))
self.assertEquals('stable',
self._branch_util.GetChannelForVersion(14))
self.assertEquals(None,
self._branch_util.GetChannelForVersion(30))
self.assertEquals(None,
self._branch_util.GetChannelForVersion(42))
if __name__ == '__main__':
unittest.main()
......@@ -2,19 +2,19 @@ cron:
- description: Load everything for trunk.
url: /_cron/trunk
schedule: every 5 minutes
target: 2-7-1
target: 2-7-2
- description: Load everything for dev.
url: /_cron/dev
schedule: every 5 minutes
target: 2-7-1
target: 2-7-2
- description: Load everything for beta.
url: /_cron/beta
schedule: every 5 minutes
target: 2-7-1
target: 2-7-2
- description: Load everything for stable.
url: /_cron/stable
schedule: every 5 minutes
target: 2-7-1
target: 2-7-2
......@@ -27,7 +27,7 @@ class _TestDelegate(CronServlet.Delegate):
self._app_version = GetAppVersion()
def CreateBranchUtility(self, object_store_creator):
return TestBranchUtility()
return TestBranchUtility.CreateWithCannedData()
def CreateHostFileSystemForBranchAndRevision(self, branch, revision):
file_system = self._create_file_system(revision)
......
......@@ -19,7 +19,7 @@ class _TestDelegate(InstanceServlet.Delegate):
self._file_system_type = file_system_type
def CreateBranchUtility(self, object_store_creator):
return TestBranchUtility()
return TestBranchUtility.CreateWithCannedData()
def CreateHostFileSystemForBranch(self, branch):
return self._file_system_type()
......
......@@ -3,19 +3,38 @@
# found in the LICENSE file.
from branch_utility import BranchUtility, ChannelInfo
from test_data.canned_data import (CANNED_BRANCHES, CANNED_CHANNELS)
class TestBranchUtility(object):
'''Mimics BranchUtility to return valid-ish data without needing omahaproxy
data.
'''
def __init__(self, branches, channels):
''' Parameters: |branches| is a mapping of versions to branches, and
|channels| is a mapping of channels to versions.
'''
self._branches = branches
self._channels = channels
@staticmethod
def CreateWithCannedData():
'''Returns a TestBranchUtility that uses 'canned' test data pulled from
older branches of SVN data.
'''
return TestBranchUtility(CANNED_BRANCHES, CANNED_CHANNELS)
def GetAllChannelInfo(self):
return [self.GetChannelInfo(channel)
for channel in BranchUtility.GetAllChannelNames()]
def GetChannelInfo(self, channel):
return ChannelInfo(channel,
'fakebranch-%s' % channel,
'fakeversion-%s' % channel)
version = self._channels[channel]
return ChannelInfo(channel, self.GetBranchForVersion(version), version)
def GetBranchForVersion(self, version):
return 'fakebranch-%s' % version
return self._branches[version]
def GetChannelForVersion(self, version):
for channel in self._channels.iterkeys():
if self._channels[channel] == version:
return channel
# Copyright 2013 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import json
CANNED_CHANNELS = {
'trunk': 'trunk',
'dev': 28,
'beta': 27,
'stable': 26
}
CANNED_BRANCHES = {
'trunk': 'trunk',
28: 1500,
27: 1453,
26: 1410,
25: 1364,
24: 1312,
23: 1271,
22: 1229,
21: 1180,
20: 1132,
19: 1084,
18: 1025,
17: 963,
16: 912,
15: 874,
14: 835,
13: 782,
12: 742,
11: 696,
10: 648,
9: 597,
8: 552,
7: 544,
6: 495,
5: 396
}
CANNED_TEST_FILE_SYSTEM_DATA = {
'api': {
'_manifest_features.json': json.dumps({
'manifest': 'features'
}),
'_permission_features.json': json.dumps({
'permission': 'features'
})
},
'docs': {
'templates': {
'intros': {
'test.html': '<h1>hi</h1>you<h2>first</h2><h3>inner</h3><h2>second</h2>'
},
'json': {
'api_availabilities.json': json.dumps({
'tester': {
'channel': 'stable',
'version': 42
}
}),
'intro_tables.json': json.dumps({
'tester': {
'Permissions': [
{
'permission': 'tester'
},
{
'extra': 'is an API.'
}
],
'LearnMore': [
{
'href': 'https://tester.test.com/welcome.html',
'content': 'Welcome!'
}
]
}
})
}
}
}
}
CANNED_API_FILE_SYSTEM_DATA = {
'trunk': {
'api': {
'_api_features.json': json.dumps({
'contextMenus': {
'channel': 'stable'
},
'events': {
'channel': 'stable'
},
'extension': {
'channel': 'stable'
}
}),
'_manifest_features.json': json.dumps({
'history': {
'channel': 'beta'
},
'runtime': {
'channel': 'stable'
},
'storage': {
'channel': 'beta'
},
'sync': {
'channel': 'trunk'
}
}),
'_permission_features.json': json.dumps({
'alarms': {
'channel': 'stable'
},
'bluetooth': {
'channel': 'dev'
},
'bookmarks': {
'channel': 'stable'
},
'cookies': {
'channel': 'dev'
},
'declarativeContent': {
'channel': 'trunk'
},
'falseBetaAPI': {
'channel': 'beta'
},
'trunkAPI': {
'channel': 'trunk'
}
}),
'idle.json': 'idle contents',
'input_ime.json': 'input.ime contents',
'menus.json': 'menus contents',
'tabs.json': 'tabs contents',
'windows.json': 'windows contents'
},
'docs': {
'templates': {
'json': {
'api_availabilities.json': json.dumps({
'jsonAPI1': {
'channel': 'stable',
'version': 10
},
'jsonAPI2': {
'channel': 'trunk'
},
'jsonAPI3': {
'channel': 'dev'
}
}),
'intro_tables.json': json.dumps({
'test': [
{
'Permissions': 'probably none'
}
]
})
}
}
}
},
'1500': {
'api': {
'_api_features.json': json.dumps({
'events': {
'channel': 'trunk'
},
'extension': {
'channel': 'stable'
},
'systemInfo.stuff': {
'channel': 'dev'
}
}),
'_manifest_features.json': json.dumps({
'contextMenus': {
'channel': 'trunk'
},
'notifications': {
'channel': 'beta'
},
'runtime': {
'channel': 'stable'
},
'storage': {
'channel': 'dev'
},
'sync': {
'channel': 'trunk'
}
}),
'_permission_features.json': json.dumps({
'alarms': {
'channel': 'stable'
},
'bluetooth': {
'channel': 'dev'
},
'bookmarks': {
'channel': 'stable'
},
'cookies': {
'channel': 'dev'
},
'declarativeContent': {
'channel': 'trunk'
},
'downloads': {
'channel': 'beta'
}
}),
'idle.json': 'idle contents',
'input_ime.json': 'input.ime contents',
'menus.json': 'menus contents',
'tabs.json': 'tabs contents',
'windows.json': 'windows contents'
}
},
'1453': {
'api': {
'_api_features.json': json.dumps({
'events': {
'channel': 'dev'
},
'extension': {
'channel': 'stable'
},
'systemInfo.cpu': {
'channel': 'stable'
},
'systemInfo.stuff': {
'channel': 'dev'
}
}),
'_manifest_features.json': json.dumps({
'notifications': {
'channel': 'dev'
},
'runtime': {
'channel': 'stable'
},
'storage': {
'channel': 'dev'
}
}),
'_permission_features.json': json.dumps({
'alarms': {
'channel': 'stable'
},
'bluetooth': {
'channel': 'dev'
},
'bookmarks': {
'channel': 'stable'
},
'context_menus': {
'channel': 'trunk'
},
'declarativeContent': {
'channel': 'trunk'
},
'downloads': {
'channel': 'dev'
}
}),
'idle.json': 'idle contents',
'input_ime.json': 'input.ime contents',
'menus.json': 'menus contents',
'tabs.json': 'tabs contents',
'windows.json': 'windows contents'
}
},
'1410': {
'api': {
'_manifest_features.json': json.dumps({
'events': {
'channel': 'beta'
},
'notifications': {
'channel': 'dev'
},
'page_action': {
'channel': 'stable'
},
'runtime': {
'channel': 'stable'
},
'web_request': {
'channel': 'stable'
}
}),
'_permission_features.json': json.dumps({
'alarms': {
'channel': 'stable'
},
'bluetooth': {
'channel': 'dev'
},
'bookmarks': {
'channel': 'stable'
},
'context_menus': {
'channel': 'trunk'
},
'declarativeContent': {
'channel': 'trunk'
},
'systemInfo.display': {
'channel': 'stable'
}
}),
'idle.json': 'idle contents',
'input_ime.json': 'input.ime contents',
'menus.json': 'menus contents',
'tabs.json': 'tabs contents',
'windows.json': 'windows contents'
}
},
'1364': {
'api': {
'_manifest_features.json': json.dumps({
'page_action': {
'channel': 'stable'
},
'runtime': {
'channel': 'stable'
}
}),
'_permission_features.json': json.dumps({
'alarms': {
'channel': 'stable'
},
'bookmarks': {
'channel': 'stable'
},
'systemInfo.display': {
'channel': 'stable'
},
'webRequest': {
'channel': 'stable'
}
}),
'idle.json': 'idle contents',
'input_ime.json': 'input.ime contents',
'menus.json': 'menus contents',
'tabs.json': 'tabs contents',
'windows.json': 'windows contents'
}
},
'1312': {
'api': {
'_manifest_features.json': json.dumps({
'page_action': {
'channel': 'stable'
},
'runtime': {
'channel': 'stable'
},
'web_request': {
'channel': 'stable'
}
}),
'_permission_features.json': json.dumps({
'alarms': {
'channel': 'stable'
},
'bookmarks': {
'channel': 'stable'
},
'systemInfo.display': {
'channel': 'stable'
}
}),
'idle.json': 'idle contents',
'input_ime.json': 'input.ime contents',
'menus.json': 'menus contents',
'tabs.json': 'tabs contents',
'windows.json': 'windows contents'
}
},
'1271': {
'api': {
'_manifest_features.json': json.dumps({
'page_action': {
'channel': 'stable'
},
'runtime': {
'channel': 'stable'
},
'system_info_display': {
'channel': 'stable'
}
}),
'_permission_features.json': json.dumps({
'alarms': {
'channel': 'beta'
},
'bookmarks': {
'channel': 'stable'
},
'webRequest': {
'channel': 'stable'
}
}),
'idle.json': 'idle contents',
'input_ime.json': 'input.ime contents',
'menus.json': 'menus contents',
'tabs.json': 'tabs contents',
'windows.json': 'windows contents'
}
},
'1229': {
'api': {
'_manifest_features.json': json.dumps({
'page_action': {
'channel': 'stable'
},
'runtime': {
'channel': 'stable'
},
'web_request': {
'channel': 'stable'
}
}),
'_permission_features.json': json.dumps({
'bookmarks': {
'channel': 'stable'
},
'systemInfo.display': {
'channel': 'beta'
}
}),
'idle.json': 'idle contents',
'input_ime.json': 'input.ime contents',
'menus.json': 'menus contents',
'tabs.json': 'tabs contents'
}
},
'1180': {
'api': {
'_manifest_features.json': json.dumps({
'page_action': {
'channel': 'stable'
},
'runtime': {
'channel': 'stable'
}
}),
'_permission_features.json': json.dumps({
'bookmarks': {
'channel': 'stable'
},
'webRequest': {
'channel': 'stable'
}
}),
'idle.json': 'idle contents',
'input_ime.json': 'input.ime contents',
'menus.json': 'menus contents',
'tabs.json': 'tabs contents'
}
},
'1132': {
'api': {
'_manifest_features.json': json.dumps({
'page_action': {
'channel': 'stable'
}
}),
'_permission_features.json': json.dumps({
'webRequest': {
'channel': 'stable'
}
}),
'idle.json': 'idle contents',
'input.ime.json': 'input.ime contents',
'menus.json': 'menus contents',
'tabs.json': 'tabs contents'
}
},
'1084': {
'api': {
'_manifest_features.json': json.dumps({
'contents': 'nothing of interest here,really'
}),
'idle.json': 'idle contents',
'input.ime.json': 'input.ime contents',
'menus.json': 'menus contents',
'pageAction.json': 'pageAction contents',
'tabs.json': 'tabs contents',
'webRequest.json': 'webRequest contents'
}
},
'1025': {
'api': {
'idle.json': 'idle contents',
'input.ime.json': 'input.ime contents',
'menus.json': 'menus contents',
'pageAction.json': 'pageAction contents',
'tabs.json': 'tabs contents',
'webRequest.json': 'webRequest contents'
}
},
'963': {
'api': {
'extension_api.json': json.dumps([
{
'namespace': 'idle'
},
{
'namespace': 'menus'
},
{
'namespace': 'pageAction'
},
{
'namespace': 'webRequest'
}
])
}
},
'912': {
'api': {
'extension_api.json': json.dumps([
{
'namespace': 'idle'
},
{
'namespace': 'menus'
},
{
'namespace': 'pageAction'
},
{
'namespace': 'experimental.webRequest'
}
])
}
},
'874': {
'api': {
'extension_api.json': json.dumps([
{
'namespace': 'idle'
},
{
'namespace': 'menus'
},
{
'namespace': 'pageAction'
}
])
}
},
'835': {
'api': {
'extension_api.json': json.dumps([
{
'namespace': 'idle'
},
{
'namespace': 'menus'
},
{
'namespace': 'pageAction'
}
])
}
},
'782': {
'api': {
'extension_api.json': json.dumps([
{
'namespace': 'idle'
},
{
'namespace': 'menus'
},
{
'namespace': 'pageAction'
}
])
}
},
'742': {
'api': {
'extension_api.json': json.dumps([
{
'namespace': 'idle'
},
{
'namespace': 'menus'
},
{
'namespace': 'pageAction'
}
])
}
},
'696': {
'api': {
'extension_api.json': json.dumps([
{
'namespace': 'idle'
},
{
'namespace': 'menus'
},
{
'namespace': 'pageAction'
}
])
}
},
'648': {
'api': {
'extension_api.json': json.dumps([
{
'namespace': 'idle'
},
{
'namespace': 'menus'
},
{
'namespace': 'pageAction'
}
])
}
},
'597': {
'api': {
'extension_api.json': json.dumps([
{
'namespace': 'idle'
},
{
'namespace': 'menus'
},
{
'namespace': 'pageAction'
}
])
}
},
'552': {
'api': {
'extension_api.json': json.dumps([
{
'namespace': 'idle'
},
{
'namespace': 'menus'
},
{
'namespace': 'pageAction'
}
])
}
},
'544': {
'api': {
'extension_api.json': json.dumps([
{
'namespace': 'idle'
},
{
'namespace': 'menus'
}
])
}
},
'495': {
'api': {
'extension_api.json': json.dumps([
{
'namespace': 'idle'
},
{
'namespace': 'menus'
}
])
}
},
'396': {
'api': {
'extension_api.json': json.dumps([
{
'namespace': 'idle'
},
{
'namespace': 'experimental.menus'
}
])
}
}
}
{
"devtools.inspectedWindow": {
"channel": "stable",
"version": 18
},
"devtools.network": {
"channel": "stable",
"version": 18
},
"devtools.panels": {
"channel": "stable",
"version": 18
},
"webstore": {
"channel": "stable",
"version": 15
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment