Disable view source for Developer Tools.
[chromium-blink-merge.git] / chrome / test / mini_installer / test_installer.py
blob669ca7bc5aec2209fb058b604d051bc6886f271f
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
3 # found in the LICENSE file.
5 """This script tests the installer with test cases specified in the config file.
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
9 the design documentation at http://goo.gl/Q0rGM6
10 """
12 import json
13 import optparse
14 import os
15 import subprocess
16 import sys
17 import unittest
19 from variable_expander import VariableExpander
20 import verifier_runner
23 class Config:
24 """Describes the machine states, actions, and test cases.
26 Attributes:
27 states: A dictionary where each key is a state name and the associated value
28 is a property dictionary describing that state.
29 actions: A dictionary where each key is an action name and the associated
30 value is the action's command.
31 tests: An array of test cases.
32 """
33 def __init__(self):
34 self.states = {}
35 self.actions = {}
36 self.tests = []
39 class InstallerTest(unittest.TestCase):
40 """Tests a test case in the config file."""
42 def __init__(self, test, config, variable_expander):
43 """Constructor.
45 Args:
46 test: An array of alternating state names and action names, starting and
47 ending with state names.
48 config: The Config object.
49 variable_expander: A VariableExpander object.
50 """
51 super(InstallerTest, self).__init__()
52 self._test = test
53 self._config = config
54 self._variable_expander = variable_expander
55 self._verifier_runner = verifier_runner.VerifierRunner()
56 self._clean_on_teardown = True
58 def __str__(self):
59 """Returns a string representing the test case.
61 Returns:
62 A string created by joining state names and action names together with
63 ' -> ', for example, 'Test: clean -> install chrome -> chrome_installed'.
64 """
65 return 'Test: %s\n' % (' -> '.join(self._test))
67 def runTest(self):
68 """Run the test case."""
69 # |test| is an array of alternating state names and action names, starting
70 # and ending with state names. Therefore, its length must be odd.
71 self.assertEqual(1, len(self._test) % 2,
72 'The length of test array must be odd')
74 state = self._test[0]
75 self._VerifyState(state)
77 # Starting at index 1, we loop through pairs of (action, state).
78 for i in range(1, len(self._test), 2):
79 action = self._test[i]
80 RunCommand(self._config.actions[action], self._variable_expander)
82 state = self._test[i + 1]
83 self._VerifyState(state)
85 # If the test makes it here, it means it was successful, because RunCommand
86 # and _VerifyState throw an exception on failure.
87 self._clean_on_teardown = False
89 def tearDown(self):
90 """Cleans up the machine if the test case fails."""
91 if self._clean_on_teardown:
92 RunCleanCommand(True, self._variable_expander)
94 def shortDescription(self):
95 """Overridden from unittest.TestCase.
97 We return None as the short description to suppress its printing.
98 The default implementation of this method returns the docstring of the
99 runTest method, which is not useful since it's the same for every test case.
100 The description from the __str__ method is informative enough.
102 return None
104 def _VerifyState(self, state):
105 """Verifies that the current machine state matches a given state.
107 Args:
108 state: A state name.
110 try:
111 self._verifier_runner.VerifyAll(self._config.states[state],
112 self._variable_expander)
113 except AssertionError as e:
114 # If an AssertionError occurs, we intercept it and add the state name
115 # to the error message so that we know where the test fails.
116 raise AssertionError("In state '%s', %s" % (state, e))
119 def RunCommand(command, variable_expander):
120 """Runs the given command from the current file's directory.
122 This function throws an Exception if the command returns with non-zero exit
123 status.
125 Args:
126 command: A command to run. It is expanded using Expand.
127 variable_expander: A VariableExpander object.
129 expanded_command = variable_expander.Expand(command)
130 script_dir = os.path.dirname(os.path.abspath(__file__))
131 exit_status = subprocess.call(expanded_command, shell=True, cwd=script_dir)
132 if exit_status != 0:
133 raise Exception('Command %s returned non-zero exit status %s' % (
134 expanded_command, exit_status))
137 def RunCleanCommand(force_clean, variable_expander):
138 """Puts the machine in the clean state (i.e. Chrome not installed).
140 Args:
141 force_clean: A boolean indicating whether to force cleaning existing
142 installations.
143 variable_expander: A VariableExpander object.
145 # TODO(sukolsak): Read the clean state from the config file and clean
146 # the machine according to it.
147 # TODO(sukolsak): Handle Chrome SxS installs.
148 commands = []
149 interactive_option = '--interactive' if not force_clean else ''
150 for level_option in ('', '--system-level'):
151 commands.append('python uninstall_chrome.py '
152 '--chrome-long-name="$CHROME_LONG_NAME" '
153 '--no-error-if-absent %s %s' %
154 (level_option, interactive_option))
155 RunCommand(' && '.join(commands), variable_expander)
158 def MergePropertyDictionaries(current_property, new_property):
159 """Merges the new property dictionary into the current property dictionary.
161 This is different from general dictionary merging in that, in case there are
162 keys with the same name, we merge values together in the first level, and we
163 override earlier values in the second level. For more details, take a look at
164 http://goo.gl/uE0RoR
166 Args:
167 current_property: The property dictionary to be modified.
168 new_property: The new property dictionary.
170 for key, value in new_property.iteritems():
171 if key not in current_property:
172 current_property[key] = value
173 else:
174 assert(isinstance(current_property[key], dict) and
175 isinstance(value, dict))
176 # This merges two dictionaries together. In case there are keys with
177 # the same name, the latter will override the former.
178 current_property[key] = dict(
179 current_property[key].items() + value.items())
182 def ParsePropertyFiles(directory, filenames):
183 """Parses an array of .prop files.
185 Args:
186 property_filenames: An array of Property filenames.
187 directory: The directory where the Config file and all Property files
188 reside in.
190 Returns:
191 A property dictionary created by merging all property dictionaries specified
192 in the array.
194 current_property = {}
195 for filename in filenames:
196 path = os.path.join(directory, filename)
197 new_property = json.load(open(path))
198 MergePropertyDictionaries(current_property, new_property)
199 return current_property
202 def ParseConfigFile(filename):
203 """Parses a .config file.
205 Args:
206 config_filename: A Config filename.
208 Returns:
209 A Config object.
211 config_data = json.load(open(filename, 'r'))
212 directory = os.path.dirname(os.path.abspath(filename))
214 config = Config()
215 config.tests = config_data['tests']
216 for state_name, state_property_filenames in config_data['states']:
217 config.states[state_name] = ParsePropertyFiles(directory,
218 state_property_filenames)
219 for action_name, action_command in config_data['actions']:
220 config.actions[action_name] = action_command
221 return config
224 def RunTests(mini_installer_path, config, force_clean):
225 """Tests the installer using the given Config object.
227 Args:
228 mini_installer_path: The path to mini_installer.exe.
229 config: A Config object.
230 force_clean: A boolean indicating whether to force cleaning existing
231 installations.
233 Returns:
234 True if all the tests passed, or False otherwise.
236 suite = unittest.TestSuite()
237 variable_expander = VariableExpander(mini_installer_path)
238 RunCleanCommand(force_clean, variable_expander)
239 for test in config.tests:
240 suite.addTest(InstallerTest(test, config, variable_expander))
241 result = unittest.TextTestRunner(verbosity=2).run(suite)
242 return result.wasSuccessful()
245 def main():
246 usage = 'usage: %prog [options] config_filename'
247 parser = optparse.OptionParser(usage, description='Test the installer.')
248 parser.add_option('--build-dir', default='out',
249 help='Path to main build directory (the parent of the '
250 'Release or Debug directory)')
251 parser.add_option('--target', default='Release',
252 help='Build target (Release or Debug)')
253 parser.add_option('--force-clean', action='store_true', dest='force_clean',
254 default=False, help='Force cleaning existing installations')
255 options, args = parser.parse_args()
256 if len(args) != 1:
257 parser.error('Incorrect number of arguments.')
258 config_filename = args[0]
260 mini_installer_path = os.path.join(options.build_dir, options.target,
261 'mini_installer.exe')
262 assert os.path.exists(mini_installer_path), ('Could not find file %s' %
263 mini_installer_path)
264 config = ParseConfigFile(config_filename)
265 if not RunTests(mini_installer_path, config, options.force_clean):
266 return 1
267 return 0
270 if __name__ == '__main__':
271 sys.exit(main())