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
19 from variable_expander
import VariableExpander
20 import verifier_runner
24 """Describes the machine states, actions, and test cases.
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.
39 class InstallerTest(unittest
.TestCase
):
40 """Tests a test case in the config file."""
42 def __init__(self
, test
, config
, variable_expander
):
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.
51 super(InstallerTest
, self
).__init
__()
54 self
._variable
_expander
= variable_expander
55 self
._verifier
_runner
= verifier_runner
.VerifierRunner()
56 self
._clean
_on
_teardown
= True
59 """Returns a string representing the test case.
62 A string created by joining state names and action names together with
63 ' -> ', for example, 'Test: clean -> install chrome -> chrome_installed'.
65 return 'Test: %s\n' % (' -> '.join(self
._test
))
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')
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
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.
104 def _VerifyState(self
, state
):
105 """Verifies that the current machine state matches a given state.
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
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
)
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).
141 force_clean: A boolean indicating whether to force cleaning existing
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.
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
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
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.
186 property_filenames: An array of Property filenames.
187 directory: The directory where the Config file and all Property files
191 A property dictionary created by merging all property dictionaries specified
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.
206 config_filename: A Config filename.
211 config_data
= json
.load(open(filename
, 'r'))
212 directory
= os
.path
.dirname(os
.path
.abspath(filename
))
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
224 def RunTests(mini_installer_path
, config
, force_clean
):
225 """Tests the installer using the given Config object.
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
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()
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()
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' %
264 config
= ParseConfigFile(config_filename
)
265 if not RunTests(mini_installer_path
, config
, options
.force_clean
):
270 if __name__
== '__main__':