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. 4 5 """This script tests the installer with test cases specified in the config file. 6 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 """ 11 12 import argparse 13 import json 14 import os 15 import subprocess 16 import unittest 17 18 import verifier 19 20 21 class Config: 22 """Describes the machine states, actions, and test cases. 23 24 Attributes: 25 states: A dictionary where each key is a state name and the associated value 26 is a property dictionary describing that state. 27 actions: A dictionary where each key is an action name and the associated 28 value is the action's command. 29 tests: An array of test cases. 30 """ 31 def __init__(self): 32 self.states = {} 33 self.actions = {} 34 self.tests = [] 35 36 37 class InstallerTest(unittest.TestCase): 38 """Tests a test case in the config file.""" 39 40 def __init__(self, test, config): 41 """Constructor. 42 43 Args: 44 test: An array of alternating state names and action names, starting and 45 ending with state names. 46 config: The Config object. 47 """ 48 super(InstallerTest, self).__init__() 49 self._test = test 50 self._config = config 51 52 def __str__(self): 53 """Returns a string representing the test case. 54 55 Returns: 56 A string created by joining state names and action names together with 57 ' -> ', for example, 'Test: clean -> install chrome -> chrome_installed'. 58 """ 59 return 'Test: %s' % (' -> '.join(self._test)) 60 61 def runTest(self): 62 """Run the test case.""" 63 # |test| is an array of alternating state names and action names, starting 64 # and ending with state names. Therefore, its length must be odd. 65 self.assertEqual(1, len(self._test) % 2, 66 'The length of test array must be odd') 67 68 # TODO(sukolsak): run a reset command that puts the machine in clean state. 69 70 state = self._test[0] 71 self._VerifyState(state) 72 73 # Starting at index 1, we loop through pairs of (action, state). 74 for i in range(1, len(self._test), 2): 75 action = self._test[i] 76 self._RunCommand(self._config.actions[action]) 77 78 state = self._test[i + 1] 79 self._VerifyState(state) 80 81 def shortDescription(self): 82 """Overridden from unittest.TestCase. 83 84 We return None as the short description to suppress its printing. 85 The default implementation of this method returns the docstring of the 86 runTest method, which is not useful since it's the same for every test case. 87 The description from the __str__ method is informative enough. 88 """ 89 return None 90 91 def _VerifyState(self, state): 92 """Verifies that the current machine state matches a given state. 93 94 Args: 95 state: A state name. 96 """ 97 try: 98 verifier.Verify(self._config.states[state]) 99 except AssertionError as e: 100 # If an AssertionError occurs, we intercept it and add the state name 101 # to the error message so that we know where the test fails. 102 raise AssertionError("In state '%s', %s" % (state, e)) 103 104 def _RunCommand(self, command): 105 subprocess.call(command, shell=True) 106 107 108 def MergePropertyDictionaries(current_property, new_property): 109 """Merges the new property dictionary into the current property dictionary. 110 111 This is different from general dictionary merging in that, in case there are 112 keys with the same name, we merge values together in the first level, and we 113 override earlier values in the second level. For more details, take a look at 114 http://goo.gl/uE0RoR 115 116 Args: 117 current_property: The property dictionary to be modified. 118 new_property: The new property dictionary. 119 """ 120 for key, value in new_property.iteritems(): 121 if key not in current_property: 122 current_property[key] = value 123 else: 124 assert(isinstance(current_property[key], dict) and 125 isinstance(value, dict)) 126 # This merges two dictionaries together. In case there are keys with 127 # the same name, the latter will override the former. 128 current_property[key] = dict( 129 current_property[key].items() + value.items()) 130 131 132 def ParsePropertyFiles(directory, filenames): 133 """Parses an array of .prop files. 134 135 Args: 136 property_filenames: An array of Property filenames. 137 directory: The directory where the Config file and all Property files 138 reside in. 139 140 Returns: 141 A property dictionary created by merging all property dictionaries specified 142 in the array. 143 """ 144 current_property = {} 145 for filename in filenames: 146 path = os.path.join(directory, filename) 147 new_property = json.load(open(path)) 148 MergePropertyDictionaries(current_property, new_property) 149 return current_property 150 151 152 def ParseConfigFile(filename): 153 """Parses a .config file. 154 155 Args: 156 config_filename: A Config filename. 157 158 Returns: 159 A Config object. 160 """ 161 config_data = json.load(open(filename, 'r')) 162 directory = os.path.dirname(os.path.abspath(filename)) 163 164 config = Config() 165 config.tests = config_data['tests'] 166 for state_name, state_property_filenames in config_data['states']: 167 config.states[state_name] = ParsePropertyFiles(directory, 168 state_property_filenames) 169 for action_name, action_command in config_data['actions']: 170 config.actions[action_name] = action_command 171 return config 172 173 174 def RunTests(config): 175 """Tests the installer using the given Config object. 176 177 Args: 178 config: A Config object. 179 """ 180 suite = unittest.TestSuite() 181 for test in config.tests: 182 suite.addTest(InstallerTest(test, config)) 183 unittest.TextTestRunner(verbosity=2).run(suite) 184 185 186 def main(): 187 parser = argparse.ArgumentParser(description='Test the installer.') 188 parser.add_argument('config_filename', 189 help='The relative/absolute path to the config file.') 190 args = parser.parse_args() 191 192 config = ParseConfigFile(args.config_filename) 193 RunTests(config) 194 195 196 if __name__ == '__main__': 197 main() 198