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 |