1#
2# Copyright OpenEmbedded Contributors
3#
4# SPDX-License-Identifier: MIT
5#
6
7import os
8import re
9import time
10import logging
11import bb.tinfoil
12
13from oeqa.selftest.case import OESelftestTestCase
14
15class TinfoilTests(OESelftestTestCase):
16    """ Basic tests for the tinfoil API """
17
18    def test_getvar(self):
19        with bb.tinfoil.Tinfoil() as tinfoil:
20            tinfoil.prepare(True)
21            machine = tinfoil.config_data.getVar('MACHINE')
22            if not machine:
23                self.fail('Unable to get MACHINE value - returned %s' % machine)
24
25    def test_expand(self):
26        with bb.tinfoil.Tinfoil() as tinfoil:
27            tinfoil.prepare(True)
28            expr = '${@os.getpid()}'
29            pid = tinfoil.config_data.expand(expr)
30            if not pid:
31                self.fail('Unable to expand "%s" - returned %s' % (expr, pid))
32
33    def test_getvar_bb_origenv(self):
34        with bb.tinfoil.Tinfoil() as tinfoil:
35            tinfoil.prepare(True)
36            origenv = tinfoil.config_data.getVar('BB_ORIGENV', False)
37            if not origenv:
38                self.fail('Unable to get BB_ORIGENV value - returned %s' % origenv)
39            self.assertEqual(origenv.getVar('HOME', False), os.environ['HOME'])
40
41    def test_parse_recipe(self):
42        with bb.tinfoil.Tinfoil() as tinfoil:
43            tinfoil.prepare(config_only=False, quiet=2)
44            testrecipe = 'mdadm'
45            best = tinfoil.find_best_provider(testrecipe)
46            if not best:
47                self.fail('Unable to find recipe providing %s' % testrecipe)
48            rd = tinfoil.parse_recipe_file(best[3])
49            self.assertEqual(testrecipe, rd.getVar('PN'))
50
51    def test_parse_virtual_recipe(self):
52        with bb.tinfoil.Tinfoil() as tinfoil:
53            tinfoil.prepare(config_only=False, quiet=2)
54            testrecipe = 'nativesdk-gcc'
55            best = tinfoil.find_best_provider(testrecipe)
56            if not best:
57                self.fail('Unable to find recipe providing %s' % testrecipe)
58            rd = tinfoil.parse_recipe_file(best[3])
59            self.assertEqual(testrecipe, rd.getVar('PN'))
60            self.assertIsNotNone(rd.getVar('FILE_LAYERNAME'))
61
62    def test_parse_recipe_copy_expand(self):
63        with bb.tinfoil.Tinfoil() as tinfoil:
64            tinfoil.prepare(config_only=False, quiet=2)
65            testrecipe = 'mdadm'
66            best = tinfoil.find_best_provider(testrecipe)
67            if not best:
68                self.fail('Unable to find recipe providing %s' % testrecipe)
69            rd = tinfoil.parse_recipe_file(best[3])
70            # Check we can get variable values
71            self.assertEqual(testrecipe, rd.getVar('PN'))
72            # Check that expanding a value that includes a variable reference works
73            self.assertEqual(testrecipe, rd.getVar('BPN'))
74            # Now check that changing the referenced variable's value in a copy gives that
75            # value when expanding
76            localdata = bb.data.createCopy(rd)
77            localdata.setVar('PN', 'hello')
78            self.assertEqual('hello', localdata.getVar('BPN'))
79
80    # The config_data API to parse_recipe_file is used by:
81    # layerindex-web layerindex/update_layer.py
82    def test_parse_recipe_custom_data(self):
83        with bb.tinfoil.Tinfoil() as tinfoil:
84            tinfoil.prepare(config_only=False, quiet=2)
85            localdata = bb.data.createCopy(tinfoil.config_data)
86            localdata.setVar("TESTVAR", "testval")
87            testrecipe = 'mdadm'
88            best = tinfoil.find_best_provider(testrecipe)
89            if not best:
90                self.fail('Unable to find recipe providing %s' % testrecipe)
91            rd = tinfoil.parse_recipe_file(best[3], config_data=localdata)
92            self.assertEqual("testval", rd.getVar('TESTVAR'))
93
94    def test_parse_virtual_recipe_custom_data(self):
95        with bb.tinfoil.Tinfoil() as tinfoil:
96            tinfoil.prepare(config_only=False, quiet=2)
97            localdata = bb.data.createCopy(tinfoil.config_data)
98            localdata.setVar("TESTVAR", "testval")
99            testrecipe = 'nativesdk-gcc'
100            best = tinfoil.find_best_provider(testrecipe)
101            if not best:
102                self.fail('Unable to find recipe providing %s' % testrecipe)
103            rd = tinfoil.parse_recipe_file(best[3], config_data=localdata)
104            self.assertEqual("testval", rd.getVar('TESTVAR'))
105
106    def test_list_recipes(self):
107        with bb.tinfoil.Tinfoil() as tinfoil:
108            tinfoil.prepare(config_only=False, quiet=2)
109            # Check pkg_pn
110            checkpns = ['tar', 'automake', 'coreutils', 'm4-native', 'nativesdk-gcc']
111            pkg_pn = tinfoil.cooker.recipecaches[''].pkg_pn
112            for pn in checkpns:
113                self.assertIn(pn, pkg_pn)
114            # Check pkg_fn
115            checkfns = {'nativesdk-gcc': '^virtual:nativesdk:.*', 'coreutils': '.*/coreutils_.*.bb'}
116            for fn, pn in tinfoil.cooker.recipecaches[''].pkg_fn.items():
117                if pn in checkpns:
118                    if pn in checkfns:
119                        self.assertTrue(re.match(checkfns[pn], fn), 'Entry for %s: %s did not match %s' % (pn, fn, checkfns[pn]))
120                    checkpns.remove(pn)
121            if checkpns:
122                self.fail('Unable to find pkg_fn entries for: %s' % ', '.join(checkpns))
123
124    def test_wait_event(self):
125        with bb.tinfoil.Tinfoil() as tinfoil:
126            tinfoil.prepare(config_only=True)
127
128            tinfoil.set_event_mask(['bb.event.FilesMatchingFound', 'bb.command.CommandCompleted', 'bb.command.CommandFailed', 'bb.command.CommandExit'])
129
130            # Need to drain events otherwise events that were masked may still be in the queue
131            while tinfoil.wait_event():
132                pass
133
134            pattern = 'conf'
135            res = tinfoil.run_command('testCookerCommandEvent', pattern, handle_events=False)
136            self.assertTrue(res)
137
138            eventreceived = False
139            commandcomplete = False
140            start = time.time()
141            # Wait for maximum 60s in total so we'd detect spurious heartbeat events for example
142            while (not (eventreceived == True and commandcomplete == True)
143                    and (time.time() - start < 60)):
144                # if we received both events (on let's say a good day), we are done
145                event = tinfoil.wait_event(1)
146                if event:
147                    if isinstance(event, bb.command.CommandCompleted):
148                        commandcomplete = True
149                    elif isinstance(event, bb.event.FilesMatchingFound):
150                        self.assertEqual(pattern, event._pattern)
151                        self.assertIn('A', event._matches)
152                        self.assertIn('B', event._matches)
153                        eventreceived = True
154                    elif isinstance(event, logging.LogRecord):
155                        continue
156                    else:
157                        self.fail('Unexpected event: %s' % event)
158
159            self.assertTrue(commandcomplete, 'Timed out waiting for CommandCompleted event from bitbake server (Matching event received: %s)' % str(eventreceived))
160            self.assertTrue(eventreceived, 'Did not receive FilesMatchingFound event from bitbake server')
161
162    def test_setvariable_clean(self):
163        # First check that setVariable affects the datastore
164        with bb.tinfoil.Tinfoil() as tinfoil:
165            tinfoil.prepare(config_only=True)
166            tinfoil.run_command('setVariable', 'TESTVAR', 'specialvalue')
167            self.assertEqual(tinfoil.config_data.getVar('TESTVAR'), 'specialvalue', 'Value set using setVariable is not reflected in client-side getVar()')
168
169        # Now check that the setVariable's effects are no longer present
170        # (this may legitimately break in future if we stop reinitialising
171        # the datastore, in which case we'll have to reconsider use of
172        # setVariable entirely)
173        with bb.tinfoil.Tinfoil() as tinfoil:
174            tinfoil.prepare(config_only=True)
175            self.assertNotEqual(tinfoil.config_data.getVar('TESTVAR'), 'specialvalue', 'Value set using setVariable is still present!')
176
177        # Now check that setVar on the main datastore works (uses setVariable internally)
178        with bb.tinfoil.Tinfoil() as tinfoil:
179            tinfoil.prepare(config_only=True)
180            tinfoil.config_data.setVar('TESTVAR', 'specialvalue')
181            value = tinfoil.run_command('getVariable', 'TESTVAR')
182            self.assertEqual(value, 'specialvalue', 'Value set using config_data.setVar() is not reflected in config_data.getVar()')
183
184    def test_datastore_operations(self):
185        with bb.tinfoil.Tinfoil() as tinfoil:
186            tinfoil.prepare(config_only=True)
187            # Test setVarFlag() / getVarFlag()
188            tinfoil.config_data.setVarFlag('TESTVAR', 'flagname', 'flagval')
189            value = tinfoil.config_data.getVarFlag('TESTVAR', 'flagname')
190            self.assertEqual(value, 'flagval', 'Value set using config_data.setVarFlag() is not reflected in config_data.getVarFlag()')
191            # Test delVarFlag()
192            tinfoil.config_data.setVarFlag('TESTVAR', 'otherflag', 'othervalue')
193            tinfoil.config_data.delVarFlag('TESTVAR', 'flagname')
194            value = tinfoil.config_data.getVarFlag('TESTVAR', 'flagname')
195            self.assertEqual(value, None, 'Varflag deleted using config_data.delVarFlag() is not reflected in config_data.getVarFlag()')
196            value = tinfoil.config_data.getVarFlag('TESTVAR', 'otherflag')
197            self.assertEqual(value, 'othervalue', 'Varflag deleted using config_data.delVarFlag() caused unrelated flag to be removed')
198            # Test delVar()
199            tinfoil.config_data.setVar('TESTVAR', 'varvalue')
200            value = tinfoil.config_data.getVar('TESTVAR')
201            self.assertEqual(value, 'varvalue', 'Value set using config_data.setVar() is not reflected in config_data.getVar()')
202            tinfoil.config_data.delVar('TESTVAR')
203            value = tinfoil.config_data.getVar('TESTVAR')
204            self.assertEqual(value, None, 'Variable deleted using config_data.delVar() appears to still have a value')
205            # Test renameVar()
206            tinfoil.config_data.setVar('TESTVAROLD', 'origvalue')
207            tinfoil.config_data.renameVar('TESTVAROLD', 'TESTVARNEW')
208            value = tinfoil.config_data.getVar('TESTVAROLD')
209            self.assertEqual(value, None, 'Variable renamed using config_data.renameVar() still seems to exist')
210            value = tinfoil.config_data.getVar('TESTVARNEW')
211            self.assertEqual(value, 'origvalue', 'Variable renamed using config_data.renameVar() does not appear with new name')
212            # Test overrides
213            tinfoil.config_data.setVar('TESTVAR', 'original')
214            tinfoil.config_data.setVar('TESTVAR:overrideone', 'one')
215            tinfoil.config_data.setVar('TESTVAR:overridetwo', 'two')
216            tinfoil.config_data.appendVar('OVERRIDES', ':overrideone')
217            value = tinfoil.config_data.getVar('TESTVAR')
218            self.assertEqual(value, 'one', 'Variable overrides not functioning correctly')
219
220    def test_variable_history(self):
221        # Basic test to ensure that variable history works when tracking=True
222        with bb.tinfoil.Tinfoil(tracking=True) as tinfoil:
223            tinfoil.prepare(config_only=False, quiet=2)
224            # Note that _tracking for any datastore we get will be
225            # false here, that's currently expected - so we can't check
226            # for that
227            history = tinfoil.config_data.varhistory.variable('DL_DIR')
228            for entry in history:
229                if entry['file'].endswith('/bitbake.conf'):
230                    if entry['op'] in ['set', 'set?']:
231                        break
232            else:
233                self.fail('Did not find history entry setting DL_DIR in bitbake.conf. History: %s' % history)
234            # Check it works for recipes as well
235            testrecipe = 'zlib'
236            rd = tinfoil.parse_recipe(testrecipe)
237            history = rd.varhistory.variable('LICENSE')
238            bbfound = -1
239            recipefound = -1
240            for i, entry in enumerate(history):
241                if entry['file'].endswith('/bitbake.conf'):
242                    if entry['detail'] == 'INVALID' and entry['op'] in ['set', 'set?']:
243                        bbfound = i
244                elif entry['file'].endswith('.bb'):
245                    if entry['op'] == 'set':
246                        recipefound = i
247            if bbfound == -1:
248                self.fail('Did not find history entry setting LICENSE in bitbake.conf parsing %s recipe. History: %s' % (testrecipe, history))
249            if recipefound == -1:
250                self.fail('Did not find history entry setting LICENSE in %s recipe. History: %s' % (testrecipe, history))
251            if bbfound > recipefound:
252                self.fail('History entry setting LICENSE in %s recipe and in bitbake.conf in wrong order. History: %s' % (testrecipe, history))
253