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 |