| OLD | NEW |
| (Empty) | |
| 1 #!/usr/bin/env python |
| 2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. |
| 5 |
| 6 """Test fixture for tests involving installing/updating Chrome. |
| 7 |
| 8 Provides an interface to install or update chrome from within a testcase, and |
| 9 allows users to run PyAuto tests using the installed version. User and system |
| 10 level installations are supported, and either one can be used for running the |
| 11 PyAuto tests. Currently the only platform that's supported is Windows. |
| 12 """ |
| 13 |
| 14 import httplib |
| 15 import logging |
| 16 import optparse |
| 17 import os |
| 18 import platform |
| 19 import re |
| 20 import shutil |
| 21 import stat |
| 22 import sys |
| 23 import tempfile |
| 24 import unittest |
| 25 import urllib |
| 26 import urlparse |
| 27 |
| 28 import chrome_checkout |
| 29 import chrome_installer_win |
| 30 from chrome_installer_win import ChromeInstallation |
| 31 |
| 32 sys.path.append(os.path.join(os.path.pardir, 'pyautolib')) |
| 33 |
| 34 # This import should go after sys.path is set appropriately. |
| 35 from fetch_prebuilt_pyauto import FetchPrebuilt |
| 36 import pyauto_utils |
| 37 from pyauto_utils import GTestTextTestRunner |
| 38 |
| 39 |
| 40 class InstallTest(unittest.TestCase): |
| 41 """Base updater test class. |
| 42 |
| 43 All dependencies, such as the specified Chrome builds, source files, and |
| 44 installers are downloaded at the beginning of the test. Dependencies are |
| 45 downloaded in the temp directory. This download only occurs once, before |
| 46 the first test is executed. A PyUITest object is created whenever a user |
| 47 installs or updates Chrome, using dependencies that correspond with that |
| 48 particular build. Users can utilize that object to run updater tests. All |
| 49 updater tests should derive from this class. |
| 50 |
| 51 Example: |
| 52 |
| 53 class ProtectorUpdater(InstallTest): |
| 54 |
| 55 def testNoChangeOnCleanProfile(self): |
| 56 self.assertFalse(self._pyauto.GetProtectorState()['showing_change']) |
| 57 self.UpdateBuild() |
| 58 self.assertFalse(self._pyauto.GetProtectorState()['showing_change']) |
| 59 |
| 60 |
| 61 Include the following in your updater test script to make it run standalone. |
| 62 |
| 63 from install_test import Main |
| 64 |
| 65 if __name__ == '__main__': |
| 66 Main() |
| 67 |
| 68 To fire off an updater test, use the command below. |
| 69 python test_script.py --url=<URL> --builds=22.0.1230.0,22.0.1231.0 |
| 70 """ |
| 71 |
| 72 _build_iterator = None |
| 73 _current_build = '' |
| 74 _current_location = '' |
| 75 # Prefix that will be appended to the deps folder names. |
| 76 _dir_prefix = '__CHRBLD__' |
| 77 _dir_iterator = None |
| 78 # Var that's populated with ChromeInstallation object on install/update. |
| 79 _installation = None |
| 80 _installer_name = 'mini_installer.exe' |
| 81 # Var that's populated with PyUITest object on install/update. |
| 82 _pyauto = None |
| 83 # Locations in Temp directory where Chrome installers are downloaded. |
| 84 _installers = [] |
| 85 # Contains paths to the deps folders. |
| 86 _download_dirs = [] |
| 87 _opts = None |
| 88 |
| 89 def __init__(self, methodName='runTest'): |
| 90 unittest.TestCase.__init__(self, methodName) |
| 91 self._platform = InstallTest.GetPlatform() |
| 92 self._Initialize() |
| 93 current_build = ChromeInstallation.GetCurrent() |
| 94 if current_build: |
| 95 current_build.Uninstall() |
| 96 for build in self._builds: |
| 97 if not self._DownloadDeps(build): |
| 98 raise RuntimeError('Could not download dependencies.') |
| 99 self._installer_iter = iter(self._installers) |
| 100 self._dir_iterator = iter(self._download_dirs) |
| 101 |
| 102 def _Initialize(self): |
| 103 """Sets test parameters.""" |
| 104 self._url = self._opts.url |
| 105 self._builds = self._opts.builds and self._opts.builds.split(',') or [] |
| 106 if not self._url or not self._builds: |
| 107 raise RuntimeError('Please specify a valid URL and two Chrome builds.') |
| 108 self._builds.sort() |
| 109 self._url = self._url.endswith('/') and self._url or self._url + '/' |
| 110 self._options = self._opts.options.split(',') if self._opts.options else [] |
| 111 self._install_type = ('system-level' in self._options and |
| 112 chrome_installer_win.InstallationType.SYSTEM or |
| 113 chrome_installer_win.InstallationType.USER) |
| 114 self._build_iterator = iter(self._builds) |
| 115 self._current_build = next(self._build_iterator, None) |
| 116 |
| 117 def setUp(self): |
| 118 """Called before each unittest to prepare the test fixture.""" |
| 119 self.InstallBuild() |
| 120 self.assertTrue(self._pyauto) |
| 121 |
| 122 def tearDown(self): |
| 123 """Called at the end of each unittest to do any test related cleanup.""" |
| 124 self._UnloadPyAutoModules() |
| 125 self._DeleteBuild() |
| 126 |
| 127 @staticmethod |
| 128 def GetPlatform(): |
| 129 """Returns the platform name.""" |
| 130 return ({'Windows': 'win', |
| 131 'Darwin': 'mac', |
| 132 'Linux': 'linux'}).get(platform.system()) |
| 133 |
| 134 def _UnloadPyAutoModules(self): |
| 135 """Deletes the PyUITest object and unloads PyAuto modules.""" |
| 136 if self._pyauto: |
| 137 del self._pyauto |
| 138 for module in ['pyauto', 'pyautolib', '_pyautolib']: |
| 139 if module in sys.modules: |
| 140 sys.modules.pop(module) |
| 141 |
| 142 def _RestorePaths(self): |
| 143 """Restores the sys.path variable to its original state.""" |
| 144 sys.path = list(frozenset(sys.path)) |
| 145 if self._current_location: |
| 146 if self._current_location in sys.path: |
| 147 sys.path.remove(self._current_location) |
| 148 if os.path.join(self._current_location, 'pyautolib') in sys.path: |
| 149 sys.path.remove(os.path.join(self._current_location, 'pyautolib')) |
| 150 |
| 151 def _Install(self): |
| 152 """Helper method that installs Chrome and creates a PyUITest object.""" |
| 153 self._pyauto = None |
| 154 installer_path = next(self._installer_iter, None) |
| 155 if not installer_path: |
| 156 raise RuntimeError('No more builds left to install.') |
| 157 self._installation = chrome_installer_win.Install(installer_path, |
| 158 self._install_type, |
| 159 self._current_build, |
| 160 self._options) |
| 161 try: |
| 162 import pyauto |
| 163 except ImportError, err: |
| 164 logging.error(err) |
| 165 return |
| 166 self._pyauto = pyauto.PyUITest(methodName='runTest', |
| 167 browser_path=os.path.dirname( |
| 168 self._installation.GetExePath())) |
| 169 self._pyauto.suite_holder = pyauto.PyUITestSuite([]) |
| 170 self._pyauto.setUp() |
| 171 |
| 172 |
| 173 def InstallBuild(self): |
| 174 self._current_location = next(self._dir_iterator, None) |
| 175 sys.path.insert(0, self._current_location) |
| 176 sys.path.insert(1, os.path.join(self._current_location, 'pyautolib')) |
| 177 self._Install() |
| 178 |
| 179 def _Update(self): |
| 180 """Helper method for updating Chrome.""" |
| 181 self._RestorePaths() |
| 182 self._current_location = next(self._dir_iterator, None) |
| 183 assert (self._current_location) |
| 184 sys.path.insert(0, self._current_location) |
| 185 sys.path.insert(1, os.path.join(self._current_location, 'pyautolib')) |
| 186 build = next(self._build_iterator, None) |
| 187 if not build: |
| 188 raise RuntimeError('No more builds left to install. Following builds ' |
| 189 'have already been installed: %r' % self._builds)
|
| 190 self._current_build = build |
| 191 self._Install() |
| 192 |
| 193 def UpdateBuild(self): |
| 194 """Installs the second Chrome build specified in the command line args.""" |
| 195 if self._pyauto: |
| 196 self._pyauto.TearDown() |
| 197 self._UnloadPyAutoModules() |
| 198 self._Update() |
| 199 |
| 200 def _SrcFilesExist(self, root, items): |
| 201 """Checks if specified files/folders exist at the specified location. |
| 202 |
| 203 Args: |
| 204 root: Parent folder where all the source directories reside. |
| 205 items: List of files/folders to be verified for existence in the root. |
| 206 |
| 207 Returns: |
| 208 True, if all sub-folders exist in the root, otherwise False. |
| 209 """ |
| 210 return all([os.path.exists(os.path.join(root, x)) for x in items]) |
| 211 |
| 212 def _CheckoutSourceFiles(self, build, location): |
| 213 """Checks out source files associated with the current build. |
| 214 |
| 215 Args: |
| 216 build: Chrome release version number. |
| 217 location: Destination where source files will be saved. |
| 218 Returns: |
| 219 Zero if successful, otherwise a negative value. |
| 220 """ |
| 221 try: |
| 222 chrome_checkout.CheckOut(build, location) |
| 223 return True |
| 224 except AssertionError: |
| 225 return False |
| 226 |
| 227 def _FetchPrebuiltPyauto(self, url, location): |
| 228 """Fetches the specified Chrome build. |
| 229 |
| 230 Args: |
| 231 url: URL where the build is located. |
| 232 location: Location where the build will be downloaded. |
| 233 |
| 234 Returns: |
| 235 True if successful, otherwise False. |
| 236 """ |
| 237 fetch_build = FetchPrebuilt(url, location, self._platform) |
| 238 if pyauto_utils.DoesUrlExist(url): |
| 239 return fetch_build.Run() == 0 |
| 240 return False |
| 241 |
| 242 def _DownloadInstaller(self, url, location): |
| 243 """Downloads the Chrome installer. |
| 244 |
| 245 Args: |
| 246 url: URL where the installer is located. |
| 247 location: Location where installer will be downloaded. |
| 248 |
| 249 Returns: |
| 250 True if successful, otherwise False. |
| 251 """ |
| 252 try: |
| 253 self._Download(self._installer_name, url, location) |
| 254 return True |
| 255 except (IOError, RuntimeError): |
| 256 return False |
| 257 |
| 258 def _DownloadDeps(self, build): |
| 259 """Downloads Chrome build, source files, and Chrome installer. |
| 260 |
| 261 Args: |
| 262 build: Chrome release build number. |
| 263 |
| 264 Returns: |
| 265 True if successful, otherwise False. |
| 266 """ |
| 267 url = '%s%s/%s' % (self._url, build, self._platform) |
| 268 dir_name = '%s%s' % (self._dir_prefix, build) |
| 269 location = os.path.join(tempfile.gettempdir(), dir_name) |
| 270 if os.path.isdir(location): |
| 271 self._download_dirs.append(location) |
| 272 self._installers.append(os.path.join(location, self._installer_name)) |
| 273 return True |
| 274 else: |
| 275 tmpdir = tempfile.mkdtemp() |
| 276 if self._CheckoutSourceFiles(build, tmpdir): |
| 277 if self._FetchPrebuiltPyauto(url, tmpdir): |
| 278 if self._DownloadInstaller(url, tmpdir): |
| 279 try: |
| 280 # This is a workaround because rename was causing problems. |
| 281 shutil.copytree(tmpdir, location) |
| 282 # Callback is there because hidden svn files are read-only, so |
| 283 # we need to change their permissions on the fly to delete them. |
| 284 self._DeleteDir(tmpdir) |
| 285 self._download_dirs.append(location) |
| 286 self._installers.append(os.path.join(location, |
| 287 self._installer_name)) |
| 288 return True |
| 289 except(OSError, IOError), err: |
| 290 return False |
| 291 return False |
| 292 |
| 293 def _DeleteBuild(self): |
| 294 """Uninstalls Chrome.""" |
| 295 self._installation.Uninstall() |
| 296 self._build_iterator = iter(self._builds) |
| 297 self._dir_iterator = iter(self._download_dirs) |
| 298 self._current_build = next(self._build_iterator, None) |
| 299 self._current_location = next(self._dir_iterator, None) |
| 300 self._RestorePaths() |
| 301 |
| 302 def _DeleteDir(self, dir_name): |
| 303 """Deletes a directory. |
| 304 |
| 305 Args: |
| 306 dir_name: Name of the directory to delete. |
| 307 """ |
| 308 def _OnError(func, path, exc_info): |
| 309 """Callback for shutil.rmtree.""" |
| 310 if not os.access(path, os.W_OK): |
| 311 os.chmod(path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) |
| 312 func(path) |
| 313 |
| 314 if os.path.isdir(dir_name): |
| 315 shutil.rmtree(dir_name, onerror=_OnError) |
| 316 |
| 317 def _Download(self, dfile, url, dest=None): |
| 318 """Downloads a file from the specified URL. |
| 319 |
| 320 Args: |
| 321 dfile: Name of the file to download. |
| 322 url: URL where the file is located. |
| 323 dest: Location where file will be downloaded. Default is CWD. |
| 324 |
| 325 Returns: |
| 326 Zero if successful, otherwise a non-zero value. |
| 327 """ |
| 328 filename = ((dest and os.path.exists(dest)) and os.path.join(dest, dfile) |
| 329 or os.path.join(tempfile.gettempdir(), dfile)) |
| 330 file_url = '%s/%s' % (url, dfile) |
| 331 if not pyauto_utils.DoesUrlExist(file_url): |
| 332 raise RuntimeError('Either the URL or the file name is invalid.') |
| 333 try: |
| 334 dfile = urllib.urlretrieve(file_url, filename) |
| 335 except IOError, err: |
| 336 raise err |
| 337 return os.path.isfile(dfile[0]) |
| 338 |
| 339 @staticmethod |
| 340 def SetOptions(opts): |
| 341 """Static method for passing command options to InstallTest. |
| 342 |
| 343 We do not instantiate InstallTest. Therefore, command arguments cannot |
| 344 be passed to its constructor. Since InstallTest needs to use these options |
| 345 and using globals is not an option, this method can be used by the Main |
| 346 class to pass the arguments it parses onto InstallTest. |
| 347 """ |
| 348 InstallTest._opts = opts |
| 349 |
| 350 |
| 351 class Main(object): |
| 352 """Main program for running Updater tests.""" |
| 353 |
| 354 _mod_path = sys.argv[0] |
| 355 |
| 356 def __init__(self): |
| 357 self._ParseArgs() |
| 358 self._Run() |
| 359 |
| 360 def _ParseArgs(self): |
| 361 """Parses command line arguments.""" |
| 362 parser = optparse.OptionParser() |
| 363 parser.add_option( |
| 364 '-b', '--builds', type='string', default='', dest='builds', |
| 365 help='Specifies the two (or more) builds needed for testing.') |
| 366 parser.add_option( |
| 367 '-u', '--url', type='string', default='', dest='url', |
| 368 help='Specifies the build url, without the build number.') |
| 369 parser.add_option( |
| 370 '-o', '--options', type='string', default='', |
| 371 help='Specifies any additional Chrome options (i.e. --system-level).') |
| 372 opts, args = parser.parse_args() |
| 373 self.ValidateArgs(opts) |
| 374 InstallTest.SetOptions(opts) |
| 375 |
| 376 def ValidateArgs(self, opts): |
| 377 """Verifies the sanity of the command arguments. |
| 378 |
| 379 Confirms that all specified builds have a valid version number, and the |
| 380 build urls are valid. |
| 381 |
| 382 Args: |
| 383 opts: An object containing values for all command args. |
| 384 """ |
| 385 builds = opts.builds.split(',') |
| 386 for build in builds: |
| 387 if not re.match('\d+\.\d+\.\d+\.\d+', build): |
| 388 raise RuntimeError('Invalid build number: %s' % build) |
| 389 if not pyauto_utils.DoesUrlExist('%s/%s/' % (opts.url, build)): |
| 390 raise RuntimeError('Could not locate build no. %s' % build) |
| 391 |
| 392 def _GetTests(self): |
| 393 """Returns a list of unittests from the calling script.""" |
| 394 mod_name = [os.path.splitext(os.path.basename(self._mod_path))[0]] |
| 395 if os.path.dirname(self._mod_path) not in sys.path: |
| 396 sys.path.append(os.path.dirname(self._mod_path)) |
| 397 return unittest.defaultTestLoader.loadTestsFromNames(mod_name) |
| 398 |
| 399 def _Run(self): |
| 400 """Runs the unit tests.""" |
| 401 tests = self._GetTests() |
| 402 result = GTestTextTestRunner(verbosity=1).run(tests) |
| 403 del(tests) |
| 404 if not result.wasSuccessful(): |
| 405 print >>sys.stderr, ('Not all tests were successful.') |
| 406 sys.exit(1) |
| 407 sys.exit(0) |
| OLD | NEW |