blob: b6b611be731443558db0ab65f5ee608255c408fd [file] [log] [blame]
Brad Bishopd7bf8c12018-02-25 22:55:05 -05001# Copyright (C) 2017 Intel Corporation
2# Released under the MIT license (see COPYING.MIT)
3
4import unittest
5
6from checklayer import LayerType, get_signatures, check_command, get_depgraph
7from checklayer.case import OECheckLayerTestCase
8
9class BSPCheckLayer(OECheckLayerTestCase):
10 @classmethod
11 def setUpClass(self):
12 if self.tc.layer['type'] != LayerType.BSP:
13 raise unittest.SkipTest("BSPCheckLayer: Layer %s isn't BSP one." %\
14 self.tc.layer['name'])
15
16 def test_bsp_defines_machines(self):
17 self.assertTrue(self.tc.layer['conf']['machines'],
18 "Layer is BSP but doesn't defines machines.")
19
20 def test_bsp_no_set_machine(self):
21 from oeqa.utils.commands import get_bb_var
22
23 machine = get_bb_var('MACHINE')
24 self.assertEqual(self.td['bbvars']['MACHINE'], machine,
25 msg="Layer %s modified machine %s -> %s" % \
26 (self.tc.layer['name'], self.td['bbvars']['MACHINE'], machine))
27
28
29 def test_machine_world(self):
30 '''
31 "bitbake world" is expected to work regardless which machine is selected.
32 BSP layers sometimes break that by enabling a recipe for a certain machine
33 without checking whether that recipe actually can be built in the current
34 distro configuration (for example, OpenGL might not enabled).
35
36 This test iterates over all machines. It would be nicer to instantiate
37 it once per machine. It merely checks for errors during parse
38 time. It does not actually attempt to build anything.
39 '''
40
41 if not self.td['machines']:
42 self.skipTest('No machines set with --machines.')
43 msg = []
44 for machine in self.td['machines']:
45 # In contrast to test_machine_signatures() below, errors are fatal here.
46 try:
47 get_signatures(self.td['builddir'], failsafe=False, machine=machine)
48 except RuntimeError as ex:
49 msg.append(str(ex))
50 if msg:
51 msg.insert(0, 'The following machines broke a world build:')
52 self.fail('\n'.join(msg))
53
54 def test_machine_signatures(self):
55 '''
56 Selecting a machine may only affect the signature of tasks that are specific
57 to that machine. In other words, when MACHINE=A and MACHINE=B share a recipe
58 foo and the output of foo, then both machine configurations must build foo
59 in exactly the same way. Otherwise it is not possible to use both machines
60 in the same distribution.
61
62 This criteria can only be tested by testing different machines in combination,
63 i.e. one main layer, potentially several additional BSP layers and an explicit
64 choice of machines:
65 yocto-check-layer --additional-layers .../meta-intel --machines intel-corei7-64 imx6slevk -- .../meta-freescale
66 '''
67
68 if not self.td['machines']:
69 self.skipTest('No machines set with --machines.')
70
71 # Collect signatures for all machines that we are testing
72 # and merge that into a hash:
73 # tune -> task -> signature -> list of machines with that combination
74 #
75 # It is an error if any tune/task pair has more than one signature,
76 # because that implies that the machines that caused those different
77 # signatures do not agree on how to execute the task.
78 tunes = {}
79 # Preserve ordering of machines as chosen by the user.
80 for machine in self.td['machines']:
81 curr_sigs, tune2tasks = get_signatures(self.td['builddir'], failsafe=True, machine=machine)
82 # Invert the tune -> [tasks] mapping.
83 tasks2tune = {}
84 for tune, tasks in tune2tasks.items():
85 for task in tasks:
86 tasks2tune[task] = tune
87 for task, sighash in curr_sigs.items():
88 tunes.setdefault(tasks2tune[task], {}).setdefault(task, {}).setdefault(sighash, []).append(machine)
89
90 msg = []
91 pruned = 0
92 last_line_key = None
93 # do_fetch, do_unpack, ..., do_build
94 taskname_list = []
95 if tunes:
96 # The output below is most useful when we start with tasks that are at
97 # the bottom of the dependency chain, i.e. those that run first. If
98 # those tasks differ, the rest also does.
99 #
100 # To get an ordering of tasks, we do a topological sort of the entire
101 # depgraph for the base configuration, then on-the-fly flatten that list by stripping
102 # out the recipe names and removing duplicates. The base configuration
103 # is not necessarily representative, but should be close enough. Tasks
104 # that were not encountered get a default priority.
105 depgraph = get_depgraph()
106 depends = depgraph['tdepends']
107 WHITE = 1
108 GRAY = 2
109 BLACK = 3
110 color = {}
111 found = set()
112 def visit(task):
113 color[task] = GRAY
114 for dep in depends.get(task, ()):
115 if color.setdefault(dep, WHITE) == WHITE:
116 visit(dep)
117 color[task] = BLACK
118 pn, taskname = task.rsplit('.', 1)
119 if taskname not in found:
120 taskname_list.append(taskname)
121 found.add(taskname)
122 for task in depends.keys():
123 if color.setdefault(task, WHITE) == WHITE:
124 visit(task)
125
126 taskname_order = dict([(task, index) for index, task in enumerate(taskname_list) ])
127 def task_key(task):
128 pn, taskname = task.rsplit(':', 1)
129 return (pn, taskname_order.get(taskname, len(taskname_list)), taskname)
130
131 for tune in sorted(tunes.keys()):
132 tasks = tunes[tune]
133 # As for test_signatures it would be nicer to sort tasks
134 # by dependencies here, but that is harder because we have
135 # to report on tasks from different machines, which might
136 # have different dependencies. We resort to pruning the
137 # output by reporting only one task per recipe if the set
138 # of machines matches.
139 #
140 # "bitbake-diffsigs -t -s" is intelligent enough to print
141 # diffs recursively, so often it does not matter that much
142 # if we don't pick the underlying difference
143 # here. However, sometimes recursion fails
144 # (https://bugzilla.yoctoproject.org/show_bug.cgi?id=6428).
145 #
146 # To mitigate that a bit, we use a hard-coded ordering of
147 # tasks that represents how they normally run and prefer
148 # to print the ones that run first.
149 for task in sorted(tasks.keys(), key=task_key):
150 signatures = tasks[task]
151 # do_build can be ignored: it is know to have
152 # different signatures in some cases, for example in
153 # the allarch ca-certificates due to RDEPENDS=openssl.
154 # That particular dependency is whitelisted via
155 # SIGGEN_EXCLUDE_SAFE_RECIPE_DEPS, but still shows up
156 # in the sstate signature hash because filtering it
157 # out would be hard and running do_build multiple
158 # times doesn't really matter.
159 if len(signatures.keys()) > 1 and \
160 not task.endswith(':do_build'):
161 # Error!
162 #
163 # Sort signatures by machines, because the hex values don't mean anything.
164 # => all-arch adwaita-icon-theme:do_build: 1234... (beaglebone, qemux86) != abcdf... (qemux86-64)
165 #
166 # Skip the line if it is covered already by the predecessor (same pn, same sets of machines).
167 pn, taskname = task.rsplit(':', 1)
168 next_line_key = (pn, sorted(signatures.values()))
169 if next_line_key != last_line_key:
170 line = ' %s %s: ' % (tune, task)
171 line += ' != '.join(['%s (%s)' % (signature, ', '.join([m for m in signatures[signature]])) for
172 signature in sorted(signatures.keys(), key=lambda s: signatures[s])])
173 last_line_key = next_line_key
174 msg.append(line)
175 # Randomly pick two mismatched signatures and remember how to invoke
176 # bitbake-diffsigs for them.
177 iterator = iter(signatures.items())
178 a = next(iterator)
179 b = next(iterator)
180 diffsig_machines = '(%s) != (%s)' % (', '.join(a[1]), ', '.join(b[1]))
181 diffsig_params = '-t %s %s -s %s %s' % (pn, taskname, a[0], b[0])
182 else:
183 pruned += 1
184
185 if msg:
186 msg.insert(0, 'The machines have conflicting signatures for some shared tasks:')
187 if pruned > 0:
188 msg.append('')
189 msg.append('%d tasks where not listed because some other task of the recipe already differed.' % pruned)
190 msg.append('It is likely that differences from different recipes also have the same root cause.')
191 msg.append('')
192 # Explain how to investigate...
193 msg.append('To investigate, run bitbake-diffsigs -t recipename taskname -s fromsig tosig.')
194 cmd = 'bitbake-diffsigs %s' % diffsig_params
195 msg.append('Example: %s in the last line' % diffsig_machines)
196 msg.append('Command: %s' % cmd)
197 # ... and actually do it automatically for that example, but without aborting
198 # when that fails.
199 try:
200 output = check_command('Comparing signatures failed.', cmd).decode('utf-8')
201 except RuntimeError as ex:
202 output = str(ex)
203 msg.extend([' ' + line for line in output.splitlines()])
204 self.fail('\n'.join(msg))