blob: 488aee19086aea3f8946b37e4f71ae52563e77e9 [file] [log] [blame]
# Copyright 2015 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Unittests for config."""
from __future__ import print_function
import copy
import cPickle
import mock
from chromite.cbuildbot import chromeos_config
from chromite.cbuildbot import config_lib
from chromite.lib import cros_test_lib
# pylint: disable=protected-access
def MockBuildConfig():
"""Create a BuildConfig object for convenient testing pleasure."""
site_config = MockSiteConfig()
return site_config['x86-generic-paladin']
def MockSiteConfig():
"""Create a SiteConfig object for convenient testing pleasure.
Shared amoung a number of unittest files, so be careful if changing it.
"""
result = config_lib.SiteConfig()
# Add a single, simple build config.
result.Add(
'x86-generic-paladin',
active_waterfall='chromiumos',
boards=['x86-generic'],
build_type='paladin',
chrome_sdk=True,
chrome_sdk_build_chrome=False,
description='Commit Queue',
doc='http://mock_url/',
image_test=True,
images=['base', 'test'],
important=True,
manifest_version=True,
prebuilts='public',
trybot_list=True,
upload_standalone_images=False,
vm_tests=['smoke_suite'],
)
return result
def AssertSiteIndependentParameters(site_config):
"""Helper function to test that SiteConfigs contain site-independent values.
Args:
site_config: A SiteConfig object.
Returns:
A boolean. True if the config contained all site-independent values.
False otherwise.
"""
# Enumerate the necessary site independent parameter keys.
# All keys must be documented.
# TODO (msartori): Fill in this list.
site_independent_params = [
]
site_params = site_config.params
return all([x in site_params for x in site_independent_params])
class _CustomObject(object):
"""Simple object. For testing deepcopy."""
def __init__(self, x):
self.x = x
def __eq__(self, other):
return self.x == other.x
class _CustomObjectWithSlots(object):
"""Simple object with slots. For testing deepcopy."""
__slots__ = ['x']
def __init__(self, x):
self.x = x
def __eq__(self, other):
return self.x == other.x
class BuildConfigClassTest(cros_test_lib.TestCase):
"""BuildConfig tests."""
def setUp(self):
self.fooConfig = config_lib.BuildConfig(name='foo', value=1)
self.barConfig = config_lib.BuildConfig(name='bar', value=2)
self.deepConfig = config_lib.BuildConfig(
name='deep', nested=[1, 2, 3], value=3,
child_configs=[self.fooConfig, self.barConfig])
self.config = {
'foo': self.fooConfig,
'bar': self.barConfig,
'deep': self.deepConfig,
}
def testMockSiteConfig(self):
"""Make sure Mock generator fucntion doesn't crash."""
site_config = MockSiteConfig()
self.assertIsNotNone(site_config)
build_config = MockBuildConfig()
self.assertIsNotNone(build_config)
def testValueAccess(self):
self.assertEqual(self.fooConfig.name, 'foo')
self.assertEqual(self.fooConfig.name, self.fooConfig['name'])
self.assertRaises(AttributeError, getattr, self.fooConfig, 'foobar')
def testDeleteKey(self):
base_config = config_lib.BuildConfig(foo='bar')
inherited_config = base_config.derive(
foo=config_lib.BuildConfig.delete_key())
self.assertTrue('foo' in base_config)
self.assertFalse('foo' in inherited_config)
def testDeleteKeys(self):
base_config = config_lib.BuildConfig(foo='bar', baz='bak')
inherited_config_1 = base_config.derive(qzr='flp')
inherited_config_2 = inherited_config_1.derive(
config_lib.BuildConfig.delete_keys(base_config))
self.assertEqual(inherited_config_2, {'qzr': 'flp'})
def testCallableOverrides(self):
append_foo = lambda x: x + 'foo' if x else 'foo'
base_config = config_lib.BuildConfig()
inherited_config_1 = base_config.derive(foo=append_foo)
inherited_config_2 = inherited_config_1.derive(foo=append_foo)
self.assertEqual(inherited_config_1, {'foo': 'foo'})
self.assertEqual(inherited_config_2, {'foo': 'foofoo'})
def AssertDeepCopy(self, obj1, obj2, obj3):
"""Assert that |obj3| is a deep copy of |obj1|.
Args:
obj1: Object that was copied.
obj2: A true deep copy of obj1 (produced using copy.deepcopy).
obj3: The purported deep copy of obj1.
"""
# Check whether the item was copied by deepcopy. If so, then it
# must have been copied by our algorithm as well.
if obj1 is not obj2:
self.assertIsNot(obj1, obj3)
# Assert the three items are all equal.
self.assertEqual(obj1, obj2)
self.assertEqual(obj1, obj3)
if isinstance(obj1, (tuple, list)):
# Copy tuples and lists item by item.
for i in range(len(obj1)):
self.AssertDeepCopy(obj1[i], obj2[i], obj3[i])
elif isinstance(obj1, set):
# Compare sorted versions of the set.
self.AssertDeepCopy(list(sorted(obj1)), list(sorted(obj2)),
list(sorted(obj3)))
elif isinstance(obj1, dict):
# Copy dicts item by item.
for k in obj1:
self.AssertDeepCopy(obj1[k], obj2[k], obj3[k])
elif hasattr(obj1, '__dict__'):
# Make sure the dicts are copied.
self.AssertDeepCopy(obj1.__dict__, obj2.__dict__, obj3.__dict__)
elif hasattr(obj1, '__slots__'):
# Make sure the slots are copied.
for attr in obj1.__slots__:
self.AssertDeepCopy(getattr(obj1, attr), getattr(obj2, attr),
getattr(obj3, attr))
else:
# This should be an object that copy.deepcopy didn't copy (probably an
# immutable object.) If not, the test needs to be updated to handle this
# kind of object.
self.assertIs(obj1, obj2)
def testDeepCopy(self):
"""Test that we deep copy correctly."""
for cfg in [self.fooConfig, self.barConfig, self.deepConfig]:
self.AssertDeepCopy(cfg, copy.deepcopy(cfg), cfg.deepcopy())
def testAssertDeepCopy(self):
"""Test that we test deep copy correctly."""
test1 = ['foo', 'bar', ['hey']]
tests = [test1,
set([tuple(x) for x in test1]),
dict(zip([tuple(x) for x in test1], test1)),
_CustomObject(test1),
_CustomObjectWithSlots(test1)]
for x in tests + [[tests]]:
copy_x = copy.deepcopy(x)
self.AssertDeepCopy(x, copy_x, copy.deepcopy(x))
self.AssertDeepCopy(x, copy_x, cPickle.loads(cPickle.dumps(x, -1)))
self.assertRaises(AssertionError, self.AssertDeepCopy, x,
copy_x, x)
if not isinstance(x, set):
self.assertRaises(AssertionError, self.AssertDeepCopy, x,
copy_x, copy.copy(x))
class SiteParametersClassTest(cros_test_lib.TestCase):
"""SiteParameters tests."""
def testAttributeAccess(self):
"""Test that SiteParameters dot-accessor works correctly."""
site_params = config_lib.SiteParameters()
# Ensure our test key is not in site_params.
self.assertTrue(site_params.get('foo') is None)
# Test that we raise when accessing a non-existent value.
# pylint: disable=pointless-statement
with self.assertRaises(AttributeError):
site_params.foo
# Test the dot-accessor.
site_params.update({'foo': 'bar'})
self.assertEquals('bar', site_params.foo)
class SiteConfigClassTest(cros_test_lib.TestCase):
"""Config tests."""
def testAdd(self):
"""Test the SiteConfig.Add behavior."""
minimal_defaults = {
'name': None, '_template': None, 'value': 'default',
}
site_config = config_lib.SiteConfig(defaults=minimal_defaults)
template = site_config.AddTemplate('template', value='template')
mixin = config_lib.BuildConfig(value='mixin')
site_config.Add('default')
site_config.Add('default_with_override',
value='override')
site_config.Add('default_with_mixin',
mixin)
site_config.Add('mixin_with_override',
mixin,
value='override')
site_config.Add('default_with_template',
template)
site_config.Add('template_with_override',
template,
value='override')
site_config.Add('template_with_mixin',
template,
mixin)
site_config.Add('template_with_mixin_override',
template,
mixin,
value='override')
expected = {
'default': {
'_template': None,
'name': 'default',
'value': 'default',
},
'default_with_override': {
'_template': None,
'name': 'default_with_override',
'value': 'override',
},
'default_with_mixin': {
'_template': None,
'name': 'default_with_mixin',
'value': 'mixin',
},
'mixin_with_override': {
'_template': None,
'name': 'mixin_with_override',
'value': 'override',
},
'default_with_template': {
'_template': 'template',
'name': 'default_with_template',
'value': 'template',
},
'template_with_override': {
'_template': 'template',
'name': 'template_with_override',
'value': 'override'
},
'template_with_mixin': {
'_template': 'template',
'name': 'template_with_mixin',
'value': 'mixin',
},
'template_with_mixin_override': {
'_template': 'template',
'name': 'template_with_mixin_override',
'value': 'override'
},
}
self.maxDiff = None
self.assertDictEqual(site_config, expected)
def testAddErrors(self):
"""Test the SiteConfig.Add behavior."""
site_config = MockSiteConfig()
site_config.Add('foo')
# Test we can't add the
with self.assertRaises(AssertionError):
site_config.Add('foo')
# Create a template without using AddTemplate so the site config doesn't
# know about it.
fake_template = config_lib.BuildConfig(
name='fake_template', _template='fake_template')
with self.assertRaises(AssertionError):
site_config.Add('bar', fake_template)
def testSaveLoadEmpty(self):
config = config_lib.SiteConfig(defaults={}, site_params={})
config_str = config.SaveConfigToString()
loaded = config_lib.LoadConfigFromString(config_str)
self.assertEqual(config, loaded)
self.assertEqual(loaded.keys(), [])
self.assertEqual(loaded._templates.keys(), [])
self.assertEqual(loaded.GetDefault(), config_lib.DefaultSettings())
self.assertEqual(loaded.params,
config_lib.SiteParameters(
config_lib.DefaultSiteParameters()))
self.assertNotEqual(loaded.SaveConfigToString(), '')
# Make sure we can dump debug content without crashing.
self.assertNotEqual(loaded.DumpExpandedConfigToString(), '')
def testSaveLoadComplex(self):
# pylint: disable=line-too-long
src_str = """{
"_default": {
"bar": true,
"baz": false,
"child_configs": [],
"foo": false,
"hw_tests": [],
"nested": { "sub1": 1, "sub2": 2 }
},
"_site_params": {
"site_foo": true,
"site_bar": false
},
"_templates": {
"build": {
"baz": true
}
},
"diff_build": {
"_template": "build",
"bar": false,
"foo": true,
"name": "diff_build"
},
"match_build": {
"name": "match_build"
},
"parent_build": {
"child_configs": [
{
"name": "empty_build"
},
{
"bar": false,
"name": "child_build",
"hw_tests": [
"{\\n \\"async\\": true,\\n \\"blocking\\": false,\\n \\"critical\\": false,\\n \\"file_bugs\\": true,\\n \\"max_retries\\": null,\\n \\"minimum_duts\\": 4,\\n \\"num\\": 2,\\n \\"offload_failures_only\\": false,\\n \\"pool\\": \\"bvt\\",\\n \\"priority\\": \\"PostBuild\\",\\n \\"retry\\": false,\\n \\"suite\\": \\"bvt-perbuild\\",\\n \\"suite_min_duts\\": 1,\\n \\"timeout\\": 13200,\\n \\"warn_only\\": false\\n}"
]
}
],
"name": "parent_build"
},
"default_name_build": {
}
}"""
config = config_lib.LoadConfigFromString(src_str)
expected_defaults = config_lib.DefaultSettings()
expected_defaults.update({
"bar": True,
"baz": False,
"child_configs": [],
"foo": False,
"hw_tests": [],
"nested": {"sub1": 1, "sub2": 2},
})
self.assertEqual(config.GetDefault(), expected_defaults)
# Verify assorted stuff in the loaded config to make sure it matches
# expectations.
self.assertFalse(config['match_build'].foo)
self.assertTrue(config['match_build'].bar)
self.assertFalse(config['match_build'].baz)
self.assertTrue(config['diff_build'].foo)
self.assertFalse(config['diff_build'].bar)
self.assertTrue(config['diff_build'].baz)
self.assertTrue(config['parent_build'].bar)
self.assertTrue(config['parent_build'].child_configs[0].bar)
self.assertFalse(config['parent_build'].child_configs[1].bar)
self.assertEqual(
config['parent_build'].child_configs[1].hw_tests[0],
config_lib.HWTestConfig(
suite='bvt-perbuild',
async=True, file_bugs=True, max_retries=None,
minimum_duts=4, num=2, priority='PostBuild',
retry=False, suite_min_duts=1))
self.assertEqual(config['default_name_build'].name, 'default_name_build')
self.assertTrue(config.params.site_foo)
self.assertFalse(config.params.site_bar)
# Load an save again, just to make sure there are no changes.
loaded = config_lib.LoadConfigFromString(config.SaveConfigToString())
self.assertEqual(config, loaded)
# Make sure we can dump debug content without crashing.
self.assertNotEqual(config.DumpExpandedConfigToString(), '')
def testChromeOsLoad(self):
"""This test compares chromeos_config to config_dump.json."""
# If there is a test failure, the diff will be big.
self.maxDiff = None
src = chromeos_config.GetConfig()
new = config_lib.LoadConfigFromFile()
self.assertDictEqual(src.GetDefault(),
new.GetDefault())
#
# BUG ALERT ON TEST FAILURE
#
# assertDictEqual can correctly compare these structs for equivalence, but
# has a bug when displaying differences on failure. The embedded
# HWTestConfig values are correctly compared, but ALWAYS display as
# different, if something else triggers a failure.
#
# This for loop is to make differences easier to find/read.
for name in src.iterkeys():
self.assertDictEqual(new[name], src[name])
# This confirms they are exactly the same.
self.assertDictEqual(new, src)
class SiteConfigFindTest(cros_test_lib.TestCase):
"""Tests related to Find helpers on SiteConfig."""
def testGetBoardsMockConfig(self):
site_config = MockSiteConfig()
self.assertEqual(
site_config.GetBoards(),
set(['x86-generic']))
def testGetBoardsComplexConfig(self):
site_config = MockSiteConfig()
site_config.AddConfigWithoutTemplate('build_a', boards=['foo_board'])
site_config.AddConfigWithoutTemplate('build_b', boards=['bar_board'])
site_config.AddConfigWithoutTemplate(
'build_c', boards=['foo_board', 'car_board'])
self.assertEqual(
site_config.GetBoards(),
set(['x86-generic', 'foo_board', 'bar_board', 'car_board']))
class FindConfigsForBoardTest(cros_test_lib.TestCase):
"""Test locating of official build for a board."""
def setUp(self):
self.config = chromeos_config.GetConfig()
def _CheckFullConfig(
self, board, external_expected=None, internal_expected=None):
"""Check FindFullConfigsForBoard has expected results.
Args:
board: Argument to pass to FindFullConfigsForBoard.
external_expected: Expected config name (singular) to be found.
internal_expected: Expected config name (singular) to be found.
"""
def check_expected(l, expected):
if expected is not None:
self.assertTrue(expected in [v['name'] for v in l])
external, internal = self.config.FindFullConfigsForBoard(board)
self.assertFalse(external_expected is None and internal_expected is None)
check_expected(external, external_expected)
check_expected(internal, internal_expected)
def _CheckCanonicalConfig(self, board, ending):
self.assertEquals(
'-'.join((board, ending)),
self.config.FindCanonicalConfigForBoard(board)['name'])
def testExternal(self):
"""Test finding of a full builder."""
self._CheckFullConfig(
'amd64-generic', external_expected='amd64-generic-full')
def testInternal(self):
"""Test finding of a release builder."""
self._CheckFullConfig('lumpy', internal_expected='lumpy-release')
def testBoth(self):
"""Both an external and internal config exist for board."""
self._CheckFullConfig(
'daisy', external_expected='daisy-full',
internal_expected='daisy-release')
def testExternalCanonicalResolution(self):
"""Test an external canonical config."""
self._CheckCanonicalConfig('x86-generic', 'full')
def testInternalCanonicalResolution(self):
"""Test prefer internal over external when both exist."""
self._CheckCanonicalConfig('daisy', 'release')
def testAFDOCanonicalResolution(self):
"""Test prefer non-AFDO over AFDO builder."""
self._CheckCanonicalConfig('lumpy', 'release')
def testOneFullConfigPerBoard(self):
"""There is at most one 'full' config for a board."""
# Verifies that there is one external 'full' and one internal 'release'
# build per board. This is to ensure that we fail any new configs that
# wrongly have names like *-bla-release or *-bla-full. This case can also
# be caught if the new suffix was added to
# config_lib.CONFIG_TYPE_DUMP_ORDER
# (see testNonOverlappingConfigTypes), but that's not guaranteed to happen.
def AtMostOneConfig(board, label, configs):
if len(configs) > 1:
self.fail(
'Found more than one %s config for %s: %r'
% (label, board, [c['name'] for c in configs]))
boards = set()
for build_config in self.config.itervalues():
boards.update(build_config['boards'])
# Sanity check of the boards.
self.assertTrue(boards)
for b in boards:
# TODO(akeshet): Figure out why we have both panther_embedded-minimal
# release and panther_embedded-release, and eliminate one of them.
if b == 'panther_embedded':
continue
external, internal = self.config.FindFullConfigsForBoard(b)
AtMostOneConfig(b, 'external', external)
AtMostOneConfig(b, 'internal', internal)
class OverrideForTrybotTest(cros_test_lib.TestCase):
"""Test config override functionality."""
# TODO(dgarrett): Test other override behaviors.
def setUp(self):
self.base_hwtests = [config_lib.HWTestConfig('base')]
self.override_hwtests = [config_lib.HWTestConfig('override')]
self.all_configs = MockSiteConfig()
self.all_configs.Add(
'no_tests_without_override',
vm_tests=[],
)
self.all_configs.Add(
'no_tests_with_override',
vm_tests=[],
vm_tests_override=['o_a', 'o_b'],
hw_tests_override=self.override_hwtests,
)
self.all_configs.Add(
'tests_without_override',
vm_tests=['a', 'b'],
hw_tests=self.base_hwtests,
)
self.all_configs.Add(
'tests_with_override',
vm_tests=['a', 'b'],
vm_tests_override=['o_a', 'o_b'],
hw_tests=self.base_hwtests,
hw_tests_override=self.override_hwtests,
)
def _createMockOptions(self, **kwargs):
mock_options = mock.Mock()
for k, v in kwargs.iteritems():
mock_options.__setattr__(k, v)
return mock_options
def testVmTestOverride(self):
"""Verify that vm_tests override for trybots pay heed to original config."""
mock_options = self._createMockOptions(hwtest=False, remote_trybot=False)
result = config_lib.OverrideConfigForTrybot(
self.all_configs['no_tests_without_override'], mock_options)
self.assertEqual(result.vm_tests, [])
result = config_lib.OverrideConfigForTrybot(
self.all_configs['no_tests_with_override'], mock_options)
self.assertEqual(result.vm_tests, ['o_a', 'o_b'])
result = config_lib.OverrideConfigForTrybot(
self.all_configs['tests_without_override'], mock_options)
self.assertEqual(result.vm_tests, ['a', 'b'])
result = config_lib.OverrideConfigForTrybot(
self.all_configs['tests_with_override'], mock_options)
self.assertEqual(result.vm_tests, ['o_a', 'o_b'])
def testHwTestOverrideDisabled(self):
"""Verify that hw_tests_override is not used without --hwtest."""
mock_options = self._createMockOptions(hwtest=False, remote_trybot=False)
result = config_lib.OverrideConfigForTrybot(
self.all_configs['no_tests_without_override'], mock_options)
self.assertEqual(result.hw_tests, [])
result = config_lib.OverrideConfigForTrybot(
self.all_configs['no_tests_with_override'], mock_options)
self.assertEqual(result.hw_tests, [])
result = config_lib.OverrideConfigForTrybot(
self.all_configs['tests_without_override'], mock_options)
self.assertEqual(result.hw_tests, self.base_hwtests)
result = config_lib.OverrideConfigForTrybot(
self.all_configs['tests_with_override'], mock_options)
self.assertEqual(result.hw_tests, self.base_hwtests)
def testHwTestOverrideEnabled(self):
"""Verify that hw_tests_override is not used without --hwtest."""
mock_options = self._createMockOptions(hwtest=True, remote_trybot=False)
result = config_lib.OverrideConfigForTrybot(
self.all_configs['no_tests_without_override'], mock_options)
self.assertEqual(result.hw_tests, [])
result = config_lib.OverrideConfigForTrybot(
self.all_configs['no_tests_with_override'], mock_options)
self.assertEqual(result.hw_tests, self.override_hwtests)
result = config_lib.OverrideConfigForTrybot(
self.all_configs['tests_without_override'], mock_options)
self.assertEqual(result.hw_tests, self.base_hwtests)
result = config_lib.OverrideConfigForTrybot(
self.all_configs['tests_with_override'], mock_options)
self.assertEqual(result.hw_tests, self.override_hwtests)
class GetConfigTests(cros_test_lib.TestCase):
"""Tests related to SiteConfig.GetConfig()."""
def testGetConfigCaching(self):
"""Test that config_lib.GetConfig() caches it's results correctly."""
config_a = config_lib.GetConfig()
config_b = config_lib.GetConfig()
# Ensure that we get a SiteConfig, and that the result is cached.
self.assertIsInstance(config_a, config_lib.SiteConfig)
self.assertIs(config_a, config_b)
# Clear our cache.
config_lib.ClearConfigCache()
config_c = config_lib.GetConfig()
config_d = config_lib.GetConfig()
# Ensure that this gives us a new instance of the SiteConfig.
self.assertIsNot(config_a, config_c)
# But also that it's cached going forward.
self.assertIsInstance(config_c, config_lib.SiteConfig)
self.assertIs(config_c, config_d)