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._was_successful = False | |
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 self._was_successful = True | |
gab
2013/09/16 18:33:45
Add a comment stating why the test is successful i
sukolsak
2013/09/16 22:02:02
Done.
| |
85 | |
86 def tearDown(self): | |
87 """Cleans up the machine if the test case fails or raises an exception.""" | |
gab
2013/09/16 18:33:45
Isn't "fail" and "raised an exception" the same th
sukolsak
2013/09/16 22:02:02
Done. Python unittest makes a distinction between
| |
88 if not self._was_successful: | |
gab
2013/09/16 18:41:22
Rename |_was_successful| to something like |clean_
sukolsak
2013/09/16 22:02:02
Done.
| |
89 RunCleanCommand(True, self._path_resolver) | |
90 | |
85 def shortDescription(self): | 91 def shortDescription(self): |
86 """Overridden from unittest.TestCase. | 92 """Overridden from unittest.TestCase. |
87 | 93 |
88 We return None as the short description to suppress its printing. | 94 We return None as the short description to suppress its printing. |
89 The default implementation of this method returns the docstring of the | 95 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. | 96 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. | 97 The description from the __str__ method is informative enough. |
92 """ | 98 """ |
93 return None | 99 return None |
94 | 100 |
95 def _VerifyState(self, state): | 101 def _VerifyState(self, state): |
96 """Verifies that the current machine state matches a given state. | 102 """Verifies that the current machine state matches a given state. |
97 | 103 |
98 Args: | 104 Args: |
99 state: A state name. | 105 state: A state name. |
100 """ | 106 """ |
101 try: | 107 try: |
102 verifier.Verify(self._config.states[state], self._path_resolver) | 108 verifier.Verify(self._config.states[state], self._path_resolver) |
103 except AssertionError as e: | 109 except AssertionError as e: |
104 # If an AssertionError occurs, we intercept it and add the state name | 110 # 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. | 111 # to the error message so that we know where the test fails. |
106 raise AssertionError("In state '%s', %s" % (state, e)) | 112 raise AssertionError("In state '%s', %s" % (state, e)) |
107 | 113 |
108 def _RunCommand(self, command): | |
109 """Runs the given command from the current file's directory. | |
110 | 114 |
111 Args: | 115 def RunCommand(command, path_resolver): |
112 command: A command to run. It is expanded using ResolvePath. | 116 """Runs the given command from the current file's directory. |
113 """ | 117 |
114 resolved_command = self._path_resolver.ResolvePath(command) | 118 This function throws an Exception if the command returns with non-zero exit |
115 script_dir = os.path.dirname(os.path.abspath(__file__)) | 119 status. |
116 exit_status = subprocess.call(resolved_command, shell=True, cwd=script_dir) | 120 |
117 if exit_status != 0: | 121 Args: |
118 self.fail('Command %s returned non-zero exit status %s' % ( | 122 command: A command to run. It is expanded using ResolvePath. |
119 resolved_command, exit_status)) | 123 path_resolver: A PathResolver object. |
124 """ | |
125 resolved_command = path_resolver.ResolvePath(command) | |
126 script_dir = os.path.dirname(os.path.abspath(__file__)) | |
127 exit_status = subprocess.call(resolved_command, shell=True, cwd=script_dir) | |
128 if exit_status != 0: | |
129 raise Exception('Command %s returned non-zero exit status %s' % ( | |
gab
2013/09/16 18:41:22
Why go from self.fail to throwing an exception?
M
sukolsak
2013/09/16 22:02:02
Because RunCommand is now a global function. I cha
gab
2013/09/16 22:15:50
Ah I see, man python is so subtle for that, uninde
| |
130 command, exit_status)) | |
131 | |
132 | |
133 def RunCleanCommand(force_clean, path_resolver): | |
134 """Puts the machine in the clean state (i.e. Chrome not installed). | |
135 | |
136 Args: | |
137 force_clean: A boolean indicating whether to force cleaning existing | |
138 installations. | |
139 path_resolver: A PathResolver object. | |
140 """ | |
141 # TODO(sukolsak): Read the clean state from the config file and clean | |
142 # the machine according to it. | |
143 # TODO(sukolsak): Handle Chrome SxS installs. | |
144 commands = [] | |
145 interactive_option = '--interactive' if not force_clean else '' | |
146 for level_option in ['', '--system-level']: | |
147 commands.append('python uninstall_chrome.py ' | |
148 '--chrome-long-name="$CHROME_LONG_NAME" ' | |
149 '--no-error-if-absent %s %s' % | |
150 (level_option, interactive_option)) | |
151 RunCommand(' && '.join(commands), path_resolver) | |
120 | 152 |
121 | 153 |
122 def MergePropertyDictionaries(current_property, new_property): | 154 def MergePropertyDictionaries(current_property, new_property): |
123 """Merges the new property dictionary into the current property dictionary. | 155 """Merges the new property dictionary into the current property dictionary. |
124 | 156 |
125 This is different from general dictionary merging in that, in case there are | 157 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 | 158 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 | 159 override earlier values in the second level. For more details, take a look at |
128 http://goo.gl/uE0RoR | 160 http://goo.gl/uE0RoR |
129 | 161 |
(...skipping 48 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
178 config = Config() | 210 config = Config() |
179 config.tests = config_data['tests'] | 211 config.tests = config_data['tests'] |
180 for state_name, state_property_filenames in config_data['states']: | 212 for state_name, state_property_filenames in config_data['states']: |
181 config.states[state_name] = ParsePropertyFiles(directory, | 213 config.states[state_name] = ParsePropertyFiles(directory, |
182 state_property_filenames) | 214 state_property_filenames) |
183 for action_name, action_command in config_data['actions']: | 215 for action_name, action_command in config_data['actions']: |
184 config.actions[action_name] = action_command | 216 config.actions[action_name] = action_command |
185 return config | 217 return config |
186 | 218 |
187 | 219 |
188 def RunTests(mini_installer_path, config): | 220 def RunTests(mini_installer_path, config, force_clean): |
189 """Tests the installer using the given Config object. | 221 """Tests the installer using the given Config object. |
190 | 222 |
191 Args: | 223 Args: |
192 mini_installer_path: The path to mini_installer.exe. | 224 mini_installer_path: The path to mini_installer.exe. |
193 config: A Config object. | 225 config: A Config object. |
226 force_clean: A boolean indicating whether to force cleaning existing | |
227 installations. | |
194 | 228 |
195 Returns: | 229 Returns: |
196 True if all the tests passed, or False otherwise. | 230 True if all the tests passed, or False otherwise. |
197 """ | 231 """ |
198 suite = unittest.TestSuite() | 232 suite = unittest.TestSuite() |
199 path_resolver = PathResolver(mini_installer_path) | 233 path_resolver = PathResolver(mini_installer_path) |
234 RunCleanCommand(force_clean, path_resolver) | |
200 for test in config.tests: | 235 for test in config.tests: |
201 suite.addTest(InstallerTest(test, config, path_resolver)) | 236 suite.addTest(InstallerTest(test, config, path_resolver)) |
202 result = unittest.TextTestRunner(verbosity=2).run(suite) | 237 result = unittest.TextTestRunner(verbosity=2).run(suite) |
203 return result.wasSuccessful() | 238 return result.wasSuccessful() |
204 | 239 |
205 | 240 |
206 def main(): | 241 def main(): |
207 usage = 'usage: %prog [options] config_filename' | 242 usage = 'usage: %prog [options] config_filename' |
208 parser = optparse.OptionParser(usage, description='Test the installer.') | 243 parser = optparse.OptionParser(usage, description='Test the installer.') |
209 parser.add_option('--build-dir', default='out', | 244 parser.add_option('--build-dir', default='out', |
210 help='Path to main build directory (the parent of the ' | 245 help='Path to main build directory (the parent of the ' |
211 'Release or Debug directory)') | 246 'Release or Debug directory)') |
212 parser.add_option('--target', default='Release', | 247 parser.add_option('--target', default='Release', |
213 help='Build target (Release or Debug)') | 248 help='Build target (Release or Debug)') |
249 parser.add_option('--force-clean', action='store_true', dest='force_clean', | |
250 default=False, help='Force cleaning existing installations') | |
214 options, args = parser.parse_args() | 251 options, args = parser.parse_args() |
215 if len(args) != 1: | 252 if len(args) != 1: |
216 parser.error('Incorrect number of arguments.') | 253 parser.error('Incorrect number of arguments.') |
217 config_filename = args[0] | 254 config_filename = args[0] |
218 | 255 |
219 mini_installer_path = os.path.join(options.build_dir, options.target, | 256 mini_installer_path = os.path.join(options.build_dir, options.target, |
220 'mini_installer.exe') | 257 'mini_installer.exe') |
221 assert os.path.exists(mini_installer_path), ('Could not find file %s' % | 258 assert os.path.exists(mini_installer_path), ('Could not find file %s' % |
222 mini_installer_path) | 259 mini_installer_path) |
223 config = ParseConfigFile(config_filename) | 260 config = ParseConfigFile(config_filename) |
224 if not RunTests(mini_installer_path, config): | 261 if not RunTests(mini_installer_path, config, options.force_clean): |
225 return 1 | 262 return 1 |
226 return 0 | 263 return 0 |
227 | 264 |
228 | 265 |
229 if __name__ == '__main__': | 266 if __name__ == '__main__': |
230 sys.exit(main()) | 267 sys.exit(main()) |
OLD | NEW |