Make callers of CommandLine use it via the base:: namespace.
[chromium-blink-merge.git] / tools / perf / profile_creators / extensions_profile_creator.py
blob7243f5f9d25f9718c4c866150a6f1d9cdbafe9a8
1 # Copyright 2014 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.
5 import json
6 import logging
7 import os
8 import platform
9 import shutil
10 import socket
11 import sys
12 import tempfile
13 import time
14 import urllib2
15 import zipfile
17 from telemetry.page import profile_creator
19 import page_sets
22 def _ExternalExtensionsPath():
23 """Returns the OS-dependent path at which to install the extension deployment
24 files"""
25 if platform.system() == 'Darwin':
26 return os.path.join('/Library', 'Application Support', 'Google', 'Chrome',
27 'External Extensions')
28 elif platform.system() == 'Linux':
29 return os.path.join('/opt', 'google', 'chrome', 'extensions' )
30 else:
31 raise NotImplementedError('Extension install on %s is not yet supported' %
32 platform.system())
34 def _DownloadExtension(extension_id, output_dir):
35 """Download an extension to disk.
37 Args:
38 extension_id: the extension id.
39 output_dir: Directory to download into.
41 Returns:
42 Extension file downloaded."""
43 extension_download_path = os.path.join(output_dir, "%s.crx" % extension_id)
44 extension_url = (
45 "https://clients2.google.com/service/update2/crx?response=redirect"
46 "&x=id%%3D%s%%26lang%%3Den-US%%26uc" % extension_id)
47 response = urllib2.urlopen(extension_url)
48 assert(response.getcode() == 200)
50 with open(extension_download_path, "w") as f:
51 f.write(response.read())
53 return extension_download_path
55 def _GetExtensionInfoFromCRX(crx_path):
56 """Parse an extension archive and return information.
58 Note:
59 The extension name returned by this function may not be valid
60 (e.g. in the case of a localized extension name). It's use is just
61 meant to be informational.
63 Args:
64 crx_path: path to crx archive to look at.
66 Returns:
67 Tuple consisting of:
68 (crx_version, extension_name)"""
69 crx_zip = zipfile.ZipFile(crx_path)
70 manifest_contents = crx_zip.read('manifest.json')
71 decoded_manifest = json.loads(manifest_contents)
72 crx_version = decoded_manifest['version']
73 extension_name = decoded_manifest['name']
75 return (crx_version, extension_name)
77 class ExtensionsProfileCreator(profile_creator.ProfileCreator):
78 """Virtual base class for profile creators that install extensions.
80 Extensions are installed using the mechanism described in
81 https://developer.chrome.com/extensions/external_extensions.html .
83 Subclasses are meant to be run interactively.
84 """
86 def __init__(self, extensions_to_install=None, theme_to_install=None):
87 self._CheckTestEnvironment()
89 super(ExtensionsProfileCreator, self).__init__()
90 self._page_set = page_sets.Typical25()
92 # Directory into which the output profile is written.
93 self._output_profile_path = None
95 # List of extensions to install.
96 self._extensions_to_install = list(extensions_to_install or [])
98 # Theme to install (if any).
99 self._theme_to_install = theme_to_install
101 # Directory to download extension files into.
102 self._extension_download_dir = None
104 # Have the extensions been installed yet?
105 self._extensions_installed = False
107 # List of files to delete after run.
108 self._files_to_cleanup = []
110 self._PrepareExtensionInstallFiles()
112 def _CheckTestEnvironment(self):
113 # Running this script on a corporate network or other managed environment
114 # could potentially alter the profile contents.
115 hostname = socket.gethostname()
116 if hostname.endswith('corp.google.com'):
117 raise Exception("It appears you are connected to a corporate network "
118 "(hostname=%s). This script needs to be run off the corp "
119 "network." % hostname)
121 prompt = ("\n!!!This script must be run on a fresh OS installation, "
122 "disconnected from any corporate network. Are you sure you want to "
123 "continue? (y/N) ")
124 if (raw_input(prompt).lower() != 'y'):
125 sys.exit(-1)
127 def _PrepareExtensionInstallFiles(self):
128 """Download extension archives and create extension install files."""
129 extensions_to_install = self._extensions_to_install
130 if self._theme_to_install:
131 extensions_to_install.append(self._theme_to_install)
132 if not extensions_to_install:
133 raise ValueError("No extensions or themes to install:",
134 extensions_to_install)
136 # Create external extensions path if it doesn't exist already.
137 external_extensions_dir = _ExternalExtensionsPath()
138 if not os.path.isdir(external_extensions_dir):
139 os.makedirs(external_extensions_dir)
141 self._extension_download_dir = tempfile.mkdtemp()
143 num_extensions = len(extensions_to_install)
144 for i, extension_id in extensions_to_install:
145 logging.info("Downloading %s - %d/%d" % (
146 extension_id, (i + 1), num_extensions))
147 extension_path = _DownloadExtension(extension_id,
148 self._extension_download_dir)
149 (version, name) = _GetExtensionInfoFromCRX(extension_path)
150 extension_info = {'external_crx' : extension_path,
151 'external_version' : version,
152 '_comment' : name}
153 extension_json_path = os.path.join(external_extensions_dir,
154 "%s.json" % extension_id)
155 with open(extension_json_path, 'w') as f:
156 f.write(json.dumps(extension_info))
157 self._files_to_cleanup.append(extension_json_path)
159 def _CleanupExtensionInstallFiles(self):
160 """Cleanup stray files before exiting."""
161 logging.info("Cleaning up stray files")
162 for filename in self._files_to_cleanup:
163 os.remove(filename)
165 if self._extension_download_dir:
166 # Simple sanity check to lessen the impact of a stray rmtree().
167 if len(self._extension_download_dir.split(os.sep)) < 3:
168 raise Exception("Path too shallow: %s" % self._extension_download_dir)
169 shutil.rmtree(self._extension_download_dir)
170 self._extension_download_dir = None
172 def CustomizeBrowserOptions(self, options):
173 self._output_profile_path = options.output_profile_path
175 def DidRunTest(self, browser, results):
176 """Run before exit."""
177 super(ExtensionsProfileCreator, self).DidRunTest()
178 # Do some basic sanity checks to make sure the profile is complete.
179 installed_extensions = browser.extensions.keys()
180 if not len(installed_extensions) == len(self._extensions_to_install):
181 # Diagnosing errors:
182 # Too many extensions: Managed environment may be installing additional
183 # extensions.
184 raise Exception("Unexpected number of extensions installed in browser",
185 installed_extensions)
187 # Check that files on this list exist and have content.
188 expected_files = [
189 os.path.join('Default', 'Network Action Predictor')]
190 for filename in expected_files:
191 filename = os.path.join(self._output_profile_path, filename)
192 if not os.path.getsize(filename) > 0:
193 raise Exception("Profile not complete: %s is zero length." % filename)
195 self._CleanupExtensionInstallFiles()
197 def CanRunForPage(self, page):
198 # No matter how many pages in the pageset, just perform two test iterations.
199 return page.page_set.pages.index(page) < 2
201 def ValidateAndMeasurePage(self, _, tab, results):
202 # Profile setup works in 2 phases:
203 # Phase 1: When the first page is loaded: we wait for a timeout to allow
204 # all extensions to install and to prime safe browsing and other
205 # caches. Extensions may open tabs as part of the install process.
206 # Phase 2: When the second page loads, user_story_runner closes all tabs -
207 # we are left with one open tab, wait for that to finish loading.
209 # Sleep for a bit to allow safe browsing and other data to load +
210 # extensions to install.
211 if not self._extensions_installed:
212 sleep_seconds = 5 * 60
213 logging.info("Sleeping for %d seconds." % sleep_seconds)
214 time.sleep(sleep_seconds)
215 self._extensions_installed = True
216 else:
217 # Phase 2: Wait for tab to finish loading.
218 for i in xrange(len(tab.browser.tabs)):
219 t = tab.browser.tabs[i]
220 t.WaitForDocumentReadyStateToBeComplete()