Chromium Code Reviews| 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 """ |
| (...skipping 34 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 45 Args: | 45 Args: |
| 46 test: An array of alternating state names and action names, starting and | 46 test: An array of alternating state names and action names, starting and |
| 47 ending with state names. | 47 ending with state names. |
| 48 config: The Config object. | 48 config: The Config object. |
| 49 path_resolver: A PathResolver object. | 49 path_resolver: A PathResolver object. |
| 50 """ | 50 """ |
| 51 super(InstallerTest, self).__init__() | 51 super(InstallerTest, self).__init__() |
| 52 self._test = test | 52 self._test = test |
| 53 self._config = config | 53 self._config = config |
| 54 self._path_resolver = path_resolver | 54 self._path_resolver = path_resolver |
| 55 self._clean_on_teardown = True | |
| 55 | 56 |
| 56 def __str__(self): | 57 def __str__(self): |
| 57 """Returns a string representing the test case. | 58 """Returns a string representing the test case. |
| 58 | 59 |
| 59 Returns: | 60 Returns: |
| 60 A string created by joining state names and action names together with | 61 A string created by joining state names and action names together with |
| 61 ' -> ', for example, 'Test: clean -> install chrome -> chrome_installed'. | 62 ' -> ', for example, 'Test: clean -> install chrome -> chrome_installed'. |
| 62 """ | 63 """ |
| 63 return 'Test: %s' % (' -> '.join(self._test)) | 64 return 'Test: %s' % (' -> '.join(self._test)) |
| 64 | 65 |
| 65 def runTest(self): | 66 def runTest(self): |
| 66 """Run the test case.""" | 67 """Run the test case.""" |
| 67 # |test| is an array of alternating state names and action names, starting | 68 # |test| is an array of alternating state names and action names, starting |
| 68 # and ending with state names. Therefore, its length must be odd. | 69 # and ending with state names. Therefore, its length must be odd. |
| 69 self.assertEqual(1, len(self._test) % 2, | 70 self.assertEqual(1, len(self._test) % 2, |
| 70 'The length of test array must be odd') | 71 'The length of test array must be odd') |
| 71 | 72 |
| 72 # TODO(sukolsak): run a reset command that puts the machine in clean state. | |
| 73 | |
| 74 state = self._test[0] | 73 state = self._test[0] |
| 75 self._VerifyState(state) | 74 self._VerifyState(state) |
| 76 | 75 |
| 77 # Starting at index 1, we loop through pairs of (action, state). | 76 # Starting at index 1, we loop through pairs of (action, state). |
| 78 for i in range(1, len(self._test), 2): | 77 for i in range(1, len(self._test), 2): |
| 79 action = self._test[i] | 78 action = self._test[i] |
| 80 self._RunCommand(self._config.actions[action]) | 79 RunCommand(self._config.actions[action], self._path_resolver) |
| 81 | 80 |
| 82 state = self._test[i + 1] | 81 state = self._test[i + 1] |
| 83 self._VerifyState(state) | 82 self._VerifyState(state) |
| 84 | 83 |
| 84 # If the test makes it here, it means it was successful, because RunCommand | |
| 85 # and _VerifyState throw an exception on failure. | |
| 86 self._clean_on_teardown = False | |
| 87 | |
| 88 def tearDown(self): | |
| 89 """Cleans up the machine if the test case fails.""" | |
| 90 if self._clean_on_teardown: | |
| 91 RunCleanCommand(True, self._path_resolver) | |
| 92 | |
| 85 def shortDescription(self): | 93 def shortDescription(self): |
| 86 """Overridden from unittest.TestCase. | 94 """Overridden from unittest.TestCase. |
| 87 | 95 |
| 88 We return None as the short description to suppress its printing. | 96 We return None as the short description to suppress its printing. |
| 89 The default implementation of this method returns the docstring of the | 97 The default implementation of this method returns the docstring of the |
| 90 runTest method, which is not useful since it's the same for every test case. | 98 runTest method, which is not useful since it's the same for every test case. |
| 91 The description from the __str__ method is informative enough. | 99 The description from the __str__ method is informative enough. |
| 92 """ | 100 """ |
| 93 return None | 101 return None |
| 94 | 102 |
| 95 def _VerifyState(self, state): | 103 def _VerifyState(self, state): |
| 96 """Verifies that the current machine state matches a given state. | 104 """Verifies that the current machine state matches a given state. |
| 97 | 105 |
| 98 Args: | 106 Args: |
| 99 state: A state name. | 107 state: A state name. |
| 100 """ | 108 """ |
| 101 try: | 109 try: |
| 102 verifier.Verify(self._config.states[state], self._path_resolver) | 110 verifier.Verify(self._config.states[state], self._path_resolver) |
| 103 except AssertionError as e: | 111 except AssertionError as e: |
| 104 # If an AssertionError occurs, we intercept it and add the state name | 112 # If an AssertionError occurs, we intercept it and add the state name |
| 105 # to the error message so that we know where the test fails. | 113 # to the error message so that we know where the test fails. |
| 106 raise AssertionError("In state '%s', %s" % (state, e)) | 114 raise AssertionError("In state '%s', %s" % (state, e)) |
| 107 | 115 |
| 108 def _RunCommand(self, command): | |
| 109 """Runs the given command from the current file's directory. | |
| 110 | 116 |
| 111 Args: | 117 def RunCommand(command, path_resolver): |
| 112 command: A command to run. It is expanded using ResolvePath. | 118 """Runs the given command from the current file's directory. |
| 113 """ | 119 |
| 114 resolved_command = self._path_resolver.ResolvePath(command) | 120 This function throws an Exception if the command returns with non-zero exit |
| 115 script_dir = os.path.dirname(os.path.abspath(__file__)) | 121 status. |
| 116 exit_status = subprocess.call(resolved_command, shell=True, cwd=script_dir) | 122 |
| 117 if exit_status != 0: | 123 Args: |
| 118 self.fail('Command %s returned non-zero exit status %s' % ( | 124 command: A command to run. It is expanded using ResolvePath. |
| 119 resolved_command, exit_status)) | 125 path_resolver: A PathResolver object. |
| 126 """ | |
| 127 resolved_command = path_resolver.ResolvePath(command) | |
| 128 script_dir = os.path.dirname(os.path.abspath(__file__)) | |
| 129 exit_status = subprocess.call(resolved_command, shell=True, cwd=script_dir) | |
| 130 if exit_status != 0: | |
| 131 raise Exception('Command %s returned non-zero exit status %s' % ( | |
| 132 command, exit_status)) | |
| 133 | |
| 134 | |
| 135 def RunCleanCommand(force_clean, path_resolver): | |
| 136 """Puts the machine in the clean state (i.e. Chrome not installed). | |
| 137 | |
| 138 Args: | |
| 139 force_clean: A boolean indicating whether to force cleaning existing | |
| 140 installations. | |
| 141 path_resolver: A PathResolver object. | |
| 142 """ | |
| 143 # TODO(sukolsak): Read the clean state from the config file and clean | |
| 144 # the machine according to it. | |
| 145 # TODO(sukolsak): Handle Chrome SxS installs. | |
| 146 commands = [] | |
| 147 interactive_option = '--interactive' if not force_clean else '' | |
| 148 for level_option in ['', '--system-level']: | |
|
Mathieu
2013/09/16 22:32:12
very optional nit: tuples are more memory efficien
sukolsak
2013/09/16 22:38:54
Done.
| |
| 149 commands.append('python uninstall_chrome.py ' | |
| 150 '--chrome-long-name="$CHROME_LONG_NAME" ' | |
| 151 '--no-error-if-absent %s %s' % | |
| 152 (level_option, interactive_option)) | |
| 153 RunCommand(' && '.join(commands), path_resolver) | |
| 120 | 154 |
| 121 | 155 |
| 122 def MergePropertyDictionaries(current_property, new_property): | 156 def MergePropertyDictionaries(current_property, new_property): |
| 123 """Merges the new property dictionary into the current property dictionary. | 157 """Merges the new property dictionary into the current property dictionary. |
| 124 | 158 |
| 125 This is different from general dictionary merging in that, in case there are | 159 This is different from general dictionary merging in that, in case there are |
| 126 keys with the same name, we merge values together in the first level, and we | 160 keys with the same name, we merge values together in the first level, and we |
| 127 override earlier values in the second level. For more details, take a look at | 161 override earlier values in the second level. For more details, take a look at |
| 128 http://goo.gl/uE0RoR | 162 http://goo.gl/uE0RoR |
| 129 | 163 |
| (...skipping 48 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 178 config = Config() | 212 config = Config() |
| 179 config.tests = config_data['tests'] | 213 config.tests = config_data['tests'] |
| 180 for state_name, state_property_filenames in config_data['states']: | 214 for state_name, state_property_filenames in config_data['states']: |
| 181 config.states[state_name] = ParsePropertyFiles(directory, | 215 config.states[state_name] = ParsePropertyFiles(directory, |
| 182 state_property_filenames) | 216 state_property_filenames) |
| 183 for action_name, action_command in config_data['actions']: | 217 for action_name, action_command in config_data['actions']: |
| 184 config.actions[action_name] = action_command | 218 config.actions[action_name] = action_command |
| 185 return config | 219 return config |
| 186 | 220 |
| 187 | 221 |
| 188 def RunTests(mini_installer_path, config): | 222 def RunTests(mini_installer_path, config, force_clean): |
| 189 """Tests the installer using the given Config object. | 223 """Tests the installer using the given Config object. |
| 190 | 224 |
| 191 Args: | 225 Args: |
| 192 mini_installer_path: The path to mini_installer.exe. | 226 mini_installer_path: The path to mini_installer.exe. |
| 193 config: A Config object. | 227 config: A Config object. |
| 228 force_clean: A boolean indicating whether to force cleaning existing | |
| 229 installations. | |
| 194 | 230 |
| 195 Returns: | 231 Returns: |
| 196 True if all the tests passed, or False otherwise. | 232 True if all the tests passed, or False otherwise. |
| 197 """ | 233 """ |
| 198 suite = unittest.TestSuite() | 234 suite = unittest.TestSuite() |
| 199 path_resolver = PathResolver(mini_installer_path) | 235 path_resolver = PathResolver(mini_installer_path) |
| 236 RunCleanCommand(force_clean, path_resolver) | |
| 200 for test in config.tests: | 237 for test in config.tests: |
| 201 suite.addTest(InstallerTest(test, config, path_resolver)) | 238 suite.addTest(InstallerTest(test, config, path_resolver)) |
| 202 result = unittest.TextTestRunner(verbosity=2).run(suite) | 239 result = unittest.TextTestRunner(verbosity=2).run(suite) |
| 203 return result.wasSuccessful() | 240 return result.wasSuccessful() |
| 204 | 241 |
| 205 | 242 |
| 206 def main(): | 243 def main(): |
| 207 usage = 'usage: %prog [options] config_filename' | 244 usage = 'usage: %prog [options] config_filename' |
| 208 parser = optparse.OptionParser(usage, description='Test the installer.') | 245 parser = optparse.OptionParser(usage, description='Test the installer.') |
| 209 parser.add_option('--build-dir', default='out', | 246 parser.add_option('--build-dir', default='out', |
| 210 help='Path to main build directory (the parent of the ' | 247 help='Path to main build directory (the parent of the ' |
| 211 'Release or Debug directory)') | 248 'Release or Debug directory)') |
| 212 parser.add_option('--target', default='Release', | 249 parser.add_option('--target', default='Release', |
| 213 help='Build target (Release or Debug)') | 250 help='Build target (Release or Debug)') |
| 251 parser.add_option('--force-clean', action='store_true', dest='force_clean', | |
| 252 default=False, help='Force cleaning existing installations') | |
| 214 options, args = parser.parse_args() | 253 options, args = parser.parse_args() |
| 215 if len(args) != 1: | 254 if len(args) != 1: |
| 216 parser.error('Incorrect number of arguments.') | 255 parser.error('Incorrect number of arguments.') |
| 217 config_filename = args[0] | 256 config_filename = args[0] |
| 218 | 257 |
| 219 mini_installer_path = os.path.join(options.build_dir, options.target, | 258 mini_installer_path = os.path.join(options.build_dir, options.target, |
| 220 'mini_installer.exe') | 259 'mini_installer.exe') |
| 221 assert os.path.exists(mini_installer_path), ('Could not find file %s' % | 260 assert os.path.exists(mini_installer_path), ('Could not find file %s' % |
| 222 mini_installer_path) | 261 mini_installer_path) |
| 223 config = ParseConfigFile(config_filename) | 262 config = ParseConfigFile(config_filename) |
| 224 if not RunTests(mini_installer_path, config): | 263 if not RunTests(mini_installer_path, config, options.force_clean): |
| 225 return 1 | 264 return 1 |
| 226 return 0 | 265 return 0 |
| 227 | 266 |
| 228 | 267 |
| 229 if __name__ == '__main__': | 268 if __name__ == '__main__': |
| 230 sys.exit(main()) | 269 sys.exit(main()) |
| OLD | NEW |