blob: 5e115df199296efa39818f759da11cbfe1aefd80 [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
from chromite.cbuildbot import cbuildbot_config
from chromite.cbuildbot import config_lib
from chromite.lib import cros_test_lib
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."""
return config_lib.CreateConfigFromString(
'''
{
"_default": {
"active_waterfall": null,
"afdo_generate": false,
"afdo_generate_min": false,
"afdo_update_ebuild": false,
"afdo_use": false,
"archive": true,
"archive_build_debug": false,
"binhost_base_url": null,
"binhost_bucket": null,
"binhost_key": null,
"binhost_test": false,
"board_replace": false,
"boards": null,
"branch": false,
"build_before_patching": false,
"build_packages_in_background": false,
"build_tests": true,
"build_type": "pfq",
"buildbot_waterfall_name": null,
"builder_class_name": null,
"child_configs": [],
"chrome_binhost_only": false,
"chrome_rev": null,
"chrome_sdk": false,
"chrome_sdk_build_chrome": true,
"chrome_sdk_goma": false,
"chromeos_official": false,
"chroot_replace": false,
"compilecheck": false,
"cpe_export": true,
"create_delta_sysroot": false,
"critical_for_chrome": false,
"debug_symbols": true,
"description": null,
"dev_installer_prebuilts": false,
"dev_manifest": "default.xml",
"disk_layout": null,
"do_not_apply_cq_patches": false,
"doc": null,
"factory": true,
"factory_install_netboot": true,
"factory_toolkit": true,
"gcc_githash": null,
"git_sync": false,
"grouped": false,
"gs_path": "default",
"health_alert_recipients": [],
"health_threshold": 0,
"hw_tests": [],
"hwqual": false,
"image_test": false,
"images": [
"test"
],
"important": false,
"internal": false,
"latest_toolchain": false,
"lkgm_manifest": "LKGM/lkgm.xml",
"manifest": "default.xml",
"manifest_repo_url": "mock_manifest_repo_url",
"manifest_version": false,
"master": false,
"name": null,
"overlays": "public",
"packages": [],
"paygen": false,
"paygen_skip_delta_payloads": false,
"paygen_skip_testing": false,
"payload_image": null,
"postsync_patch": true,
"postsync_reexec": true,
"pre_cq": false,
"prebuilts": false,
"profile": null,
"push_image": false,
"push_overlays": null,
"quick_unit": false,
"rootfs_verification": true,
"sanity_check_slaves": null,
"separate_debug_symbols": true,
"shared_user_password": null,
"signer_tests": false,
"sync_chrome": null,
"tests_supported": true,
"trybot_list": false,
"unittest_blacklist": [],
"unittests": true,
"upload_hw_test_artifacts": true,
"upload_standalone_images": true,
"upload_symbols": false,
"uprev": true,
"use_chrome_lkgm": false,
"use_lkgm": false,
"use_sdk": true,
"useflags": [],
"usepkg_build_packages": true,
"usepkg_toolchain": true,
"vm_test_runs": 1,
"vm_tests": [
"smoke_suite",
"pfq_suite"
]
},
"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,
"name": "x86-generic-paladin",
"prebuilts": "public",
"trybot_list": true,
"upload_standalone_images": false,
"vm_tests": [
"smoke_suite"
]
}
}
''')
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')
# pylint: disable=protected-access
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 ConfigClassTest(cros_test_lib.TestCase):
"""Config tests."""
def testSaveLoadEmpty(self):
config = config_lib.SiteConfig()
config_str = config.SaveConfigToString()
self.assertEqual(config_str, """{
"_default": {}
}""")
loaded = config_lib.CreateConfigFromString(config_str)
loaded_str = loaded.SaveConfigToString()
self.assertEqual(config, loaded)
self.assertEqual(config_str, loaded_str)
def testSaveLoadComplex(self):
# pylint: disable=line-too-long
src_str = """{
"_default": {
"bar": true,
"child_configs": [],
"foo": false,
"hw_tests": []
},
"diff_build": {
"bar": false,
"foo": true
},
"match_build": {},
"parent_build": {
"child_configs": [
{},
{
"bar": false,
"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 \\"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}"
]
}
]
}
}"""
config = config_lib.CreateConfigFromString(src_str)
config_str = config.SaveConfigToString()
# Verify that the dumped string matches the source string.
self.assertEqual(src_str, config_str)
# 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.assertTrue(config['diff_build'].foo)
self.assertFalse(config['diff_build'].bar)
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))
# Load an save again, just to make sure there are no changes.
loaded = config_lib.CreateConfigFromString(config_str)
loaded_str = loaded.SaveConfigToString()
self.assertEqual(config, loaded)
self.assertEqual(config_str, loaded_str)
class FindConfigsForBoardTest(cros_test_lib.TestCase):
"""Test locating of official build for a board."""
def setUp(self):
self.config = cbuildbot_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)