|  | # Copyright (C) 2017 Intel Corporation | 
|  | # | 
|  | # SPDX-License-Identifier: MIT | 
|  | # | 
|  |  | 
|  | import unittest | 
|  |  | 
|  | from checklayer import LayerType, get_signatures, check_command, get_depgraph | 
|  | from checklayer.case import OECheckLayerTestCase | 
|  |  | 
|  | class BSPCheckLayer(OECheckLayerTestCase): | 
|  | @classmethod | 
|  | def setUpClass(self): | 
|  | if self.tc.layer['type'] not in (LayerType.BSP, LayerType.CORE): | 
|  | raise unittest.SkipTest("BSPCheckLayer: Layer %s isn't BSP one." %\ | 
|  | self.tc.layer['name']) | 
|  |  | 
|  | def test_bsp_defines_machines(self): | 
|  | self.assertTrue(self.tc.layer['conf']['machines'], | 
|  | "Layer is BSP but doesn't defines machines.") | 
|  |  | 
|  | def test_bsp_no_set_machine(self): | 
|  | from oeqa.utils.commands import get_bb_var | 
|  |  | 
|  | machine = get_bb_var('MACHINE') | 
|  | self.assertEqual(self.td['bbvars']['MACHINE'], machine, | 
|  | msg="Layer %s modified machine %s -> %s" % \ | 
|  | (self.tc.layer['name'], self.td['bbvars']['MACHINE'], machine)) | 
|  |  | 
|  |  | 
|  | def test_machine_world(self): | 
|  | ''' | 
|  | "bitbake world" is expected to work regardless which machine is selected. | 
|  | BSP layers sometimes break that by enabling a recipe for a certain machine | 
|  | without checking whether that recipe actually can be built in the current | 
|  | distro configuration (for example, OpenGL might not enabled). | 
|  |  | 
|  | This test iterates over all machines. It would be nicer to instantiate | 
|  | it once per machine. It merely checks for errors during parse | 
|  | time. It does not actually attempt to build anything. | 
|  | ''' | 
|  |  | 
|  | if not self.td['machines']: | 
|  | self.skipTest('No machines set with --machines.') | 
|  | msg = [] | 
|  | for machine in self.td['machines']: | 
|  | # In contrast to test_machine_signatures() below, errors are fatal here. | 
|  | try: | 
|  | get_signatures(self.td['builddir'], failsafe=False, machine=machine) | 
|  | except RuntimeError as ex: | 
|  | msg.append(str(ex)) | 
|  | if msg: | 
|  | msg.insert(0, 'The following machines broke a world build:') | 
|  | self.fail('\n'.join(msg)) | 
|  |  | 
|  | def test_machine_signatures(self): | 
|  | ''' | 
|  | Selecting a machine may only affect the signature of tasks that are specific | 
|  | to that machine. In other words, when MACHINE=A and MACHINE=B share a recipe | 
|  | foo and the output of foo, then both machine configurations must build foo | 
|  | in exactly the same way. Otherwise it is not possible to use both machines | 
|  | in the same distribution. | 
|  |  | 
|  | This criteria can only be tested by testing different machines in combination, | 
|  | i.e. one main layer, potentially several additional BSP layers and an explicit | 
|  | choice of machines: | 
|  | yocto-check-layer --additional-layers .../meta-intel --machines intel-corei7-64 imx6slevk -- .../meta-freescale | 
|  | ''' | 
|  |  | 
|  | if not self.td['machines']: | 
|  | self.skipTest('No machines set with --machines.') | 
|  |  | 
|  | # Collect signatures for all machines that we are testing | 
|  | # and merge that into a hash: | 
|  | # tune -> task -> signature -> list of machines with that combination | 
|  | # | 
|  | # It is an error if any tune/task pair has more than one signature, | 
|  | # because that implies that the machines that caused those different | 
|  | # signatures do not agree on how to execute the task. | 
|  | tunes = {} | 
|  | # Preserve ordering of machines as chosen by the user. | 
|  | for machine in self.td['machines']: | 
|  | curr_sigs, tune2tasks = get_signatures(self.td['builddir'], failsafe=True, machine=machine) | 
|  | # Invert the tune -> [tasks] mapping. | 
|  | tasks2tune = {} | 
|  | for tune, tasks in tune2tasks.items(): | 
|  | for task in tasks: | 
|  | tasks2tune[task] = tune | 
|  | for task, sighash in curr_sigs.items(): | 
|  | tunes.setdefault(tasks2tune[task], {}).setdefault(task, {}).setdefault(sighash, []).append(machine) | 
|  |  | 
|  | msg = [] | 
|  | pruned = 0 | 
|  | last_line_key = None | 
|  | # do_fetch, do_unpack, ..., do_build | 
|  | taskname_list = [] | 
|  | if tunes: | 
|  | # The output below is most useful when we start with tasks that are at | 
|  | # the bottom of the dependency chain, i.e. those that run first. If | 
|  | # those tasks differ, the rest also does. | 
|  | # | 
|  | # To get an ordering of tasks, we do a topological sort of the entire | 
|  | # depgraph for the base configuration, then on-the-fly flatten that list by stripping | 
|  | # out the recipe names and removing duplicates. The base configuration | 
|  | # is not necessarily representative, but should be close enough. Tasks | 
|  | # that were not encountered get a default priority. | 
|  | depgraph = get_depgraph() | 
|  | depends = depgraph['tdepends'] | 
|  | WHITE = 1 | 
|  | GRAY = 2 | 
|  | BLACK = 3 | 
|  | color = {} | 
|  | found = set() | 
|  | def visit(task): | 
|  | color[task] = GRAY | 
|  | for dep in depends.get(task, ()): | 
|  | if color.setdefault(dep, WHITE) == WHITE: | 
|  | visit(dep) | 
|  | color[task] = BLACK | 
|  | pn, taskname = task.rsplit('.', 1) | 
|  | if taskname not in found: | 
|  | taskname_list.append(taskname) | 
|  | found.add(taskname) | 
|  | for task in depends.keys(): | 
|  | if color.setdefault(task, WHITE) == WHITE: | 
|  | visit(task) | 
|  |  | 
|  | taskname_order = dict([(task, index) for index, task in enumerate(taskname_list) ]) | 
|  | def task_key(task): | 
|  | pn, taskname = task.rsplit(':', 1) | 
|  | return (pn, taskname_order.get(taskname, len(taskname_list)), taskname) | 
|  |  | 
|  | for tune in sorted(tunes.keys()): | 
|  | tasks = tunes[tune] | 
|  | # As for test_signatures it would be nicer to sort tasks | 
|  | # by dependencies here, but that is harder because we have | 
|  | # to report on tasks from different machines, which might | 
|  | # have different dependencies. We resort to pruning the | 
|  | # output by reporting only one task per recipe if the set | 
|  | # of machines matches. | 
|  | # | 
|  | # "bitbake-diffsigs -t -s" is intelligent enough to print | 
|  | # diffs recursively, so often it does not matter that much | 
|  | # if we don't pick the underlying difference | 
|  | # here. However, sometimes recursion fails | 
|  | # (https://bugzilla.yoctoproject.org/show_bug.cgi?id=6428). | 
|  | # | 
|  | # To mitigate that a bit, we use a hard-coded ordering of | 
|  | # tasks that represents how they normally run and prefer | 
|  | # to print the ones that run first. | 
|  | for task in sorted(tasks.keys(), key=task_key): | 
|  | signatures = tasks[task] | 
|  | # do_build can be ignored: it is know to have | 
|  | # different signatures in some cases, for example in | 
|  | # the allarch ca-certificates due to RDEPENDS=openssl. | 
|  | # That particular dependency is marked via | 
|  | # SIGGEN_EXCLUDE_SAFE_RECIPE_DEPS, but still shows up | 
|  | # in the sstate signature hash because filtering it | 
|  | # out would be hard and running do_build multiple | 
|  | # times doesn't really matter. | 
|  | if len(signatures.keys()) > 1 and \ | 
|  | not task.endswith(':do_build'): | 
|  | # Error! | 
|  | # | 
|  | # Sort signatures by machines, because the hex values don't mean anything. | 
|  | # => all-arch adwaita-icon-theme:do_build: 1234... (beaglebone, qemux86) != abcdf... (qemux86-64) | 
|  | # | 
|  | # Skip the line if it is covered already by the predecessor (same pn, same sets of machines). | 
|  | pn, taskname = task.rsplit(':', 1) | 
|  | next_line_key = (pn, sorted(signatures.values())) | 
|  | if next_line_key != last_line_key: | 
|  | line = '   %s %s: ' % (tune, task) | 
|  | line += ' != '.join(['%s (%s)' % (signature, ', '.join([m for m in signatures[signature]])) for | 
|  | signature in sorted(signatures.keys(), key=lambda s: signatures[s])]) | 
|  | last_line_key = next_line_key | 
|  | msg.append(line) | 
|  | # Randomly pick two mismatched signatures and remember how to invoke | 
|  | # bitbake-diffsigs for them. | 
|  | iterator = iter(signatures.items()) | 
|  | a = next(iterator) | 
|  | b = next(iterator) | 
|  | diffsig_machines = '(%s) != (%s)' % (', '.join(a[1]), ', '.join(b[1])) | 
|  | diffsig_params = '-t %s %s -s %s %s' % (pn, taskname, a[0], b[0]) | 
|  | else: | 
|  | pruned += 1 | 
|  |  | 
|  | if msg: | 
|  | msg.insert(0, 'The machines have conflicting signatures for some shared tasks:') | 
|  | if pruned > 0: | 
|  | msg.append('') | 
|  | msg.append('%d tasks where not listed because some other task of the recipe already differed.' % pruned) | 
|  | msg.append('It is likely that differences from different recipes also have the same root cause.') | 
|  | msg.append('') | 
|  | # Explain how to investigate... | 
|  | msg.append('To investigate, run bitbake-diffsigs -t recipename taskname -s fromsig tosig.') | 
|  | cmd = 'bitbake-diffsigs %s' % diffsig_params | 
|  | msg.append('Example: %s in the last line' % diffsig_machines) | 
|  | msg.append('Command: %s' % cmd) | 
|  | # ... and actually do it automatically for that example, but without aborting | 
|  | # when that fails. | 
|  | try: | 
|  | output = check_command('Comparing signatures failed.', cmd).decode('utf-8') | 
|  | except RuntimeError as ex: | 
|  | output = str(ex) | 
|  | msg.extend(['   ' + line for line in output.splitlines()]) | 
|  | self.fail('\n'.join(msg)) |