Home | History | Annotate | Download | only in mini_installer
      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 json
     13 import optparse
     14 import os
     15 import subprocess
     16 import sys
     17 import unittest
     18 
     19 from variable_expander import VariableExpander
     20 import verifier_runner
     21 
     22 
     23 class Config:
     24   """Describes the machine states, actions, and test cases.
     25 
     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 = []
     37 
     38 
     39 class InstallerTest(unittest.TestCase):
     40   """Tests a test case in the config file."""
     41 
     42   def __init__(self, test, config, variable_expander):
     43     """Constructor.
     44 
     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
     57 
     58   def __str__(self):
     59     """Returns a string representing the test case.
     60 
     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))
     66 
     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')
     73 
     74     state = self._test[0]
     75     self._VerifyState(state)
     76 
     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)
     81 
     82       state = self._test[i + 1]
     83       self._VerifyState(state)
     84 
     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
     88 
     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)
     93 
     94   def shortDescription(self):
     95     """Overridden from unittest.TestCase.
     96 
     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.
    101     """
    102     return None
    103 
    104   def _VerifyState(self, state):
    105     """Verifies that the current machine state matches a given state.
    106 
    107     Args:
    108       state: A state name.
    109     """
    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))
    117 
    118 
    119 def RunCommand(command, variable_expander):
    120   """Runs the given command from the current file's directory.
    121 
    122   This function throws an Exception if the command returns with non-zero exit
    123   status.
    124 
    125   Args:
    126     command: A command to run. It is expanded using Expand.
    127     variable_expander: A VariableExpander object.
    128   """
    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))
    135 
    136 
    137 def RunCleanCommand(force_clean, variable_expander):
    138   """Puts the machine in the clean state (i.e. Chrome not installed).
    139 
    140   Args:
    141     force_clean: A boolean indicating whether to force cleaning existing
    142         installations.
    143     variable_expander: A VariableExpander object.
    144   """
    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)
    156 
    157 
    158 def MergePropertyDictionaries(current_property, new_property):
    159   """Merges the new property dictionary into the current property dictionary.
    160 
    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
    165 
    166   Args:
    167     current_property: The property dictionary to be modified.
    168     new_property: The new property dictionary.
    169   """
    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())
    180 
    181 
    182 def ParsePropertyFiles(directory, filenames):
    183   """Parses an array of .prop files.
    184 
    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.
    189 
    190   Returns:
    191     A property dictionary created by merging all property dictionaries specified
    192         in the array.
    193   """
    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
    200 
    201 
    202 def ParseConfigFile(filename):
    203   """Parses a .config file.
    204 
    205   Args:
    206     config_filename: A Config filename.
    207 
    208   Returns:
    209     A Config object.
    210   """
    211   config_data = json.load(open(filename, 'r'))
    212   directory = os.path.dirname(os.path.abspath(filename))
    213 
    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
    222 
    223 
    224 def RunTests(mini_installer_path, config, force_clean):
    225   """Tests the installer using the given Config object.
    226 
    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.
    232 
    233   Returns:
    234     True if all the tests passed, or False otherwise.
    235   """
    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()
    243 
    244 
    245 def IsComponentBuild(mini_installer_path):
    246   """ Invokes the mini_installer asking whether it is a component build.
    247 
    248   Args:
    249     mini_installer_path: The path to mini_installer.exe.
    250 
    251   Returns:
    252     True if the mini_installer is a component build, False otherwise.
    253   """
    254   query_command = mini_installer_path + ' --query-component-build'
    255   script_dir = os.path.dirname(os.path.abspath(__file__))
    256   exit_status = subprocess.call(query_command, shell=True, cwd=script_dir)
    257   return exit_status != 0
    258 
    259 
    260 def main():
    261   usage = 'usage: %prog [options] config_filename'
    262   parser = optparse.OptionParser(usage, description='Test the installer.')
    263   parser.add_option('--build-dir', default='out',
    264                     help='Path to main build directory (the parent of the '
    265                          'Release or Debug directory)')
    266   parser.add_option('--target', default='Release',
    267                     help='Build target (Release or Debug)')
    268   parser.add_option('--force-clean', action='store_true', dest='force_clean',
    269                     default=False, help='Force cleaning existing installations')
    270   options, args = parser.parse_args()
    271   if len(args) != 1:
    272     parser.error('Incorrect number of arguments.')
    273   config_filename = args[0]
    274 
    275   mini_installer_path = os.path.join(options.build_dir, options.target,
    276                                      'mini_installer.exe')
    277   assert os.path.exists(mini_installer_path), ('Could not find file %s' %
    278                                                mini_installer_path)
    279 
    280   # Set the env var used by mini_installer.exe to decide to not show UI.
    281   os.environ['MINI_INSTALLER_TEST'] = '1'
    282   if IsComponentBuild(mini_installer_path):
    283     print ('Component build is currently unsupported by the mini_installer: '
    284            'http://crbug.com/377839')
    285     return 0
    286 
    287   config = ParseConfigFile(config_filename)
    288   if not RunTests(mini_installer_path, config, options.force_clean):
    289     return 1
    290   return 0
    291 
    292 
    293 if __name__ == '__main__':
    294   sys.exit(main())
    295