| # -*- coding: utf-8 -*- |
| # 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. |
| |
| """Tests for verifying prebuilts.""" |
| |
| from __future__ import print_function |
| |
| import inspect |
| import os |
| import unittest |
| |
| from chromite.cbuildbot import binhost |
| from chromite.lib import config_lib |
| from chromite.lib import constants |
| from chromite.lib import cros_build_lib |
| from chromite.lib import cros_logging as logging |
| from chromite.lib import cros_test_lib |
| from chromite.lib import osutils |
| from chromite.lib import parallel |
| |
| |
| class PrebuiltCompatibilityTest(cros_test_lib.TestCase): |
| """Ensure that prebuilts are present for all builders and are compatible.""" |
| |
| # Whether to cache setup from run to run. If set, requires that you install |
| # joblib (sudo easy_install joblib). This is useful for iterating on the |
| # unit tests, but note that if you 'repo sync', you'll need to clear out |
| # /tmp/joblib and blow away /build in order to update the caches. Note that |
| # this is never normally set to True -- if you want to use this feature, |
| # you'll need to hand-edit this file. |
| # TODO(davidjames): Add a --caching option. |
| CACHING = False |
| |
| # A dict mapping BoardKeys to their associated compat ids. |
| COMPAT_IDS = None |
| |
| # Boards that don't have Chromium PFQs. |
| # TODO(davidjames): Empty this list. |
| BOARDS_WITHOUT_CHROMIUM_PFQS = ['veyron_rialto'] |
| |
| site_config = config_lib.GetConfig() |
| |
| @classmethod |
| def setUpClass(cls): |
| assert cros_build_lib.IsInsideChroot() |
| logging.info('Generating board configs.') |
| board_keys = binhost.GetAllImportantBoardKeys(cls.site_config) |
| boards = set(key.board for key in board_keys) |
| inputs = [[board, not cls.CACHING, False] for board in boards] |
| parallel.RunTasksInProcessPool(binhost.GenConfigsForBoard, inputs) |
| fetcher = binhost.CompatIdFetcher(caching=cls.CACHING) |
| cls.COMPAT_IDS = fetcher.FetchCompatIds(list(board_keys)) |
| logging.info('Running tests...') |
| |
| def setUp(self): |
| self.complaints = [] |
| self.fatal_complaints = [] |
| |
| def tearDown(self): |
| for complaint in self.complaints: |
| logging.warn(complaint) |
| for complaint in self.fatal_complaints: |
| logging.error(complaint) |
| if self.fatal_complaints: |
| self.fail('Fatal errors found in this test') |
| |
| def Complain(self, msg, fatal): |
| """Complain about an error when the test exits. |
| |
| Args: |
| msg: The message to print. |
| fatal: Whether the message should be fatal. If not, the message will be |
| considered a warning. |
| """ |
| if fatal: |
| self.fatal_complaints.append(msg) |
| else: |
| self.complaints.append(msg) |
| |
| def GetCompatIdDiff(self, expected, actual): |
| """Return a string describing the differences between expected and actual. |
| |
| Args: |
| expected: Expected value for CompatId. |
| actual: Actual value for CompatId. |
| """ |
| if expected.arch != actual.arch: |
| return 'arch differs: %s != %s' % (expected.arch, actual.arch) |
| elif expected.useflags != actual.useflags: |
| msg = self.GetSequenceDiff(expected.useflags, actual.useflags) |
| return msg.replace('Sequences', 'useflags') |
| elif expected.cflags != actual.cflags: |
| msg = self.GetSequenceDiff(expected.cflags, actual.cflags) |
| return msg.replace('Sequences', 'cflags') |
| else: |
| assert expected == actual |
| return 'no differences' |
| |
| def _FindCloseConfigs(self, pfq_configs, config, skipped_useflags): |
| """Find configs in |pfq_configs| that are "close" to |config|. |
| |
| If there are no prebuilts from any PFQ that match this board, then try to |
| help with diagnostics by finding the "closest" matches. Since most failures |
| are related to mismatched USE flags, we ignore the compiler settings and |
| find configs that have the fewest USE flag changes. |
| |
| Args: |
| pfq_configs: A PrebuiltMapping object. |
| config: The baseline config to compare against. |
| skipped_useflags: set of USE flags to ignore when computing changes. |
| |
| Returns: |
| A list of (BoardKey, CompatId, added-USE-flags, removed-USE-flags) sorted |
| by the number of changed USE flags. |
| """ |
| compat_id = self.GetCompatId(config) |
| |
| ret = [] |
| for close_id, board_key in pfq_configs.by_compat_id.items(): |
| # Only consider matching architectures. |
| if compat_id.arch == close_id.arch: |
| added = (set(close_id.useflags) - set(compat_id.useflags) - |
| skipped_useflags) |
| removed = (set(compat_id.useflags) - set(close_id.useflags) - |
| skipped_useflags) |
| if added or removed: |
| ret.append((board_key, compat_id, |
| sorted('+%s' % x for x in added), |
| sorted('-%s' % x for x in removed))) |
| |
| # Do the final sort of the configs based on number of USE changes. |
| return sorted(ret, key=lambda x: len(x[2]) + len(x[3])) |
| |
| def AssertChromePrebuilts(self, pfq_configs, config, skip_useflags=False): |
| """Verify that the specified config has Chrome prebuilts. |
| |
| Args: |
| pfq_configs: A PrebuiltMapping object. |
| config: The config to check. |
| skip_useflags: Don't use extra useflags from the config. |
| """ |
| # Skip over useflags from the useflag if needed. |
| msg_prefix = '' |
| skipped_useflags = set() |
| if skip_useflags and config.useflags: |
| skipped_useflags = set(config.useflags) |
| msg_prefix = ('When we take out config-requested useflags %s for ' |
| 'public/partner builds, ' |
| % (tuple(x.encode('ascii') for x in config.useflags),)) |
| config = config.deepcopy() |
| config.useflags = [] |
| |
| compat_id = self.GetCompatId(config) |
| pfqs = pfq_configs.by_compat_id.get(compat_id, set()) |
| if not pfqs: |
| arch_useflags = (compat_id.arch, compat_id.useflags) |
| for key in pfq_configs.by_arch_useflags[arch_useflags]: |
| # If there wasn't an exact match for this CompatId, but there |
| # was an (arch, useflags) match, then we'll be using mismatched |
| # Chrome prebuilts. Complain. |
| # TODO(davidjames): This should be a fatal error for important |
| # builders, but we need to clean up existing cases first. |
| pfq_compat_id = self.COMPAT_IDS.get(key) |
| if pfq_compat_id: |
| err = self.GetCompatIdDiff(compat_id, pfq_compat_id) |
| msg = '%s%s uses mismatched Chrome prebuilts from %s\n\t%s' |
| self.Complain(msg % (msg_prefix, config.name, key.board, err), |
| fatal=False) |
| pfqs.add(key) |
| |
| if not pfqs: |
| pre_cq = (config.build_type == config_lib.CONFIG_TYPE_PRECQ) |
| msg = ('%s%s cannot find Chrome prebuilts (probably due to USE flag ' |
| 'mismatch)\nBuild settings: %s' |
| % (msg_prefix, config.name, compat_id)) |
| |
| # For brevity, we only show the first three closest matches. After that, |
| # we start getting redundant, and the deltas get larger. This is just a |
| # debug display, so it need not be perfect. |
| close_configs = self._FindCloseConfigs(pfq_configs, config, |
| skipped_useflags) |
| if close_configs: |
| msg += '\nClosest matching configs:\n' |
| for board_key, compat_id, added, removed in close_configs[0:3]: |
| msg += ('\tBoards: %s\n\t\tUSE changes: %s\n\t\tBuild settings: %s\n' |
| % (board_key, added + removed, compat_id)) |
| |
| self.Complain(msg, fatal=pre_cq or config.important) |
| |
| def GetCompatId(self, config, board=None): |
| """Get the CompatId for a config. |
| |
| Args: |
| config: A config_lib.BuildConfig object. |
| board: Board to use. Defaults to the first board in the config. |
| Optional if len(config.boards) == 1. |
| """ |
| if board is None: |
| assert len(config.boards) == 1 |
| board = config.boards[0] |
| else: |
| assert board in config.boards |
| |
| board_key = binhost.GetBoardKey(config, board) |
| compat_id = self.COMPAT_IDS.get(board_key) |
| if compat_id is None: |
| compat_id = binhost.CalculateCompatId(board, config.useflags) |
| self.COMPAT_IDS[board_key] = compat_id |
| return compat_id |
| |
| def _GuessActiveConfigs(self): |
| """Guess at which build configs are artively used. |
| |
| LUCI Scheduler's config is the source of truth, but that's |
| not available here, so take a guess at "good enough". |
| See crbug.com/831929 |
| |
| Returns: |
| A map of build configs. { name: config } |
| """ |
| result = {} |
| for name, config in self.site_config.items(): |
| if config.master and config.important: |
| result[name] = config |
| for s in config.slave_configs: |
| result[s] = self.site_config[s] |
| |
| return result |
| |
| def testChromePrebuiltsPresent(self, filename=None): |
| """Verify all builds that use Chrome have matching Chrome PFQ configs. |
| |
| Args: |
| filename: Filename to load our PFQ mappings from. By default, generate |
| the PFQ mappings based on the current config. |
| """ |
| if filename is not None: |
| logging.info('Checking PFQ database: %s', filename) |
| pfq_configs = binhost.PrebuiltMapping.Load(filename) |
| else: |
| logging.info('Checking config_lib.GetConfig().site_config') |
| keys = binhost.GetChromePrebuiltConfigs(self.site_config).keys() |
| pfq_configs = binhost.PrebuiltMapping.Get(keys, self.COMPAT_IDS) |
| |
| for compat_id, pfqs in sorted(pfq_configs.by_compat_id.items(), |
| key=lambda x: str(x[1])): |
| if len(pfqs) > 1: |
| self.Complain( |
| 'The following Chrome PFQs produce identical prebuilts:\n' |
| '\t%s\n\t%s' |
| % ('\n\t'.join(sorted(str(x) for x in pfqs)), compat_id), |
| fatal=False) |
| |
| # Sort the names to ensure consistent errors. |
| for _name, config in sorted(self._GuessActiveConfigs().items()): |
| |
| # Skip over configs that don't have Chrome or have >1 board. |
| if config.sync_chrome is False or len(config.boards) != 1: |
| continue |
| |
| # Look for boards with missing prebuilts. |
| if config.usepkg_build_packages and not config.chrome_rev: |
| self.AssertChromePrebuilts(pfq_configs, config) |
| |
| # Check that we have a builder for the version w/o custom useflags as |
| # well. |
| if (config.useflags and |
| config.boards[0] not in self.BOARDS_WITHOUT_CHROMIUM_PFQS): |
| self.AssertChromePrebuilts(pfq_configs, config, skip_useflags=True) |
| |
| def testCurrentChromePrebuiltsEnough(self): |
| """Verify Chrome prebuilts actually exist for all configs that build Chrome. |
| |
| This loads the list of Chrome prebuilts that were generated during the last |
| Chrome PFQ run from disk and verifies that it is sufficient. |
| """ |
| filename = binhost.PrebuiltMapping.GetFilename(constants.SOURCE_ROOT, |
| 'chrome') |
| if os.path.exists(filename): |
| self.testChromePrebuiltsPresent(filename) |
| |
| def testDumping(self): |
| """Verify Chrome prebuilts exist for all configs that build Chrome. |
| |
| This loads the list of Chrome prebuilts that were generated during the last |
| Chrome PFQ run from disk and verifies that it is sufficient. |
| """ |
| with osutils.TempDir() as tempdir: |
| keys = binhost.GetChromePrebuiltConfigs(self.site_config).keys() |
| pfq_configs = binhost.PrebuiltMapping.Get(keys, self.COMPAT_IDS) |
| filename = os.path.join(tempdir, 'foo.json') |
| pfq_configs.Dump(filename) |
| self.assertEqual(pfq_configs, binhost.PrebuiltMapping.Load(filename)) |
| |
| |
| def NoIncremental(): |
| """Creates a suite containing only non-incremental tests. |
| |
| This suite should be used on the Chrome PFQ as we don't need to preserve |
| incremental compatibility of prebuilts. |
| |
| Returns: |
| A unittest.TestSuite that does not contain any incremental tests. |
| """ |
| suite = unittest.TestSuite() |
| method_names = [f[0] for f in inspect.getmembers(PrebuiltCompatibilityTest, |
| predicate=inspect.ismethod)] |
| for m in method_names: |
| if m.startswith('test') and m != 'testCurrentChromePrebuiltsEnough': |
| suite.addTest(PrebuiltCompatibilityTest(m)) |
| return suite |