| OLD | NEW |
| 1 # Copyright 2013 The Chromium Authors. All rights reserved. | 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 | 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
| 4 | 4 |
| 5 """This script tests the installer with test cases specified in the config file. | 5 """This script tests the installer with test cases specified in the config file. |
| 6 | 6 |
| 7 For each test case, it checks that the machine states after the execution of | 7 For each test case, it checks that the machine states after the execution of |
| 8 each command match the expected machine states. For more details, take a look at | 8 each command match the expected machine states. For more details, take a look at |
| 9 the design documentation at http://goo.gl/Q0rGM6 | 9 the design documentation at http://goo.gl/Q0rGM6 |
| 10 """ | 10 """ |
| 11 | 11 |
| 12 import argparse | 12 import argparse |
| 13 import datetime | 13 import datetime |
| 14 import inspect | 14 import inspect |
| 15 import json | 15 import json |
| 16 import os | 16 import os |
| 17 import subprocess | 17 import subprocess |
| 18 import sys | 18 import sys |
| 19 import time | 19 import time |
| 20 import traceback | 20 import traceback |
| 21 import unittest | 21 import unittest |
| 22 import _winreg | |
| 23 | 22 |
| 23 from state_walker import StateWalker |
| 24 from variable_expander import VariableExpander | 24 from variable_expander import VariableExpander |
| 25 import verifier_runner | 25 |
| 26 import cleaner_visitor |
| 27 import verifier_visitor |
| 26 | 28 |
| 27 | 29 |
| 28 def LogMessage(message): | 30 def LogMessage(message): |
| 29 """Logs a message to stderr. | 31 """Logs a message to stderr. |
| 30 | 32 |
| 31 Args: | 33 Args: |
| 32 message: The message string to be logged. | 34 message: The message string to be logged. |
| 33 """ | 35 """ |
| 34 now = datetime.datetime.now() | 36 now = datetime.datetime.now() |
| 35 frameinfo = inspect.getframeinfo(inspect.currentframe().f_back) | 37 frameinfo = inspect.getframeinfo(inspect.currentframe().f_back) |
| (...skipping 15 matching lines...) Expand all Loading... |
| 51 """ | 53 """ |
| 52 def __init__(self): | 54 def __init__(self): |
| 53 self.states = {} | 55 self.states = {} |
| 54 self.actions = {} | 56 self.actions = {} |
| 55 self.tests = [] | 57 self.tests = [] |
| 56 | 58 |
| 57 | 59 |
| 58 class InstallerTest(unittest.TestCase): | 60 class InstallerTest(unittest.TestCase): |
| 59 """Tests a test case in the config file.""" | 61 """Tests a test case in the config file.""" |
| 60 | 62 |
| 61 def __init__(self, name, test, config, variable_expander, quiet): | 63 def __init__(self, name, test, config, variable_expander, quiet, clean_state): |
| 62 """Constructor. | 64 """Constructor. |
| 63 | 65 |
| 64 Args: | 66 Args: |
| 65 name: The name of this test. | 67 name: The name of this test. |
| 66 test: An array of alternating state names and action names, starting and | 68 test: An array of alternating state names and action names, starting and |
| 67 ending with state names. | 69 ending with state names. |
| 68 config: The Config object. | 70 config: The Config object. |
| 69 variable_expander: A VariableExpander object. | 71 variable_expander: A VariableExpander object. |
| 72 quiet: A boolean to control the test output. |
| 73 clean_state: A string to represent the cleanup state. |
| 70 """ | 74 """ |
| 71 super(InstallerTest, self).__init__() | 75 super(InstallerTest, self).__init__() |
| 72 self._name = name | 76 self._name = name |
| 73 self._test = test | 77 self._test = test |
| 74 self._config = config | 78 self._config = config |
| 75 self._variable_expander = variable_expander | 79 self._variable_expander = variable_expander |
| 76 self._quiet = quiet | 80 self._quiet = quiet |
| 77 self._verifier_runner = verifier_runner.VerifierRunner() | 81 self._verifier = StateWalker(verifier_visitor.VerifierVisitor()) |
| 78 self._clean_on_teardown = True | 82 self._clean_on_teardown = True |
| 83 self._clean_state = clean_state |
| 79 | 84 |
| 80 def __str__(self): | 85 def __str__(self): |
| 81 """Returns a string representing the test case. | 86 """Returns a string representing the test case. |
| 82 | 87 |
| 83 Returns: | 88 Returns: |
| 84 A string created by joining state names and action names together with | 89 A string created by joining state names and action names together with |
| 85 ' -> ', for example, 'Test: clean -> install chrome -> chrome_installed'. | 90 ' -> ', for example, 'Test: clean -> install chrome -> chrome_installed'. |
| 86 """ | 91 """ |
| 87 return '%s: %s\n' % (self._name, ' -> '.join(self._test)) | 92 return '%s: %s\n' % (self._name, ' -> '.join(self._test)) |
| 88 | 93 |
| (...skipping 26 matching lines...) Expand all Loading... |
| 115 state = self._test[i + 1] | 120 state = self._test[i + 1] |
| 116 self._VerifyState(state) | 121 self._VerifyState(state) |
| 117 | 122 |
| 118 # If the test makes it here, it means it was successful, because RunCommand | 123 # If the test makes it here, it means it was successful, because RunCommand |
| 119 # and _VerifyState throw an exception on failure. | 124 # and _VerifyState throw an exception on failure. |
| 120 self._clean_on_teardown = False | 125 self._clean_on_teardown = False |
| 121 | 126 |
| 122 def tearDown(self): | 127 def tearDown(self): |
| 123 """Cleans up the machine if the test case fails.""" | 128 """Cleans up the machine if the test case fails.""" |
| 124 if self._clean_on_teardown: | 129 if self._clean_on_teardown: |
| 125 RunCleanCommand(True, self._variable_expander) | 130 RunCleanCommand(True, self._config.states[self._clean_state], |
| 131 self._variable_expander) |
| 126 | 132 |
| 127 def shortDescription(self): | 133 def shortDescription(self): |
| 128 """Overridden from unittest.TestCase. | 134 """Overridden from unittest.TestCase. |
| 129 | 135 |
| 130 We return None as the short description to suppress its printing. | 136 We return None as the short description to suppress its printing. |
| 131 The default implementation of this method returns the docstring of the | 137 The default implementation of this method returns the docstring of the |
| 132 runTest method, which is not useful since it's the same for every test case. | 138 runTest method, which is not useful since it's the same for every test case. |
| 133 The description from the __str__ method is informative enough. | 139 The description from the __str__ method is informative enough. |
| 134 """ | 140 """ |
| 135 return None | 141 return None |
| 136 | 142 |
| 137 def _VerifyState(self, state): | 143 def _VerifyState(self, state): |
| 138 """Verifies that the current machine state matches a given state. | 144 """Verifies that the current machine state matches a given state. |
| 139 | 145 |
| 140 Args: | 146 Args: |
| 141 state: A state name. | 147 state: A state name. |
| 142 """ | 148 """ |
| 143 if not self._quiet: | 149 if not self._quiet: |
| 144 LogMessage('Verifying state %s' % state) | 150 LogMessage('Verifying state %s' % state) |
| 145 try: | 151 try: |
| 146 self._verifier_runner.VerifyAll(self._config.states[state], | 152 self._verifier.Walk(self._variable_expander, self._config.states[state]) |
| 147 self._variable_expander) | |
| 148 except AssertionError as e: | 153 except AssertionError as e: |
| 149 # If an AssertionError occurs, we intercept it and add the state name | 154 # If an AssertionError occurs, we intercept it and add the state name |
| 150 # to the error message so that we know where the test fails. | 155 # to the error message so that we know where the test fails. |
| 151 raise AssertionError("In state '%s', %s" % (state, e)) | 156 raise AssertionError("In state '%s', %s" % (state, e)) |
| 152 | 157 |
| 153 | 158 |
| 154 def RunCommand(command, variable_expander): | 159 def RunCommand(command, variable_expander): |
| 155 """Runs the given command from the current file's directory. | 160 """Runs the given command from the current file's directory. |
| 156 | 161 |
| 157 This function throws an Exception if the command returns with non-zero exit | 162 This function throws an Exception if the command returns with non-zero exit |
| 158 status. | 163 status. |
| 159 | 164 |
| 160 Args: | 165 Args: |
| 161 command: A command to run. It is expanded using Expand. | 166 command: A command to run. It is expanded using Expand. |
| 162 variable_expander: A VariableExpander object. | 167 variable_expander: A VariableExpander object. |
| 163 """ | 168 """ |
| 164 expanded_command = variable_expander.Expand(command) | 169 expanded_command = variable_expander.Expand(command) |
| 165 script_dir = os.path.dirname(os.path.abspath(__file__)) | 170 script_dir = os.path.dirname(os.path.abspath(__file__)) |
| 166 exit_status = subprocess.call(expanded_command, shell=True, cwd=script_dir) | 171 exit_status = subprocess.call(expanded_command, shell=True, cwd=script_dir) |
| 167 if exit_status != 0: | 172 if exit_status != 0: |
| 168 raise Exception('Command %s returned non-zero exit status %s' % ( | 173 raise Exception('Command %s returned non-zero exit status %s' % ( |
| 169 expanded_command, exit_status)) | 174 expanded_command, exit_status)) |
| 170 | 175 |
| 171 | 176 def RunCleanCommand(force_clean, clean_state, variable_expander): |
| 172 def DeleteGoogleUpdateRegistration(system_level, registry_subkey, | |
| 173 variable_expander): | |
| 174 """Deletes Chrome's registration with Google Update. | |
| 175 | |
| 176 Args: | |
| 177 system_level: True if system-level Chrome is to be deleted. | |
| 178 registry_subkey: The pre-expansion registry subkey for the product. | |
| 179 variable_expander: A VariableExpander object. | |
| 180 """ | |
| 181 root = (_winreg.HKEY_LOCAL_MACHINE if system_level | |
| 182 else _winreg.HKEY_CURRENT_USER) | |
| 183 key_name = variable_expander.Expand(registry_subkey) | |
| 184 try: | |
| 185 key_handle = _winreg.OpenKey(root, key_name, 0, | |
| 186 _winreg.KEY_SET_VALUE | | |
| 187 _winreg.KEY_WOW64_32KEY) | |
| 188 _winreg.DeleteValue(key_handle, 'pv') | |
| 189 except WindowsError: | |
| 190 # The key isn't present, so there is no value to delete. | |
| 191 pass | |
| 192 | |
| 193 | |
| 194 def RunCleanCommand(force_clean, variable_expander): | |
| 195 """Puts the machine in the clean state (i.e. Chrome not installed). | 177 """Puts the machine in the clean state (i.e. Chrome not installed). |
| 196 | 178 |
| 197 Args: | 179 Args: |
| 198 force_clean: A boolean indicating whether to force cleaning existing | 180 force_clean: A boolean indicating whether to force cleaning existing |
| 199 installations. | 181 installations. |
| 200 variable_expander: A VariableExpander object. | 182 variable_expander: A VariableExpander object. |
| 201 """ | 183 """ |
| 202 # A list of (system_level, product_name, product_switch, registry_subkey) | 184 # A list of (product_name, product_switch) |
| 203 # tuples for the possible installed products. | 185 # tuples for the possible installed products. |
| 204 data = [ | 186 data = [ |
| 205 (False, '$CHROME_LONG_NAME', '', | 187 ('$CHROME_LONG_NAME', ''), |
| 206 '$CHROME_UPDATE_REGISTRY_SUBKEY'), | 188 ('$CHROME_LONG_NAME', '--system-level'), |
| 207 (True, '$CHROME_LONG_NAME', '--system-level', | |
| 208 '$CHROME_UPDATE_REGISTRY_SUBKEY'), | |
| 209 ] | 189 ] |
| 210 if variable_expander.Expand('$SUPPORTS_SXS') == 'True': | 190 if variable_expander.Expand('$SUPPORTS_SXS') == 'True': |
| 211 data.append((False, '$CHROME_LONG_NAME_SXS', '', | 191 data.append(('$CHROME_LONG_NAME_SXS', '')) |
| 212 '$CHROME_UPDATE_REGISTRY_SUBKEY_SXS')) | |
| 213 | 192 |
| 214 interactive_option = '--interactive' if not force_clean else '' | 193 interactive_option = '--interactive' if not force_clean else '' |
| 215 for system_level, product_name, product_switch, registry_subkey in data: | 194 for product_name, product_switch in data: |
| 216 command = ('python uninstall_chrome.py ' | 195 command = ('python uninstall_chrome.py ' |
| 217 '--chrome-long-name="%s" ' | 196 '--chrome-long-name="%s" ' |
| 218 '--no-error-if-absent %s %s' % | 197 '--no-error-if-absent %s %s' % |
| 219 (product_name, product_switch, interactive_option)) | 198 (product_name, product_switch, interactive_option)) |
| 220 try: | 199 try: |
| 221 RunCommand(command, variable_expander) | 200 RunCommand(command, variable_expander) |
| 222 except: | 201 except (Exception, OSError, ValueError): |
| 223 message = traceback.format_exception(*sys.exc_info()) | 202 message = traceback.format_exception(*sys.exc_info()) |
| 224 message.insert(0, 'Error cleaning up an old install with:\n') | 203 message.insert(0, 'Error cleaning up an old install with:\n') |
| 225 LogMessage(''.join(message)) | 204 LogMessage(''.join(message)) |
| 226 if force_clean: | |
| 227 DeleteGoogleUpdateRegistration(system_level, registry_subkey, | |
| 228 variable_expander) | |
| 229 | 205 |
| 206 if force_clean: |
| 207 StateWalker(cleaner_visitor.CleanerVisitor()).Walk( |
| 208 variable_expander, clean_state) |
| 230 | 209 |
| 231 def MergePropertyDictionaries(current_property, new_property): | 210 def MergePropertyDictionaries(current_property, new_property): |
| 232 """Merges the new property dictionary into the current property dictionary. | 211 """Merges the new property dictionary into the current property dictionary. |
| 233 | 212 |
| 234 This is different from general dictionary merging in that, in case there are | 213 This is different from general dictionary merging in that, in case there are |
| 235 keys with the same name, we merge values together in the first level, and we | 214 keys with the same name, we merge values together in the first level, and we |
| 236 override earlier values in the second level. For more details, take a look at | 215 override earlier values in the second level. For more details, take a look at |
| 237 http://goo.gl/uE0RoR | 216 http://goo.gl/uE0RoR |
| 238 | 217 |
| 239 Args: | 218 Args: |
| (...skipping 76 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 316 variable_expander), | 295 variable_expander), |
| 317 config.tests) | 296 config.tests) |
| 318 for state_name, state_property_filenames in config_data['states']: | 297 for state_name, state_property_filenames in config_data['states']: |
| 319 config.states[state_name] = ParsePropertyFiles(directory, | 298 config.states[state_name] = ParsePropertyFiles(directory, |
| 320 state_property_filenames, | 299 state_property_filenames, |
| 321 variable_expander) | 300 variable_expander) |
| 322 for action_name, action_command in config_data['actions']: | 301 for action_name, action_command in config_data['actions']: |
| 323 config.actions[action_name] = action_command | 302 config.actions[action_name] = action_command |
| 324 return config | 303 return config |
| 325 | 304 |
| 305 DEFAULT_CLEAN_STATE = 'clean' |
| 326 | 306 |
| 327 def main(): | 307 def main(): |
| 328 parser = argparse.ArgumentParser() | 308 parser = argparse.ArgumentParser() |
| 329 parser.add_argument('--build-dir', default='out', | 309 parser.add_argument('--build-dir', default='out', |
| 330 help='Path to main build directory (the parent of the ' | 310 help='Path to main build directory (the parent of the ' |
| 331 'Release or Debug directory)') | 311 'Release or Debug directory)') |
| 332 parser.add_argument('--target', default='Release', | 312 parser.add_argument('--target', default='Release', |
| 333 help='Build target (Release or Debug)') | 313 help='Build target (Release or Debug)') |
| 334 parser.add_argument('--force-clean', action='store_true', default=False, | 314 parser.add_argument('--force-clean', action='store_true', default=False, |
| 335 help='Force cleaning existing installations') | 315 help='Force cleaning existing installations') |
| 316 parser.add_argument('--clean-state', default='clean', |
| 317 help='The state that is used to cleanup the machine after' |
| 318 'each test case.') |
| 336 parser.add_argument('-q', '--quiet', action='store_true', default=False, | 319 parser.add_argument('-q', '--quiet', action='store_true', default=False, |
| 337 help='Reduce test runner output') | 320 help='Reduce test runner output') |
| 338 parser.add_argument('--write-full-results-to', metavar='FILENAME', | 321 parser.add_argument('--write-full-results-to', metavar='FILENAME', |
| 339 help='Path to write the list of full results to.') | 322 help='Path to write the list of full results to.') |
| 340 parser.add_argument('--config', metavar='FILENAME', | 323 parser.add_argument('--config', metavar='FILENAME', |
| 341 help='Path to test configuration file') | 324 help='Path to test configuration file') |
| 342 parser.add_argument('test', nargs='*', | 325 parser.add_argument('test', nargs='*', |
| 343 help='Name(s) of tests to run.') | 326 help=('Name(s) of tests to run. For example, ' |
| 327 '__main__.InstallerTest.ChromeUserLevel')) |
| 344 args = parser.parse_args() | 328 args = parser.parse_args() |
| 345 if not args.config: | 329 if not args.config: |
| 346 parser.error('missing mandatory --config FILENAME argument') | 330 parser.error('missing mandatory --config FILENAME argument') |
| 347 | 331 |
| 348 mini_installer_path = os.path.join(args.build_dir, args.target, | 332 mini_installer_path = os.path.join(args.build_dir, args.target, |
| 349 'mini_installer.exe') | 333 'mini_installer.exe') |
| 350 assert os.path.exists(mini_installer_path), ('Could not find file %s' % | 334 assert os.path.exists(mini_installer_path), ('Could not find file %s' % |
| 351 mini_installer_path) | 335 mini_installer_path) |
| 352 | 336 |
| 353 next_version_mini_installer_path = os.path.join( | 337 next_version_mini_installer_path = os.path.join( |
| 354 args.build_dir, args.target, 'next_version_mini_installer.exe') | 338 args.build_dir, args.target, 'next_version_mini_installer.exe') |
| 355 assert os.path.exists(next_version_mini_installer_path), ( | 339 assert os.path.exists(next_version_mini_installer_path), ( |
| 356 'Could not find file %s' % next_version_mini_installer_path) | 340 'Could not find file %s' % next_version_mini_installer_path) |
| 357 | 341 |
| 358 suite = unittest.TestSuite() | 342 suite = unittest.TestSuite() |
| 359 | 343 |
| 360 variable_expander = VariableExpander(mini_installer_path, | 344 variable_expander = VariableExpander(mini_installer_path, |
| 361 next_version_mini_installer_path) | 345 next_version_mini_installer_path) |
| 362 config = ParseConfigFile(args.config, variable_expander) | 346 config = ParseConfigFile(args.config, variable_expander) |
| 363 | 347 |
| 364 RunCleanCommand(args.force_clean, variable_expander) | 348 RunCleanCommand(args.force_clean, config.states[args.clean_state], |
| 349 variable_expander) |
| 365 for test in config.tests: | 350 for test in config.tests: |
| 366 # If tests were specified via |tests|, their names are formatted like so: | 351 # If tests were specified via |tests|, their names are formatted like so: |
| 367 test_name = '%s.%s.%s' % (InstallerTest.__module__, | 352 test_name = '%s.%s.%s' % (InstallerTest.__module__, |
| 368 InstallerTest.__name__, | 353 InstallerTest.__name__, |
| 369 test['name']) | 354 test['name']) |
| 370 if not args.test or test_name in args.test: | 355 if not args.test or test_name in args.test: |
| 371 suite.addTest(InstallerTest(test['name'], test['traversal'], config, | 356 suite.addTest(InstallerTest(test['name'], test['traversal'], config, |
| 372 variable_expander, args.quiet)) | 357 variable_expander, args.quiet, |
| 358 args.clean_state)) |
| 373 | 359 |
| 374 verbosity = 2 if not args.quiet else 1 | 360 verbosity = 2 if not args.quiet else 1 |
| 375 result = unittest.TextTestRunner(verbosity=verbosity).run(suite) | 361 result = unittest.TextTestRunner(verbosity=verbosity).run(suite) |
| 376 if args.write_full_results_to: | 362 if args.write_full_results_to: |
| 377 with open(args.write_full_results_to, 'w') as fp: | 363 with open(args.write_full_results_to, 'w') as fp: |
| 378 json.dump(_FullResults(suite, result, {}), fp, indent=2) | 364 json.dump(_FullResults(suite, result, {}), fp, indent=2) |
| 379 fp.write('\n') | 365 fp.write('\n') |
| 380 return 0 if result.wasSuccessful() else 1 | 366 return 0 if result.wasSuccessful() else 1 |
| 381 | 367 |
| 382 | 368 |
| (...skipping 59 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 442 trie[path] = value | 428 trie[path] = value |
| 443 return | 429 return |
| 444 directory, rest = path.split(TEST_SEPARATOR, 1) | 430 directory, rest = path.split(TEST_SEPARATOR, 1) |
| 445 if directory not in trie: | 431 if directory not in trie: |
| 446 trie[directory] = {} | 432 trie[directory] = {} |
| 447 _AddPathToTrie(trie[directory], rest, value) | 433 _AddPathToTrie(trie[directory], rest, value) |
| 448 | 434 |
| 449 | 435 |
| 450 if __name__ == '__main__': | 436 if __name__ == '__main__': |
| 451 sys.exit(main()) | 437 sys.exit(main()) |
| OLD | NEW |