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 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