xref: /openbmc/openbmc/poky/meta/lib/oeqa/selftest/cases/devtool.py (revision c9537f57ab488bf5d90132917b0184e2527970a5)
1#
2# Copyright OpenEmbedded Contributors
3#
4# SPDX-License-Identifier: MIT
5#
6
7import errno
8import os
9import re
10import shutil
11import tempfile
12import glob
13import fnmatch
14import unittest
15import json
16
17from oeqa.selftest.case import OESelftestTestCase
18from oeqa.utils.commands import runCmd, bitbake, get_bb_var, create_temp_layer
19from oeqa.utils.commands import get_bb_vars, runqemu, get_test_layer
20from oeqa.core.decorator import OETestTag
21
22oldmetapath = None
23
24def setUpModule():
25    import bb.utils
26
27    global templayerdir
28    templayerdir = tempfile.mkdtemp(prefix='devtoolqa')
29    corecopydir = os.path.join(templayerdir, 'core-copy')
30    bblayers_conf = os.path.join(os.environ['BUILDDIR'], 'conf', 'bblayers.conf')
31    edited_layers = []
32    # make sure user doesn't have a local workspace
33    result = runCmd('bitbake-layers show-layers')
34    assert "workspacelayer" not in result.output, "Devtool test suite cannot be run with a local workspace directory"
35
36    # We need to take a copy of the meta layer so we can modify it and not
37    # have any races against other tests that might be running in parallel
38    # however things like COREBASE mean that you can't just copy meta, you
39    # need the whole repository.
40    def bblayers_edit_cb(layerpath, canonical_layerpath):
41        global oldmetapath
42        if not canonical_layerpath.endswith('/'):
43            # This helps us match exactly when we're using this path later
44            canonical_layerpath += '/'
45        if not edited_layers and canonical_layerpath.endswith('/meta/'):
46            canonical_layerpath = os.path.realpath(canonical_layerpath) + '/'
47            edited_layers.append(layerpath)
48            oldmetapath = os.path.realpath(layerpath)
49
50            # when downloading poky from tar.gz some tests will be skipped (BUG 12389)
51            try:
52                runCmd('git rev-parse --is-inside-work-tree', cwd=canonical_layerpath)
53            except:
54                raise unittest.SkipTest("devtool tests require folder to be a git repo")
55
56            result = runCmd('git rev-parse --show-toplevel', cwd=canonical_layerpath)
57            oldreporoot = result.output.rstrip()
58            newmetapath = os.path.join(corecopydir, os.path.relpath(oldmetapath, oldreporoot))
59            runCmd('git clone file://%s %s' % (oldreporoot, corecopydir), cwd=templayerdir)
60            # Now we need to copy any modified files
61            # You might ask "why not just copy the entire tree instead of
62            # cloning and doing this?" - well, the problem with that is
63            # TMPDIR or an equally large subdirectory might exist
64            # under COREBASE and we don't want to copy that, so we have
65            # to be selective.
66            result = runCmd('git status --porcelain', cwd=oldreporoot)
67
68            # Also copy modifications to the 'scripts/' directory
69            canonical_layerpath_scripts = os.path.normpath(canonical_layerpath + "../scripts")
70
71            for line in result.output.splitlines():
72                if line.startswith(' M ') or line.startswith('?? '):
73                    relpth = line.split()[1]
74                    pth = os.path.join(oldreporoot, relpth)
75                    if pth.startswith(canonical_layerpath) or pth.startswith(canonical_layerpath_scripts):
76                        if relpth.endswith('/'):
77                            destdir = os.path.join(corecopydir, relpth)
78                            # avoid race condition by not copying .pyc files YPBZ#13421,13803
79                            shutil.copytree(pth, destdir, ignore=shutil.ignore_patterns('*.pyc', '__pycache__'))
80                        else:
81                            destdir = os.path.join(corecopydir, os.path.dirname(relpth))
82                            bb.utils.mkdirhier(destdir)
83                            shutil.copy2(pth, destdir)
84            return newmetapath
85        else:
86            return layerpath
87    bb.utils.edit_bblayers_conf(bblayers_conf, None, None, bblayers_edit_cb)
88
89def tearDownModule():
90    if oldmetapath:
91        edited_layers = []
92        def bblayers_edit_cb(layerpath, canonical_layerpath):
93            if not edited_layers and canonical_layerpath.endswith('/meta'):
94                edited_layers.append(layerpath)
95                return oldmetapath
96            else:
97                return layerpath
98        bblayers_conf = os.path.join(os.environ['BUILDDIR'], 'conf', 'bblayers.conf')
99        bb.utils.edit_bblayers_conf(bblayers_conf, None, None, bblayers_edit_cb)
100    shutil.rmtree(templayerdir)
101
102class DevtoolTestCase(OESelftestTestCase):
103
104    def setUp(self):
105        """Test case setup function"""
106        super(DevtoolTestCase, self).setUp()
107        self.workspacedir = os.path.join(self.builddir, 'workspace')
108        self.assertTrue(not os.path.exists(self.workspacedir),
109                        'This test cannot be run with a workspace directory '
110                        'under the build directory')
111
112    def _check_src_repo(self, repo_dir):
113        """Check srctree git repository"""
114        self.assertTrue(os.path.isdir(os.path.join(repo_dir, '.git')),
115                        'git repository for external source tree not found')
116        result = runCmd('git status --porcelain', cwd=repo_dir)
117        self.assertEqual(result.output.strip(), "",
118                         'Created git repo is not clean')
119        result = runCmd('git symbolic-ref HEAD', cwd=repo_dir)
120        self.assertEqual(result.output.strip(), "refs/heads/devtool",
121                         'Wrong branch in git repo')
122
123    def _check_repo_status(self, repo_dir, expected_status):
124        """Check the worktree status of a repository"""
125        result = runCmd('git status . --porcelain',
126                        cwd=repo_dir)
127        for line in result.output.splitlines():
128            for ind, (f_status, fn_re) in enumerate(expected_status):
129                if re.match(fn_re, line[3:]):
130                    if f_status != line[:2]:
131                        self.fail('Unexpected status in line: %s' % line)
132                    expected_status.pop(ind)
133                    break
134            else:
135                self.fail('Unexpected modified file in line: %s' % line)
136        if expected_status:
137            self.fail('Missing file changes: %s' % expected_status)
138
139    def _test_recipe_contents(self, recipefile, checkvars, checkinherits):
140        with open(recipefile, 'r') as f:
141            invar = None
142            invalue = None
143            inherits = set()
144            for line in f:
145                var = None
146                if invar:
147                    value = line.strip().strip('"')
148                    if value.endswith('\\'):
149                        invalue += ' ' + value[:-1].strip()
150                        continue
151                    else:
152                        invalue += ' ' + value.strip()
153                        var = invar
154                        value = invalue
155                        invar = None
156                elif '=' in line:
157                    splitline = line.split('=', 1)
158                    var = splitline[0].rstrip()
159                    value = splitline[1].strip().strip('"')
160                    if value.endswith('\\'):
161                        invalue = value[:-1].strip()
162                        invar = var
163                        continue
164                elif line.startswith('inherit '):
165                    inherits.update(line.split()[1:])
166
167                if var and var in checkvars:
168                    needvalue = checkvars.pop(var)
169                    if needvalue is None:
170                        self.fail('Variable %s should not appear in recipe, but value is being set to "%s"' % (var, value))
171                    if isinstance(needvalue, set):
172                        if var == 'LICENSE':
173                            value = set(value.split(' & '))
174                        else:
175                            value = set(value.split())
176                    self.assertEqual(value, needvalue, 'values for %s do not match' % var)
177
178
179        missingvars = {}
180        for var, value in checkvars.items():
181            if value is not None:
182                missingvars[var] = value
183        self.assertEqual(missingvars, {}, 'Some expected variables not found in recipe: %s' % checkvars)
184
185        for inherit in checkinherits:
186            self.assertIn(inherit, inherits, 'Missing inherit of %s' % inherit)
187
188    def _check_bbappend(self, testrecipe, recipefile, appenddir):
189        result = runCmd('bitbake-layers show-appends', cwd=self.builddir)
190        resultlines = result.output.splitlines()
191        inrecipe = False
192        bbappends = []
193        bbappendfile = None
194        for line in resultlines:
195            if inrecipe:
196                if line.startswith(' '):
197                    bbappends.append(line.strip())
198                else:
199                    break
200            elif line == '%s:' % os.path.basename(recipefile):
201                inrecipe = True
202        self.assertLessEqual(len(bbappends), 2, '%s recipe is being bbappended by another layer - bbappends found:\n  %s' % (testrecipe, '\n  '.join(bbappends)))
203        for bbappend in bbappends:
204            if bbappend.startswith(appenddir):
205                bbappendfile = bbappend
206                break
207        else:
208            self.fail('bbappend for recipe %s does not seem to be created in test layer' % testrecipe)
209        return bbappendfile
210
211    def _create_temp_layer(self, templayerdir, addlayer, templayername, priority=999, recipepathspec='recipes-*/*'):
212        create_temp_layer(templayerdir, templayername, priority, recipepathspec)
213        if addlayer:
214            self.add_command_to_tearDown('bitbake-layers remove-layer %s || true' % templayerdir)
215            result = runCmd('bitbake-layers add-layer %s' % templayerdir, cwd=self.builddir)
216
217    def _process_ls_output(self, output):
218        """
219        Convert ls -l output to a format we can reasonably compare from one context
220        to another (e.g. from host to target)
221        """
222        filelist = []
223        for line in output.splitlines():
224            splitline = line.split()
225            if len(splitline) < 8:
226                self.fail('_process_ls_output: invalid output line: %s' % line)
227            # Remove trailing . on perms
228            splitline[0] = splitline[0].rstrip('.')
229            # Remove leading . on paths
230            splitline[-1] = splitline[-1].lstrip('.')
231            # Drop fields we don't want to compare
232            del splitline[7]
233            del splitline[6]
234            del splitline[5]
235            del splitline[4]
236            del splitline[1]
237            filelist.append(' '.join(splitline))
238        return filelist
239
240    def _check_diff(self, diffoutput, addlines, removelines):
241        """Check output from 'git diff' matches expectation"""
242        remaining_addlines = addlines[:]
243        remaining_removelines = removelines[:]
244        for line in diffoutput.splitlines():
245            if line.startswith('+++') or line.startswith('---'):
246                continue
247            elif line.startswith('+'):
248                matched = False
249                for item in addlines:
250                    if re.match(item, line[1:].strip()):
251                        matched = True
252                        remaining_addlines.remove(item)
253                        break
254                self.assertTrue(matched, 'Unexpected diff add line: %s' % line)
255            elif line.startswith('-'):
256                matched = False
257                for item in removelines:
258                    if re.match(item, line[1:].strip()):
259                        matched = True
260                        remaining_removelines.remove(item)
261                        break
262                self.assertTrue(matched, 'Unexpected diff remove line: %s' % line)
263        if remaining_addlines:
264            self.fail('Expected added lines not found: %s' % remaining_addlines)
265        if remaining_removelines:
266            self.fail('Expected removed lines not found: %s' % remaining_removelines)
267
268    def _check_runqemu_prerequisites(self):
269        """Check runqemu is available
270
271        Whilst some tests would seemingly be better placed as a runtime test,
272        unfortunately the runtime tests run under bitbake and you can't run
273        devtool within bitbake (since devtool needs to run bitbake itself).
274        Additionally we are testing build-time functionality as well, so
275        really this has to be done as an oe-selftest test.
276        """
277        machine = get_bb_var('MACHINE')
278        if not machine.startswith('qemu'):
279            self.skipTest('This test only works with qemu machines')
280        if not os.path.exists('/etc/runqemu-nosudo'):
281            self.skipTest('You must set up tap devices with scripts/runqemu-gen-tapdevs before running this test')
282        result = runCmd('PATH="$PATH:/sbin:/usr/sbin" ip tuntap show', ignore_status=True)
283        if result.status != 0:
284            result = runCmd('PATH="$PATH:/sbin:/usr/sbin" ifconfig -a', ignore_status=True)
285            if result.status != 0:
286                self.skipTest('Failed to determine if tap devices exist with ifconfig or ip: %s' % result.output)
287        for line in result.output.splitlines():
288            if line.startswith('tap'):
289                break
290        else:
291            self.skipTest('No tap devices found - you must set up tap devices with scripts/runqemu-gen-tapdevs before running this test')
292
293    def _test_devtool_add_git_url(self, git_url, version, pn, resulting_src_uri, srcrev=None):
294        self.track_for_cleanup(self.workspacedir)
295        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
296        command = 'devtool add --version %s %s %s' % (version, pn, git_url)
297        if srcrev :
298            command += ' --srcrev %s' %srcrev
299        result = runCmd(command)
300        self.assertExists(os.path.join(self.workspacedir, 'conf', 'layer.conf'), 'Workspace directory not created')
301        # Check the recipe name is correct
302        recipefile = get_bb_var('FILE', pn)
303        self.assertIn('%s_git.bb' % pn, recipefile, 'Recipe file incorrectly named')
304        self.assertIn(recipefile, result.output)
305        # Test devtool status
306        result = runCmd('devtool status')
307        self.assertIn(pn, result.output)
308        self.assertIn(recipefile, result.output)
309        checkvars = {}
310        checkvars['SRC_URI'] = resulting_src_uri
311        self._test_recipe_contents(recipefile, checkvars, [])
312
313class DevtoolBase(DevtoolTestCase):
314
315    @classmethod
316    def setUpClass(cls):
317        super(DevtoolBase, cls).setUpClass()
318        bb_vars = get_bb_vars(['TOPDIR', 'SSTATE_DIR'])
319        cls.original_sstate = bb_vars['SSTATE_DIR']
320        cls.devtool_sstate = os.path.join(bb_vars['TOPDIR'], 'sstate_devtool')
321        cls.sstate_conf  = 'SSTATE_DIR = "%s"\n' % cls.devtool_sstate
322        cls.sstate_conf += ('SSTATE_MIRRORS += "file://.* file:///%s/PATH"\n'
323                            % cls.original_sstate)
324        cls.sstate_conf += ('BB_HASHSERVE_UPSTREAM = "hashserv.yoctoproject.org:8686"\n')
325
326    @classmethod
327    def tearDownClass(cls):
328        cls.logger.debug('Deleting devtool sstate cache on %s' % cls.devtool_sstate)
329        runCmd('rm -rf %s' % cls.devtool_sstate)
330        super(DevtoolBase, cls).tearDownClass()
331
332    def setUp(self):
333        """Test case setup function"""
334        super(DevtoolBase, self).setUp()
335        self.append_config(self.sstate_conf)
336
337
338class DevtoolTests(DevtoolBase):
339
340    def test_create_workspace(self):
341        # Check preconditions
342        result = runCmd('bitbake-layers show-layers')
343        self.assertTrue('\nworkspace' not in result.output, 'This test cannot be run with a workspace layer in bblayers.conf')
344        # remove conf/devtool.conf to avoid it corrupting tests
345        devtoolconf = os.path.join(self.builddir, 'conf', 'devtool.conf')
346        self.track_for_cleanup(devtoolconf)
347        # Try creating a workspace layer with a specific path
348        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
349        self.track_for_cleanup(tempdir)
350        result = runCmd('devtool create-workspace %s' % tempdir)
351        self.assertTrue(os.path.isfile(os.path.join(tempdir, 'conf', 'layer.conf')), msg = "No workspace created. devtool output: %s " % result.output)
352        result = runCmd('bitbake-layers show-layers')
353        self.assertIn(tempdir, result.output)
354        # Try creating a workspace layer with the default path
355        self.track_for_cleanup(self.workspacedir)
356        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
357        result = runCmd('devtool create-workspace')
358        self.assertTrue(os.path.isfile(os.path.join(self.workspacedir, 'conf', 'layer.conf')), msg = "No workspace created. devtool output: %s " % result.output)
359        result = runCmd('bitbake-layers show-layers')
360        self.assertNotIn(tempdir, result.output)
361        self.assertIn(self.workspacedir, result.output)
362
363class DevtoolAddTests(DevtoolBase):
364
365    def test_devtool_add(self):
366        # Fetch source
367        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
368        self.track_for_cleanup(tempdir)
369        pn = 'pv'
370        pv = '1.5.3'
371        url = 'http://downloads.yoctoproject.org/mirror/sources/pv-1.5.3.tar.bz2'
372        result = runCmd('wget %s' % url, cwd=tempdir)
373        result = runCmd('tar xfv %s' % os.path.basename(url), cwd=tempdir)
374        srcdir = os.path.join(tempdir, '%s-%s' % (pn, pv))
375        self.assertTrue(os.path.isfile(os.path.join(srcdir, 'configure')), 'Unable to find configure script in source directory')
376        # Test devtool add
377        self.track_for_cleanup(self.workspacedir)
378        self.add_command_to_tearDown('bitbake -c cleansstate %s' % pn)
379        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
380        result = runCmd('devtool add %s %s' % (pn, srcdir))
381        self.assertExists(os.path.join(self.workspacedir, 'conf', 'layer.conf'), 'Workspace directory not created')
382        # Test devtool status
383        result = runCmd('devtool status')
384        recipepath = '%s/recipes/%s/%s_%s.bb' % (self.workspacedir, pn, pn, pv)
385        self.assertIn(recipepath, result.output)
386        self.assertIn(srcdir, result.output)
387        # Test devtool find-recipe
388        result = runCmd('devtool -q find-recipe %s' % pn)
389        self.assertEqual(recipepath, result.output.strip())
390        # Test devtool edit-recipe
391        result = runCmd('VISUAL="echo 123" devtool -q edit-recipe %s' % pn)
392        self.assertEqual('123 %s' % recipepath, result.output.strip())
393        # Clean up anything in the workdir/sysroot/sstate cache (have to do this *after* devtool add since the recipe only exists then)
394        bitbake('%s -c cleansstate' % pn)
395        # Test devtool build
396        result = runCmd('devtool build %s' % pn)
397        bb_vars = get_bb_vars(['D', 'bindir'], pn)
398        installdir = bb_vars['D']
399        self.assertTrue(installdir, 'Could not query installdir variable')
400        bindir = bb_vars['bindir']
401        self.assertTrue(bindir, 'Could not query bindir variable')
402        if bindir[0] == '/':
403            bindir = bindir[1:]
404        self.assertTrue(os.path.isfile(os.path.join(installdir, bindir, 'pv')), 'pv binary not found in D')
405
406    def test_devtool_add_binary(self):
407        # Create a binary package containing a known test file
408        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
409        self.track_for_cleanup(tempdir)
410        pn = 'tst-bin'
411        pv = '1.0'
412        test_file_dir     = "var/lib/%s/" % pn
413        test_file_name    = "test_file"
414        test_file_content = "TEST CONTENT"
415        test_file_package_root = os.path.join(tempdir, pn)
416        test_file_dir_full = os.path.join(test_file_package_root, test_file_dir)
417        bb.utils.mkdirhier(test_file_dir_full)
418        with open(os.path.join(test_file_dir_full, test_file_name), "w") as f:
419           f.write(test_file_content)
420        bin_package_path = os.path.join(tempdir, "%s.tar.gz" % pn)
421        runCmd("tar czf %s -C %s ." % (bin_package_path, test_file_package_root))
422
423        # Test devtool add -b on the binary package
424        self.track_for_cleanup(self.workspacedir)
425        self.add_command_to_tearDown('bitbake -c cleansstate %s' % pn)
426        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
427        result = runCmd('devtool add  -b %s %s' % (pn, bin_package_path))
428        self.assertExists(os.path.join(self.workspacedir, 'conf', 'layer.conf'), 'Workspace directory not created')
429
430        # Build the resulting recipe
431        result = runCmd('devtool build %s' % pn)
432        installdir = get_bb_var('D', pn)
433        self.assertTrue(installdir, 'Could not query installdir variable')
434
435        # Check that a known file from the binary package has indeed been installed
436        self.assertTrue(os.path.isfile(os.path.join(installdir, test_file_dir, test_file_name)), '%s not found in D' % test_file_name)
437
438    def test_devtool_add_git_local(self):
439        # We need dbus built so that DEPENDS recognition works
440        bitbake('dbus')
441        # Fetch source from a remote URL, but do it outside of devtool
442        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
443        self.track_for_cleanup(tempdir)
444        pn = 'dbus-wait'
445        srcrev = '6cc6077a36fe2648a5f993fe7c16c9632f946517'
446        # We choose an https:// git URL here to check rewriting the URL works
447        url = 'https://git.yoctoproject.org/git/dbus-wait'
448        # Force fetching to "noname" subdir so we verify we're picking up the name from autoconf
449        # instead of the directory name
450        result = runCmd('git clone %s noname' % url, cwd=tempdir)
451        srcdir = os.path.join(tempdir, 'noname')
452        result = runCmd('git reset --hard %s' % srcrev, cwd=srcdir)
453        self.assertTrue(os.path.isfile(os.path.join(srcdir, 'configure.ac')), 'Unable to find configure script in source directory')
454        # Test devtool add
455        self.track_for_cleanup(self.workspacedir)
456        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
457        # Don't specify a name since we should be able to auto-detect it
458        result = runCmd('devtool add %s' % srcdir)
459        self.assertExists(os.path.join(self.workspacedir, 'conf', 'layer.conf'), 'Workspace directory not created')
460        # Check the recipe name is correct
461        recipefile = get_bb_var('FILE', pn)
462        self.assertIn('%s_git.bb' % pn, recipefile, 'Recipe file incorrectly named')
463        self.assertIn(recipefile, result.output)
464        # Test devtool status
465        result = runCmd('devtool status')
466        self.assertIn(pn, result.output)
467        self.assertIn(srcdir, result.output)
468        self.assertIn(recipefile, result.output)
469        checkvars = {}
470        checkvars['LICENSE'] = 'GPL-2.0-only'
471        checkvars['LIC_FILES_CHKSUM'] = 'file://COPYING;md5=b234ee4d69f5fce4486a80fdaf4a4263'
472        checkvars['S'] = '${WORKDIR}/git'
473        checkvars['PV'] = '0.1+git'
474        checkvars['SRC_URI'] = 'git://git.yoctoproject.org/git/dbus-wait;protocol=https;branch=master'
475        checkvars['SRCREV'] = srcrev
476        checkvars['DEPENDS'] = set(['dbus'])
477        self._test_recipe_contents(recipefile, checkvars, [])
478
479    def test_devtool_add_git_style1(self):
480        version = 'v3.1.0'
481        pn = 'mbedtls'
482        # this will trigger reformat_git_uri with branch parameter in url
483        git_url = "'git://git@github.com/ARMmbed/mbedtls.git;branch=mbedtls-2.28;protocol=https'"
484        resulting_src_uri = "git://git@github.com/ARMmbed/mbedtls.git;branch=mbedtls-2.28;protocol=https"
485        self._test_devtool_add_git_url(git_url, version, pn, resulting_src_uri)
486
487    def test_devtool_add_git_style2(self):
488        version = 'v3.1.0'
489        srcrev = 'v3.1.0'
490        pn = 'mbedtls'
491        # this will trigger reformat_git_uri with branch parameter in url
492        git_url = "'git://git@github.com/ARMmbed/mbedtls.git;protocol=https'"
493        resulting_src_uri = "git://git@github.com/ARMmbed/mbedtls.git;protocol=https;branch=master"
494        self._test_devtool_add_git_url(git_url, version, pn, resulting_src_uri, srcrev)
495
496    def test_devtool_add_library(self):
497        # Fetch source
498        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
499        self.track_for_cleanup(tempdir)
500        version = '1.1'
501        url = 'https://www.intra2net.com/en/developer/libftdi/download/libftdi1-%s.tar.bz2' % version
502        result = runCmd('wget %s' % url, cwd=tempdir)
503        result = runCmd('tar xfv libftdi1-%s.tar.bz2' % version, cwd=tempdir)
504        srcdir = os.path.join(tempdir, 'libftdi1-%s' % version)
505        self.assertTrue(os.path.isfile(os.path.join(srcdir, 'CMakeLists.txt')), 'Unable to find CMakeLists.txt in source directory')
506        # Test devtool add (and use -V so we test that too)
507        self.track_for_cleanup(self.workspacedir)
508        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
509        result = runCmd('devtool add libftdi %s -V %s' % (srcdir, version))
510        self.assertExists(os.path.join(self.workspacedir, 'conf', 'layer.conf'), 'Workspace directory not created')
511        # Test devtool status
512        result = runCmd('devtool status')
513        self.assertIn('libftdi', result.output)
514        self.assertIn(srcdir, result.output)
515        # Clean up anything in the workdir/sysroot/sstate cache (have to do this *after* devtool add since the recipe only exists then)
516        bitbake('libftdi -c cleansstate')
517        # libftdi's python/CMakeLists.txt is a bit broken, so let's just disable it
518        # There's also the matter of it installing cmake files to a path we don't
519        # normally cover, which triggers the installed-vs-shipped QA test we have
520        # within do_package
521        recipefile = '%s/recipes/libftdi/libftdi_%s.bb' % (self.workspacedir, version)
522        result = runCmd('recipetool setvar %s EXTRA_OECMAKE -- \'-DPYTHON_BINDINGS=OFF -DLIBFTDI_CMAKE_CONFIG_DIR=${datadir}/cmake/Modules\'' % recipefile)
523        with open(recipefile, 'a') as f:
524            f.write('\nFILES:${PN}-dev += "${datadir}/cmake/Modules"\n')
525            # We don't have the ability to pick up this dependency automatically yet...
526            f.write('\nDEPENDS += "libusb1"\n')
527            f.write('\nTESTLIBOUTPUT = "${COMPONENTS_DIR}/${TUNE_PKGARCH}/${PN}/${libdir}"\n')
528        # Test devtool build
529        result = runCmd('devtool build libftdi')
530        bb_vars = get_bb_vars(['TESTLIBOUTPUT', 'STAMP'], 'libftdi')
531        staging_libdir = bb_vars['TESTLIBOUTPUT']
532        self.assertTrue(staging_libdir, 'Could not query TESTLIBOUTPUT variable')
533        self.assertTrue(os.path.isfile(os.path.join(staging_libdir, 'libftdi1.so.2.1.0')), "libftdi binary not found in STAGING_LIBDIR. Output of devtool build libftdi %s" % result.output)
534        # Test devtool reset
535        stampprefix = bb_vars['STAMP']
536        result = runCmd('devtool reset libftdi')
537        result = runCmd('devtool status')
538        self.assertNotIn('libftdi', result.output)
539        self.assertTrue(stampprefix, 'Unable to get STAMP value for recipe libftdi')
540        matches = glob.glob(stampprefix + '*')
541        self.assertFalse(matches, 'Stamp files exist for recipe libftdi that should have been cleaned')
542        self.assertFalse(os.path.isfile(os.path.join(staging_libdir, 'libftdi1.so.2.1.0')), 'libftdi binary still found in STAGING_LIBDIR after cleaning')
543
544    def test_devtool_add_fetch(self):
545        # Fetch source
546        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
547        self.track_for_cleanup(tempdir)
548        testver = '0.23'
549        url = 'https://files.pythonhosted.org/packages/c0/41/bae1254e0396c0cc8cf1751cb7d9afc90a602353695af5952530482c963f/MarkupSafe-%s.tar.gz' % testver
550        testrecipe = 'python-markupsafe'
551        srcdir = os.path.join(tempdir, testrecipe)
552        # Test devtool add
553        self.track_for_cleanup(self.workspacedir)
554        self.add_command_to_tearDown('bitbake -c cleansstate %s' % testrecipe)
555        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
556        result = runCmd('devtool add --no-pypi %s %s -f %s' % (testrecipe, srcdir, url))
557        self.assertExists(os.path.join(self.workspacedir, 'conf', 'layer.conf'), 'Workspace directory not created. %s' % result.output)
558        self.assertTrue(os.path.isfile(os.path.join(srcdir, 'setup.py')), 'Unable to find setup.py in source directory')
559        self.assertTrue(os.path.isdir(os.path.join(srcdir, '.git')), 'git repository for external source tree was not created')
560        # Test devtool status
561        result = runCmd('devtool status')
562        self.assertIn(testrecipe, result.output)
563        self.assertIn(srcdir, result.output)
564        # Check recipe
565        recipefile = get_bb_var('FILE', testrecipe)
566        self.assertIn('%s_%s.bb' % (testrecipe, testver), recipefile, 'Recipe file incorrectly named')
567        checkvars = {}
568        checkvars['S'] = '${WORKDIR}/MarkupSafe-${PV}'
569        checkvars['SRC_URI'] = url.replace(testver, '${PV}')
570        self._test_recipe_contents(recipefile, checkvars, [])
571        # Try with version specified
572        result = runCmd('devtool reset -n %s' % testrecipe)
573        shutil.rmtree(srcdir)
574        fakever = '1.9'
575        result = runCmd('devtool add --no-pypi %s %s -f %s -V %s' % (testrecipe, srcdir, url, fakever))
576        self.assertTrue(os.path.isfile(os.path.join(srcdir, 'setup.py')), 'Unable to find setup.py in source directory')
577        # Test devtool status
578        result = runCmd('devtool status')
579        self.assertIn(testrecipe, result.output)
580        self.assertIn(srcdir, result.output)
581        # Check recipe
582        recipefile = get_bb_var('FILE', testrecipe)
583        self.assertIn('%s_%s.bb' % (testrecipe, fakever), recipefile, 'Recipe file incorrectly named')
584        checkvars = {}
585        checkvars['S'] = '${WORKDIR}/MarkupSafe-%s' % testver
586        checkvars['SRC_URI'] = url
587        self._test_recipe_contents(recipefile, checkvars, [])
588
589    def test_devtool_add_fetch_git(self):
590        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
591        self.track_for_cleanup(tempdir)
592        url = 'gitsm://git.yoctoproject.org/mraa'
593        url_branch = '%s;branch=master' % url
594        checkrev = 'ae127b19a50aa54255e4330ccfdd9a5d058e581d'
595        testrecipe = 'mraa'
596        srcdir = os.path.join(tempdir, testrecipe)
597        # Test devtool add
598        self.track_for_cleanup(self.workspacedir)
599        self.add_command_to_tearDown('bitbake -c cleansstate %s' % testrecipe)
600        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
601        result = runCmd('devtool add %s %s -a -f %s' % (testrecipe, srcdir, url))
602        self.assertExists(os.path.join(self.workspacedir, 'conf', 'layer.conf'), 'Workspace directory not created: %s' % result.output)
603        self.assertTrue(os.path.isfile(os.path.join(srcdir, 'imraa', 'imraa.c')), 'Unable to find imraa/imraa.c in source directory')
604        # Test devtool status
605        result = runCmd('devtool status')
606        self.assertIn(testrecipe, result.output)
607        self.assertIn(srcdir, result.output)
608        # Check recipe
609        recipefile = get_bb_var('FILE', testrecipe)
610        self.assertIn('_git.bb', recipefile, 'Recipe file incorrectly named')
611        checkvars = {}
612        checkvars['S'] = '${WORKDIR}/git'
613        checkvars['PV'] = '1.0+git'
614        checkvars['SRC_URI'] = url_branch
615        checkvars['SRCREV'] = '${AUTOREV}'
616        self._test_recipe_contents(recipefile, checkvars, [])
617        # Try with revision and version specified
618        result = runCmd('devtool reset -n %s' % testrecipe)
619        shutil.rmtree(srcdir)
620        url_rev = '%s;rev=%s' % (url, checkrev)
621        result = runCmd('devtool add %s %s -f "%s" -V 1.5' % (testrecipe, srcdir, url_rev))
622        self.assertTrue(os.path.isfile(os.path.join(srcdir, 'imraa', 'imraa.c')), 'Unable to find imraa/imraa.c in source directory')
623        # Test devtool status
624        result = runCmd('devtool status')
625        self.assertIn(testrecipe, result.output)
626        self.assertIn(srcdir, result.output)
627        # Check recipe
628        recipefile = get_bb_var('FILE', testrecipe)
629        self.assertIn('_git.bb', recipefile, 'Recipe file incorrectly named')
630        checkvars = {}
631        checkvars['S'] = '${WORKDIR}/git'
632        checkvars['PV'] = '1.5+git'
633        checkvars['SRC_URI'] = url_branch
634        checkvars['SRCREV'] = checkrev
635        self._test_recipe_contents(recipefile, checkvars, [])
636
637    def test_devtool_add_fetch_simple(self):
638        # Fetch source from a remote URL, auto-detecting name
639        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
640        self.track_for_cleanup(tempdir)
641        testver = '1.6.0'
642        url = 'http://www.ivarch.com/programs/sources/pv-%s.tar.bz2' % testver
643        testrecipe = 'pv'
644        srcdir = os.path.join(self.workspacedir, 'sources', testrecipe)
645        # Test devtool add
646        self.track_for_cleanup(self.workspacedir)
647        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
648        result = runCmd('devtool add %s' % url)
649        self.assertExists(os.path.join(self.workspacedir, 'conf', 'layer.conf'), 'Workspace directory not created. %s' % result.output)
650        self.assertTrue(os.path.isfile(os.path.join(srcdir, 'configure')), 'Unable to find configure script in source directory')
651        self.assertTrue(os.path.isdir(os.path.join(srcdir, '.git')), 'git repository for external source tree was not created')
652        # Test devtool status
653        result = runCmd('devtool status')
654        self.assertIn(testrecipe, result.output)
655        self.assertIn(srcdir, result.output)
656        # Check recipedevtool add
657        recipefile = get_bb_var('FILE', testrecipe)
658        self.assertIn('%s_%s.bb' % (testrecipe, testver), recipefile, 'Recipe file incorrectly named')
659        checkvars = {}
660        checkvars['S'] = None
661        checkvars['SRC_URI'] = url.replace(testver, '${PV}')
662        self._test_recipe_contents(recipefile, checkvars, [])
663
664    def test_devtool_add_npm(self):
665        collections = get_bb_var('BBFILE_COLLECTIONS').split()
666        if "openembedded-layer" not in collections:
667            self.skipTest("Test needs meta-oe for nodejs")
668
669        pn = 'savoirfairelinux-node-server-example'
670        pv = '1.0.0'
671        url = 'npm://registry.npmjs.org;package=@savoirfairelinux/node-server-example;version=' + pv
672        # Test devtool add
673        self.track_for_cleanup(self.workspacedir)
674        self.add_command_to_tearDown('bitbake -c cleansstate %s' % pn)
675        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
676        result = runCmd('devtool add \'%s\'' % url)
677        self.assertExists(os.path.join(self.workspacedir, 'conf', 'layer.conf'), 'Workspace directory not created')
678        self.assertExists(os.path.join(self.workspacedir, 'recipes', pn, '%s_%s.bb' % (pn, pv)), 'Recipe not created')
679        self.assertExists(os.path.join(self.workspacedir, 'recipes', pn, pn, 'npm-shrinkwrap.json'), 'Shrinkwrap not created')
680        # Test devtool status
681        result = runCmd('devtool status')
682        self.assertIn(pn, result.output)
683        # Clean up anything in the workdir/sysroot/sstate cache (have to do this *after* devtool add since the recipe only exists then)
684        bitbake('%s -c cleansstate' % pn)
685        # Test devtool build
686        result = runCmd('devtool build %s' % pn)
687
688    def test_devtool_add_python_egg_requires(self):
689        # Fetch source
690        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
691        self.track_for_cleanup(tempdir)
692        testver = '0.14.0'
693        url = 'https://files.pythonhosted.org/packages/e9/9e/25d59f5043cf763833b2581c8027fa92342c4cf8ee523b498ecdf460c16d/uvicorn-%s.tar.gz' % testver
694        testrecipe = 'python3-uvicorn'
695        srcdir = os.path.join(tempdir, testrecipe)
696        # Test devtool add
697        self.track_for_cleanup(self.workspacedir)
698        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
699        result = runCmd('devtool add %s %s -f %s' % (testrecipe, srcdir, url))
700
701class DevtoolModifyTests(DevtoolBase):
702
703    def test_devtool_modify(self):
704        import oe.path
705
706        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
707        self.track_for_cleanup(tempdir)
708        self.track_for_cleanup(self.workspacedir)
709        self.add_command_to_tearDown('bitbake -c clean mdadm')
710        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
711        result = runCmd('devtool modify mdadm -x %s' % tempdir)
712        self.assertExists(os.path.join(tempdir, 'Makefile'), 'Extracted source could not be found')
713        self.assertExists(os.path.join(self.workspacedir, 'conf', 'layer.conf'), 'Workspace directory not created')
714        matches = glob.glob(os.path.join(self.workspacedir, 'appends', 'mdadm_*.bbappend'))
715        self.assertTrue(matches, 'bbappend not created %s' % result.output)
716
717        # Test devtool status
718        result = runCmd('devtool status')
719        self.assertIn('mdadm', result.output)
720        self.assertIn(tempdir, result.output)
721        self._check_src_repo(tempdir)
722
723        bitbake('mdadm -C unpack')
724
725        def check_line(checkfile, expected, message, present=True):
726            # Check for $expected, on a line on its own, in checkfile.
727            with open(checkfile, 'r') as f:
728                if present:
729                    self.assertIn(expected + '\n', f, message)
730                else:
731                    self.assertNotIn(expected + '\n', f, message)
732
733        modfile = os.path.join(tempdir, 'mdadm.8.in')
734        bb_vars = get_bb_vars(['PKGD', 'mandir'], 'mdadm')
735        pkgd = bb_vars['PKGD']
736        self.assertTrue(pkgd, 'Could not query PKGD variable')
737        mandir = bb_vars['mandir']
738        self.assertTrue(mandir, 'Could not query mandir variable')
739        manfile = oe.path.join(pkgd, mandir, 'man8', 'mdadm.8')
740
741        check_line(modfile, 'Linux Software RAID', 'Could not find initial string')
742        check_line(modfile, 'antique pin sardine', 'Unexpectedly found replacement string', present=False)
743
744        result = runCmd("sed -i 's!^Linux Software RAID$!antique pin sardine!' %s" % modfile)
745        check_line(modfile, 'antique pin sardine', 'mdadm.8.in file not modified (sed failed)')
746
747        bitbake('mdadm -c package')
748        check_line(manfile, 'antique pin sardine', 'man file not modified. man searched file path: %s' % manfile)
749
750        result = runCmd('git checkout -- %s' % modfile, cwd=tempdir)
751        check_line(modfile, 'Linux Software RAID', 'man .in file not restored (git failed)')
752
753        bitbake('mdadm -c package')
754        check_line(manfile, 'Linux Software RAID', 'man file not updated. man searched file path: %s' % manfile)
755
756        result = runCmd('devtool reset mdadm')
757        result = runCmd('devtool status')
758        self.assertNotIn('mdadm', result.output)
759
760    def test_devtool_modify_go(self):
761        import oe.path
762        from tempfile import TemporaryDirectory
763        with TemporaryDirectory(prefix='devtoolqa') as tempdir:
764            self.track_for_cleanup(self.workspacedir)
765            self.add_command_to_tearDown('bitbake -c clean go-helloworld')
766            self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
767            result = runCmd('devtool modify go-helloworld -x %s' % tempdir)
768            self.assertExists(
769                oe.path.join(tempdir, 'src', 'golang.org', 'x', 'example', 'go.mod'),
770                             'Extracted source could not be found'
771            )
772            self.assertExists(
773                oe.path.join(self.workspacedir, 'conf', 'layer.conf'),
774                'Workspace directory not created'
775            )
776            matches = glob.glob(oe.path.join(self.workspacedir, 'appends', 'go-helloworld_*.bbappend'))
777            self.assertTrue(matches, 'bbappend not created %s' % result.output)
778
779    def test_devtool_buildclean(self):
780        def assertFile(path, *paths):
781            f = os.path.join(path, *paths)
782            self.assertExists(f)
783        def assertNoFile(path, *paths):
784            f = os.path.join(path, *paths)
785            self.assertNotExists(f)
786
787        # Clean up anything in the workdir/sysroot/sstate cache
788        bitbake('mdadm m4 -c cleansstate')
789        # Try modifying a recipe
790        tempdir_mdadm = tempfile.mkdtemp(prefix='devtoolqa')
791        tempdir_m4 = tempfile.mkdtemp(prefix='devtoolqa')
792        builddir_m4 = tempfile.mkdtemp(prefix='devtoolqa')
793        self.track_for_cleanup(tempdir_mdadm)
794        self.track_for_cleanup(tempdir_m4)
795        self.track_for_cleanup(builddir_m4)
796        self.track_for_cleanup(self.workspacedir)
797        self.add_command_to_tearDown('bitbake -c clean mdadm m4')
798        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
799        self.write_recipeinc('m4', 'EXTERNALSRC_BUILD = "%s"\ndo_clean() {\n\t:\n}\n' % builddir_m4)
800        try:
801            runCmd('devtool modify mdadm -x %s' % tempdir_mdadm)
802            runCmd('devtool modify m4 -x %s' % tempdir_m4)
803            assertNoFile(tempdir_mdadm, 'mdadm')
804            assertNoFile(builddir_m4, 'src/m4')
805            result = bitbake('m4 -e')
806            result = bitbake('mdadm m4 -c compile')
807            self.assertEqual(result.status, 0)
808            assertFile(tempdir_mdadm, 'mdadm')
809            assertFile(builddir_m4, 'src/m4')
810            # Check that buildclean task exists and does call make clean
811            bitbake('mdadm m4 -c buildclean')
812            assertNoFile(tempdir_mdadm, 'mdadm')
813            assertNoFile(builddir_m4, 'src/m4')
814            runCmd('echo "#Trigger rebuild" >> %s/Makefile' % tempdir_mdadm)
815            bitbake('mdadm m4 -c compile')
816            assertFile(tempdir_mdadm, 'mdadm')
817            assertFile(builddir_m4, 'src/m4')
818            bitbake('mdadm m4 -c clean')
819            # Check that buildclean task is run before clean for B == S
820            assertNoFile(tempdir_mdadm, 'mdadm')
821            # Check that buildclean task is not run before clean for B != S
822            assertFile(builddir_m4, 'src/m4')
823        finally:
824            self.delete_recipeinc('m4')
825
826    def test_devtool_modify_invalid(self):
827        # Try modifying some recipes
828        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
829        self.track_for_cleanup(tempdir)
830        self.track_for_cleanup(self.workspacedir)
831        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
832
833        testrecipes = 'perf kernel-devsrc package-index core-image-minimal meta-toolchain packagegroup-core-sdk'.split()
834        # Find actual name of gcc-source since it now includes the version - crude, but good enough for this purpose
835        result = runCmd('bitbake-layers show-recipes gcc-source*')
836        for line in result.output.splitlines():
837            # just match those lines that contain a real target
838            m = re.match('(?P<recipe>^[a-zA-Z0-9.-]+)(?P<colon>:$)', line)
839            if m:
840                testrecipes.append(m.group('recipe'))
841        for testrecipe in testrecipes:
842            # Check it's a valid recipe
843            bitbake('%s -e' % testrecipe)
844            # devtool extract should fail
845            result = runCmd('devtool extract %s %s' % (testrecipe, os.path.join(tempdir, testrecipe)), ignore_status=True)
846            self.assertNotEqual(result.status, 0, 'devtool extract on %s should have failed. devtool output: %s' % (testrecipe, result.output))
847            self.assertNotIn('Fetching ', result.output, 'devtool extract on %s should have errored out before trying to fetch' % testrecipe)
848            self.assertIn('ERROR: ', result.output, 'devtool extract on %s should have given an ERROR' % testrecipe)
849            # devtool modify should fail
850            result = runCmd('devtool modify %s -x %s' % (testrecipe, os.path.join(tempdir, testrecipe)), ignore_status=True)
851            self.assertNotEqual(result.status, 0, 'devtool modify on %s should have failed. devtool output: %s' %  (testrecipe, result.output))
852            self.assertIn('ERROR: ', result.output, 'devtool modify on %s should have given an ERROR' % testrecipe)
853
854    def test_devtool_modify_native(self):
855        # Check preconditions
856        self.assertTrue(not os.path.exists(self.workspacedir), 'This test cannot be run with a workspace directory under the build directory')
857        # Try modifying some recipes
858        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
859        self.track_for_cleanup(tempdir)
860        self.track_for_cleanup(self.workspacedir)
861        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
862
863        bbclassextended = False
864        inheritnative = False
865        testrecipes = 'cdrtools-native mtools-native apt-native desktop-file-utils-native'.split()
866        for testrecipe in testrecipes:
867            checkextend = 'native' in (get_bb_var('BBCLASSEXTEND', testrecipe) or '').split()
868            if not bbclassextended:
869                bbclassextended = checkextend
870            if not inheritnative:
871                inheritnative = not checkextend
872            result = runCmd('devtool modify %s -x %s' % (testrecipe, os.path.join(tempdir, testrecipe)))
873            self.assertNotIn('ERROR: ', result.output, 'ERROR in devtool modify output: %s' % result.output)
874            result = runCmd('devtool build %s' % testrecipe)
875            self.assertNotIn('ERROR: ', result.output, 'ERROR in devtool build output: %s' % result.output)
876            result = runCmd('devtool reset %s' % testrecipe)
877            self.assertNotIn('ERROR: ', result.output, 'ERROR in devtool reset output: %s' % result.output)
878
879        self.assertTrue(bbclassextended, 'None of these recipes are BBCLASSEXTENDed to native - need to adjust testrecipes list: %s' % ', '.join(testrecipes))
880        self.assertTrue(inheritnative, 'None of these recipes do "inherit native" - need to adjust testrecipes list: %s' % ', '.join(testrecipes))
881
882    def test_devtool_modify_localfiles_only(self):
883        # Check preconditions
884        testrecipe = 'base-files'
885        src_uri = (get_bb_var('SRC_URI', testrecipe) or '').split()
886        foundlocalonly = False
887        correct_symlink = False
888        for item in src_uri:
889            if item.startswith('file://'):
890                if '.patch' not in item:
891                    foundlocalonly = True
892            else:
893                foundlocalonly = False
894                break
895        self.assertTrue(foundlocalonly, 'This test expects the %s recipe to fetch local files only and it seems that it no longer does' % testrecipe)
896        # Clean up anything in the workdir/sysroot/sstate cache
897        bitbake('%s -c cleansstate' % testrecipe)
898        # Try modifying a recipe
899        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
900        self.track_for_cleanup(tempdir)
901        self.track_for_cleanup(self.workspacedir)
902        self.add_command_to_tearDown('bitbake -c clean %s' % testrecipe)
903        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
904        result = runCmd('devtool modify %s -x %s' % (testrecipe, tempdir))
905        srcfile = os.path.join(tempdir, 'share/dot.bashrc')
906        self.assertExists(srcfile, 'Extracted source could not be found')
907        matches = glob.glob(os.path.join(self.workspacedir, 'appends', '%s_*.bbappend' % testrecipe))
908        self.assertTrue(matches, 'bbappend not created')
909        # Test devtool status
910        result = runCmd('devtool status')
911        self.assertIn(testrecipe, result.output)
912        self.assertIn(tempdir, result.output)
913        # Try building
914        bitbake(testrecipe)
915
916    def test_devtool_modify_git(self):
917        # Check preconditions
918        testrecipe = 'psplash'
919        src_uri = get_bb_var('SRC_URI', testrecipe)
920        self.assertIn('git://', src_uri, 'This test expects the %s recipe to be a git recipe' % testrecipe)
921        # Clean up anything in the workdir/sysroot/sstate cache
922        bitbake('%s -c cleansstate' % testrecipe)
923        # Try modifying a recipe
924        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
925        self.track_for_cleanup(tempdir)
926        self.track_for_cleanup(self.workspacedir)
927        self.add_command_to_tearDown('bitbake -c clean %s' % testrecipe)
928        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
929        result = runCmd('devtool modify %s -x %s' % (testrecipe, tempdir))
930        self.assertExists(os.path.join(tempdir, 'Makefile.am'), 'Extracted source could not be found')
931        self.assertExists(os.path.join(self.workspacedir, 'conf', 'layer.conf'), 'Workspace directory not created. devtool output: %s' % result.output)
932        matches = glob.glob(os.path.join(self.workspacedir, 'appends', 'psplash_*.bbappend'))
933        self.assertTrue(matches, 'bbappend not created')
934        # Test devtool status
935        result = runCmd('devtool status')
936        self.assertIn(testrecipe, result.output)
937        self.assertIn(tempdir, result.output)
938        # Check git repo
939        self._check_src_repo(tempdir)
940        # Try building
941        bitbake(testrecipe)
942
943    def test_devtool_modify_git_no_extract(self):
944        # Check preconditions
945        testrecipe = 'psplash'
946        src_uri = get_bb_var('SRC_URI', testrecipe)
947        self.assertIn('git://', src_uri, 'This test expects the %s recipe to be a git recipe' % testrecipe)
948        # Clean up anything in the workdir/sysroot/sstate cache
949        bitbake('%s -c cleansstate' % testrecipe)
950        # Try modifying a recipe
951        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
952        self.track_for_cleanup(tempdir)
953        self.track_for_cleanup(self.workspacedir)
954        self.add_command_to_tearDown('bitbake -c clean %s' % testrecipe)
955        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
956        result = runCmd('git clone https://git.yoctoproject.org/psplash %s && devtool modify -n %s %s' % (tempdir, testrecipe, tempdir))
957        self.assertExists(os.path.join(self.workspacedir, 'conf', 'layer.conf'), 'Workspace directory not created. devtool output: %s' % result.output)
958        matches = glob.glob(os.path.join(self.workspacedir, 'appends', 'psplash_*.bbappend'))
959        self.assertTrue(matches, 'bbappend not created')
960        # Test devtool status
961        result = runCmd('devtool status')
962        self.assertIn(testrecipe, result.output)
963        self.assertIn(tempdir, result.output)
964
965    def test_devtool_modify_git_crates_subpath(self):
966        # This tests two things in devtool context:
967        #   - that we support local git dependencies for cargo based recipe
968        #   - that we support patches in SRC_URI when git url contains subpath parameter
969
970        # Check preconditions:
971        #    recipe inherits cargo
972        #    git:// uri with a subpath as the main package
973        #    some crate:// in SRC_URI
974        #    others git:// in SRC_URI
975        #    cointains a patch
976        testrecipe = 'hello-rs'
977        bb_vars = get_bb_vars(['SRC_URI', 'FILE', 'UNPACKDIR', 'CARGO_HOME'], testrecipe)
978        recipefile = bb_vars['FILE']
979        unpackdir = bb_vars['UNPACKDIR']
980        cargo_home = bb_vars['CARGO_HOME']
981        src_uri = bb_vars['SRC_URI'].split()
982        self.assertTrue(src_uri[0].startswith('git://'),
983                        'This test expects the %s recipe to have a git repo has its main uri' % testrecipe)
984        self.assertIn(';subpath=', src_uri[0],
985                      'This test expects the %s recipe to have a git uri with subpath' % testrecipe)
986        self.assertTrue(any([uri.startswith('crate://') for uri in src_uri]),
987                        'This test expects the %s recipe to have some crates in its src uris' % testrecipe)
988        self.assertGreaterEqual(sum(map(lambda x:x.startswith('git://'), src_uri)), 2,
989                           'This test expects the %s recipe to have several git:// uris' % testrecipe)
990        self.assertTrue(any([uri.startswith('file://') and '.patch' in uri for uri in src_uri]),
991                        'This test expects the %s recipe to have a patch in its src uris' % testrecipe)
992
993        self._test_recipe_contents(recipefile, {}, ['ptest-cargo'])
994
995        # Clean up anything in the workdir/sysroot/sstate cache
996        bitbake('%s -c cleansstate' % testrecipe)
997        # Try modifying a recipe
998        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
999        self.track_for_cleanup(tempdir)
1000        self.track_for_cleanup(self.workspacedir)
1001        self.add_command_to_tearDown('bitbake -c clean %s' % testrecipe)
1002        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
1003        result = runCmd('devtool modify %s -x %s' % (testrecipe, tempdir))
1004        self.assertExists(os.path.join(tempdir, 'Cargo.toml'), 'Extracted source could not be found')
1005        self.assertExists(os.path.join(self.workspacedir, 'conf', 'layer.conf'), 'Workspace directory not created. devtool output: %s' % result.output)
1006        matches = glob.glob(os.path.join(self.workspacedir, 'appends', '%s_*.bbappend' % testrecipe))
1007        self.assertTrue(matches, 'bbappend not created')
1008        # Test devtool status
1009        result = runCmd('devtool status')
1010        self.assertIn(testrecipe, result.output)
1011        self.assertIn(tempdir, result.output)
1012        # Check git repo
1013        self._check_src_repo(tempdir)
1014        # Check that the patch is correctly applied.
1015        # The last commit message in the tree must contain the following note:
1016        # Notes (devtool):
1017        #     original patch: <patchname>
1018        # ..
1019        patchname = None
1020        for uri in src_uri:
1021            if uri.startswith('file://') and '.patch' in uri:
1022                patchname = uri.replace("file://", "").partition('.patch')[0] + '.patch'
1023        self.assertIsNotNone(patchname)
1024        result = runCmd('git -C %s log -1' % tempdir)
1025        self.assertIn("Notes (devtool):\n    original patch: %s" % patchname, result.output)
1026
1027        # Configure the recipe to check that the git dependencies are correctly patched in cargo config
1028        bitbake('-c configure %s' % testrecipe)
1029
1030        cargo_config_path = os.path.join(cargo_home, 'config.toml')
1031        with open(cargo_config_path, "r") as f:
1032            cargo_config_contents = [line.strip('\n') for line in f.readlines()]
1033
1034        # Get back git dependencies of the recipe (ignoring the main one)
1035        # and check that they are all correctly patched to be fetched locally
1036        git_deps = [uri for uri in src_uri if uri.startswith("git://")][1:]
1037        for git_dep in git_deps:
1038            raw_url, _, raw_parms = git_dep.partition(";")
1039            parms = {}
1040            for parm in raw_parms.split(";"):
1041                name_parm, _, value_parm = parm.partition('=')
1042                parms[name_parm]=value_parm
1043            self.assertIn('protocol', parms, 'git dependencies uri should contain the "protocol" parameter')
1044            self.assertIn('name', parms, 'git dependencies uri should contain the "name" parameter')
1045            self.assertIn('destsuffix', parms, 'git dependencies uri should contain the "destsuffix" parameter')
1046            self.assertIn('type', parms, 'git dependencies uri should contain the "type" parameter')
1047            self.assertEqual(parms['type'], 'git-dependency', 'git dependencies uri should have "type=git-dependency"')
1048            raw_url = raw_url.replace("git://", '%s://' % parms['protocol'])
1049            patch_line = '[patch."%s"]' % raw_url
1050            path_patched = os.path.join(unpackdir, parms['destsuffix'])
1051            path_override_line = '%s = { path = "%s" }' % (parms['name'], path_patched)
1052            # Would have been better to use tomllib to read this file :/
1053            self.assertIn(patch_line, cargo_config_contents)
1054            self.assertIn(path_override_line, cargo_config_contents)
1055
1056        # Try to package the recipe
1057        bitbake('-c package_qa %s' % testrecipe)
1058
1059    def test_devtool_modify_localfiles(self):
1060        # Check preconditions
1061        testrecipe = 'lighttpd'
1062        src_uri = (get_bb_var('SRC_URI', testrecipe) or '').split()
1063        foundlocal = False
1064        for item in src_uri:
1065            if item.startswith('file://') and '.patch' not in item:
1066                foundlocal = True
1067                break
1068        self.assertTrue(foundlocal, 'This test expects the %s recipe to fetch local files and it seems that it no longer does' % testrecipe)
1069        # Clean up anything in the workdir/sysroot/sstate cache
1070        bitbake('%s -c cleansstate' % testrecipe)
1071        # Try modifying a recipe
1072        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
1073        self.track_for_cleanup(tempdir)
1074        self.track_for_cleanup(self.workspacedir)
1075        self.add_command_to_tearDown('bitbake -c clean %s' % testrecipe)
1076        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
1077        result = runCmd('devtool modify %s -x %s' % (testrecipe, tempdir))
1078        self.assertExists(os.path.join(tempdir, 'configure.ac'), 'Extracted source could not be found')
1079        self.assertExists(os.path.join(self.workspacedir, 'conf', 'layer.conf'), 'Workspace directory not created')
1080        matches = glob.glob(os.path.join(self.workspacedir, 'appends', '%s_*.bbappend' % testrecipe))
1081        self.assertTrue(matches, 'bbappend not created')
1082        # Test devtool status
1083        result = runCmd('devtool status')
1084        self.assertIn(testrecipe, result.output)
1085        self.assertIn(tempdir, result.output)
1086        # Try building
1087        bitbake(testrecipe)
1088
1089    def test_devtool_modify_virtual(self):
1090        # Try modifying a virtual recipe
1091        virtrecipe = 'virtual/make'
1092        realrecipe = 'make'
1093        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
1094        self.track_for_cleanup(tempdir)
1095        self.track_for_cleanup(self.workspacedir)
1096        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
1097        result = runCmd('devtool modify %s -x %s' % (virtrecipe, tempdir))
1098        self.assertExists(os.path.join(tempdir, 'Makefile.am'), 'Extracted source could not be found')
1099        self.assertExists(os.path.join(self.workspacedir, 'conf', 'layer.conf'), 'Workspace directory not created')
1100        matches = glob.glob(os.path.join(self.workspacedir, 'appends', '%s_*.bbappend' % realrecipe))
1101        self.assertTrue(matches, 'bbappend not created %s' % result.output)
1102        # Test devtool status
1103        result = runCmd('devtool status')
1104        self.assertNotIn(virtrecipe, result.output)
1105        self.assertIn(realrecipe, result.output)
1106        # Check git repo
1107        self._check_src_repo(tempdir)
1108        # This is probably sufficient
1109
1110    def test_devtool_modify_overrides(self):
1111        # Try modifying a recipe with patches in overrides
1112        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
1113        self.track_for_cleanup(tempdir)
1114        self.track_for_cleanup(self.workspacedir)
1115        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
1116        result = runCmd('devtool modify devtool-patch-overrides -x %s' % (tempdir))
1117
1118        self._check_src_repo(tempdir)
1119        source = os.path.join(tempdir, "source")
1120        def check(branch, expected):
1121            runCmd('git -C %s checkout %s' % (tempdir, branch))
1122            with open(source, "rt") as f:
1123                content = f.read()
1124            self.assertEqual(content, expected)
1125        if self.td["MACHINE"] == "qemux86":
1126            check('devtool', 'This is a test for qemux86\n')
1127        elif self.td["MACHINE"] == "qemuarm":
1128            check('devtool', 'This is a test for qemuarm\n')
1129        else:
1130            check('devtool', 'This is a test for something\n')
1131        check('devtool-no-overrides', 'This is a test for something\n')
1132        check('devtool-override-qemuarm', 'This is a test for qemuarm\n')
1133        check('devtool-override-qemux86', 'This is a test for qemux86\n')
1134
1135    def test_devtool_modify_multiple_sources(self):
1136        # This test check that recipes fetching several sources can be used with devtool modify/build
1137        # Check preconditions
1138        testrecipe = 'bzip2'
1139        src_uri = get_bb_var('SRC_URI', testrecipe)
1140        src1 = 'https://' in src_uri
1141        src2 = 'git://' in src_uri
1142        self.assertTrue(src1 and src2, 'This test expects the %s recipe to fetch both a git source and a tarball and it seems that it no longer does' % testrecipe)
1143        # Clean up anything in the workdir/sysroot/sstate cache
1144        bitbake('%s -c cleansstate' % testrecipe)
1145        # Try modifying a recipe
1146        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
1147        self.track_for_cleanup(tempdir)
1148        self.track_for_cleanup(self.workspacedir)
1149        self.add_command_to_tearDown('bitbake -c clean %s' % testrecipe)
1150        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
1151        result = runCmd('devtool modify %s -x %s' % (testrecipe, tempdir))
1152        self.assertEqual(result.status, 0, "Could not modify recipe %s. Output: %s" % (testrecipe, result.output))
1153        # Test devtool status
1154        result = runCmd('devtool status')
1155        self.assertIn(testrecipe, result.output)
1156        self.assertIn(tempdir, result.output)
1157        # Try building
1158        result = bitbake(testrecipe)
1159        self.assertEqual(result.status, 0, "Bitbake failed, exit code %s, output %s" % (result.status, result.output))
1160
1161class DevtoolUpdateTests(DevtoolBase):
1162
1163    def test_devtool_update_recipe(self):
1164        # Check preconditions
1165        testrecipe = 'minicom'
1166        bb_vars = get_bb_vars(['FILE', 'SRC_URI'], testrecipe)
1167        recipefile = bb_vars['FILE']
1168        src_uri = bb_vars['SRC_URI']
1169        self.assertNotIn('git://', src_uri, 'This test expects the %s recipe to NOT be a git recipe' % testrecipe)
1170        self._check_repo_status(os.path.dirname(recipefile), [])
1171        # First, modify a recipe
1172        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
1173        self.track_for_cleanup(tempdir)
1174        self.track_for_cleanup(self.workspacedir)
1175        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
1176        # (don't bother with cleaning the recipe on teardown, we won't be building it)
1177        # We don't use -x here so that we test the behaviour of devtool modify without it
1178        result = runCmd('devtool modify %s %s' % (testrecipe, tempdir))
1179        # Check git repo
1180        self._check_src_repo(tempdir)
1181        # Add a couple of commits
1182        # FIXME: this only tests adding, need to also test update and remove
1183        result = runCmd('echo "Additional line" >> README', cwd=tempdir)
1184        result = runCmd('git commit -a -m "Change the README"', cwd=tempdir)
1185        result = runCmd('echo "A new file" > devtool-new-file', cwd=tempdir)
1186        result = runCmd('git add devtool-new-file', cwd=tempdir)
1187        result = runCmd('git commit -m "Add a new file"', cwd=tempdir)
1188        cleanup_cmd = 'cd %s; rm %s/*.patch; git add %s; git checkout %s' % (os.path.dirname(recipefile), testrecipe, testrecipe, os.path.basename(recipefile))
1189        self.add_command_to_tearDown(cleanup_cmd)
1190        result = runCmd('devtool update-recipe %s' % testrecipe)
1191        result = runCmd('git add minicom', cwd=os.path.dirname(recipefile))
1192        expected_status = [(' M', '.*/%s$' % os.path.basename(recipefile)),
1193                           ('A ', '.*/0001-Change-the-README.patch$'),
1194                           ('A ', '.*/0002-Add-a-new-file.patch$')]
1195        self._check_repo_status(os.path.dirname(recipefile), expected_status)
1196        result = runCmd(cleanup_cmd)
1197        self._check_repo_status(os.path.dirname(recipefile), [])
1198
1199    def test_devtool_update_recipe_git(self):
1200        # Check preconditions
1201        testrecipe = 'mtd-utils-selftest'
1202        bb_vars = get_bb_vars(['FILE', 'SRC_URI'], testrecipe)
1203        recipefile = bb_vars['FILE']
1204        src_uri = bb_vars['SRC_URI']
1205        self.assertIn('git://', src_uri, 'This test expects the %s recipe to be a git recipe' % testrecipe)
1206        patches = []
1207        for entry in src_uri.split():
1208            if entry.startswith('file://') and entry.endswith('.patch'):
1209                patches.append(entry[7:].split(';')[0])
1210        self.assertGreater(len(patches), 0, 'The %s recipe does not appear to contain any patches, so this test will not be effective' % testrecipe)
1211        self._check_repo_status(os.path.dirname(recipefile), [])
1212        # First, modify a recipe
1213        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
1214        self.track_for_cleanup(tempdir)
1215        self.track_for_cleanup(self.workspacedir)
1216        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
1217        # (don't bother with cleaning the recipe on teardown, we won't be building it)
1218        result = runCmd('devtool modify %s -x %s' % (testrecipe, tempdir))
1219        # Check git repo
1220        self._check_src_repo(tempdir)
1221        # Add a couple of commits
1222        # FIXME: this only tests adding, need to also test update and remove
1223        result = runCmd('echo "# Additional line" >> Makefile.am', cwd=tempdir)
1224        result = runCmd('git commit -a -m "Change the Makefile"', cwd=tempdir)
1225        result = runCmd('echo "A new file" > devtool-new-file', cwd=tempdir)
1226        result = runCmd('git add devtool-new-file', cwd=tempdir)
1227        result = runCmd('git commit -m "Add a new file"', cwd=tempdir)
1228        self.add_command_to_tearDown('cd %s; rm -rf %s; git checkout %s %s' % (os.path.dirname(recipefile), testrecipe, testrecipe, os.path.basename(recipefile)))
1229        result = runCmd('devtool update-recipe -m srcrev %s' % testrecipe)
1230        expected_status = [(' M', '.*/%s$' % os.path.basename(recipefile))] + \
1231                          [(' D', '.*/%s$' % patch) for patch in patches]
1232        self._check_repo_status(os.path.dirname(recipefile), expected_status)
1233
1234        result = runCmd('git diff %s' % os.path.basename(recipefile), cwd=os.path.dirname(recipefile))
1235        addlines = ['SRCREV = ".*"', 'SRC_URI = "git://git.infradead.org/mtd-utils.git;branch=master"']
1236        srcurilines = src_uri.split()
1237        srcurilines[0] = 'SRC_URI = "' + srcurilines[0]
1238        srcurilines.append('"')
1239        removelines = ['SRCREV = ".*"'] + srcurilines
1240        self._check_diff(result.output, addlines, removelines)
1241        # Now try with auto mode
1242        runCmd('cd %s; git checkout %s %s' % (os.path.dirname(recipefile), testrecipe, os.path.basename(recipefile)))
1243        result = runCmd('devtool update-recipe %s' % testrecipe)
1244        result = runCmd('git rev-parse --show-toplevel', cwd=os.path.dirname(recipefile))
1245        topleveldir = result.output.strip()
1246        relpatchpath = os.path.join(os.path.relpath(os.path.dirname(recipefile), topleveldir), testrecipe)
1247        expected_status = [(' M', os.path.relpath(recipefile, topleveldir)),
1248                           ('??', '%s/0001-Change-the-Makefile.patch' % relpatchpath),
1249                           ('??', '%s/0002-Add-a-new-file.patch' % relpatchpath)]
1250        self._check_repo_status(os.path.dirname(recipefile), expected_status)
1251
1252    def test_devtool_update_recipe_append(self):
1253        # Check preconditions
1254        testrecipe = 'minicom'
1255        bb_vars = get_bb_vars(['FILE', 'SRC_URI'], testrecipe)
1256        recipefile = bb_vars['FILE']
1257        src_uri = bb_vars['SRC_URI']
1258        self.assertNotIn('git://', src_uri, 'This test expects the %s recipe to NOT be a git recipe' % testrecipe)
1259        self._check_repo_status(os.path.dirname(recipefile), [])
1260        # First, modify a recipe
1261        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
1262        tempsrcdir = os.path.join(tempdir, 'source')
1263        templayerdir = os.path.join(tempdir, 'layer')
1264        self.track_for_cleanup(tempdir)
1265        self.track_for_cleanup(self.workspacedir)
1266        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
1267        # (don't bother with cleaning the recipe on teardown, we won't be building it)
1268        result = runCmd('devtool modify %s -x %s' % (testrecipe, tempsrcdir))
1269        # Check git repo
1270        self._check_src_repo(tempsrcdir)
1271        # Add a commit
1272        result = runCmd('echo "Additional line" >> README', cwd=tempsrcdir)
1273        result = runCmd('git commit -a -m "Add our custom version"', cwd=tempsrcdir)
1274        self.add_command_to_tearDown('cd %s; rm -f %s/*.patch; git checkout .' % (os.path.dirname(recipefile), testrecipe))
1275        # Create a temporary layer and add it to bblayers.conf
1276        self._create_temp_layer(templayerdir, True, 'selftestupdaterecipe')
1277        # Create the bbappend
1278        result = runCmd('devtool update-recipe %s -a %s' % (testrecipe, templayerdir))
1279        self.assertNotIn('WARNING:', result.output)
1280        # Check recipe is still clean
1281        self._check_repo_status(os.path.dirname(recipefile), [])
1282        # Check bbappend was created
1283        splitpath = os.path.dirname(recipefile).split(os.sep)
1284        appenddir = os.path.join(templayerdir, splitpath[-2], splitpath[-1])
1285        bbappendfile = self._check_bbappend(testrecipe, recipefile, appenddir)
1286        patchfile = os.path.join(appenddir, testrecipe, '0001-Add-our-custom-version.patch')
1287        self.assertExists(patchfile, 'Patch file not created')
1288
1289        # Check bbappend contents
1290        expectedlines = ['FILESEXTRAPATHS:prepend := "${THISDIR}/${PN}:"\n',
1291                         '\n',
1292                         'SRC_URI += "file://0001-Add-our-custom-version.patch"\n',
1293                         '\n']
1294        with open(bbappendfile, 'r') as f:
1295            self.assertEqual(expectedlines, f.readlines())
1296
1297        # Check we can run it again and bbappend isn't modified
1298        result = runCmd('devtool update-recipe %s -a %s' % (testrecipe, templayerdir))
1299        with open(bbappendfile, 'r') as f:
1300            self.assertEqual(expectedlines, f.readlines())
1301        # Drop new commit and check patch gets deleted
1302        result = runCmd('git reset HEAD^ --hard', cwd=tempsrcdir)
1303        result = runCmd('devtool update-recipe %s -a %s' % (testrecipe, templayerdir))
1304        self.assertNotExists(patchfile, 'Patch file not deleted')
1305        expectedlines2 = ['FILESEXTRAPATHS:prepend := "${THISDIR}/${PN}:"\n',
1306                         '\n']
1307        with open(bbappendfile, 'r') as f:
1308            self.assertEqual(expectedlines2, f.readlines())
1309        # Put commit back and check we can run it if layer isn't in bblayers.conf
1310        os.remove(bbappendfile)
1311        result = runCmd('echo "Additional line" >> README', cwd=tempsrcdir)
1312        result = runCmd('git commit -a -m "Add our custom version"', cwd=tempsrcdir)
1313        result = runCmd('bitbake-layers remove-layer %s' % templayerdir, cwd=self.builddir)
1314        result = runCmd('devtool update-recipe %s -a %s' % (testrecipe, templayerdir))
1315        self.assertIn('WARNING: Specified layer is not currently enabled in bblayers.conf', result.output)
1316        self.assertExists(patchfile, 'Patch file not created (with disabled layer)')
1317        with open(bbappendfile, 'r') as f:
1318            self.assertEqual(expectedlines, f.readlines())
1319        # Deleting isn't expected to work under these circumstances
1320
1321    def test_devtool_update_recipe_append_git(self):
1322        # Check preconditions
1323        testrecipe = 'mtd-utils-selftest'
1324        bb_vars = get_bb_vars(['FILE', 'SRC_URI', 'LAYERSERIES_CORENAMES'], testrecipe)
1325        recipefile = bb_vars['FILE']
1326        src_uri = bb_vars['SRC_URI']
1327        corenames = bb_vars['LAYERSERIES_CORENAMES']
1328        self.assertIn('git://', src_uri, 'This test expects the %s recipe to be a git recipe' % testrecipe)
1329        for entry in src_uri.split():
1330            if entry.startswith('git://'):
1331                git_uri = entry
1332                break
1333        self._check_repo_status(os.path.dirname(recipefile), [])
1334        # First, modify a recipe
1335        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
1336        tempsrcdir = os.path.join(tempdir, 'source')
1337        templayerdir = os.path.join(tempdir, 'layer')
1338        self.track_for_cleanup(tempdir)
1339        self.track_for_cleanup(self.workspacedir)
1340        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
1341        # (don't bother with cleaning the recipe on teardown, we won't be building it)
1342        result = runCmd('devtool modify %s -x %s' % (testrecipe, tempsrcdir))
1343        # Check git repo
1344        self._check_src_repo(tempsrcdir)
1345        # Add a commit
1346        result = runCmd('echo "# Additional line" >> Makefile.am', cwd=tempsrcdir)
1347        result = runCmd('git commit -a -m "Change the Makefile"', cwd=tempsrcdir)
1348        self.add_command_to_tearDown('cd %s; rm -f %s/*.patch; git checkout .' % (os.path.dirname(recipefile), testrecipe))
1349        # Create a temporary layer
1350        os.makedirs(os.path.join(templayerdir, 'conf'))
1351        with open(os.path.join(templayerdir, 'conf', 'layer.conf'), 'w') as f:
1352            f.write('BBPATH .= ":${LAYERDIR}"\n')
1353            f.write('BBFILES += "${LAYERDIR}/recipes-*/*/*.bbappend"\n')
1354            f.write('BBFILE_COLLECTIONS += "oeselftesttemplayer"\n')
1355            f.write('BBFILE_PATTERN_oeselftesttemplayer = "^${LAYERDIR}/"\n')
1356            f.write('BBFILE_PRIORITY_oeselftesttemplayer = "999"\n')
1357            f.write('BBFILE_PATTERN_IGNORE_EMPTY_oeselftesttemplayer = "1"\n')
1358            f.write('LAYERSERIES_COMPAT_oeselftesttemplayer = "%s"\n' % corenames)
1359        self.add_command_to_tearDown('bitbake-layers remove-layer %s || true' % templayerdir)
1360        result = runCmd('bitbake-layers add-layer %s' % templayerdir, cwd=self.builddir)
1361        # Create the bbappend
1362        result = runCmd('devtool update-recipe -m srcrev %s -a %s' % (testrecipe, templayerdir))
1363        self.assertNotIn('WARNING:', result.output)
1364        # Check recipe is still clean
1365        self._check_repo_status(os.path.dirname(recipefile), [])
1366        # Check bbappend was created
1367        splitpath = os.path.dirname(recipefile).split(os.sep)
1368        appenddir = os.path.join(templayerdir, splitpath[-2], splitpath[-1])
1369        bbappendfile = self._check_bbappend(testrecipe, recipefile, appenddir)
1370        self.assertNotExists(os.path.join(appenddir, testrecipe), 'Patch directory should not be created')
1371
1372        # Check bbappend contents
1373        result = runCmd('git rev-parse HEAD', cwd=tempsrcdir)
1374        expectedlines = set(['SRCREV = "%s"\n' % result.output,
1375                             '\n',
1376                             'SRC_URI = "%s"\n' % git_uri,
1377                             '\n'])
1378        with open(bbappendfile, 'r') as f:
1379            self.assertEqual(expectedlines, set(f.readlines()))
1380
1381        # Check we can run it again and bbappend isn't modified
1382        result = runCmd('devtool update-recipe -m srcrev %s -a %s' % (testrecipe, templayerdir))
1383        with open(bbappendfile, 'r') as f:
1384            self.assertEqual(expectedlines, set(f.readlines()))
1385        # Drop new commit and check SRCREV changes
1386        result = runCmd('git reset HEAD^ --hard', cwd=tempsrcdir)
1387        result = runCmd('devtool update-recipe -m srcrev %s -a %s' % (testrecipe, templayerdir))
1388        self.assertNotExists(os.path.join(appenddir, testrecipe), 'Patch directory should not be created')
1389        result = runCmd('git rev-parse HEAD', cwd=tempsrcdir)
1390        expectedlines = set(['SRCREV = "%s"\n' % result.output,
1391                             '\n',
1392                             'SRC_URI = "%s"\n' % git_uri,
1393                             '\n'])
1394        with open(bbappendfile, 'r') as f:
1395            self.assertEqual(expectedlines, set(f.readlines()))
1396        # Put commit back and check we can run it if layer isn't in bblayers.conf
1397        os.remove(bbappendfile)
1398        result = runCmd('echo "# Additional line" >> Makefile.am', cwd=tempsrcdir)
1399        result = runCmd('git commit -a -m "Change the Makefile"', cwd=tempsrcdir)
1400        result = runCmd('bitbake-layers remove-layer %s' % templayerdir, cwd=self.builddir)
1401        result = runCmd('devtool update-recipe -m srcrev %s -a %s' % (testrecipe, templayerdir))
1402        self.assertIn('WARNING: Specified layer is not currently enabled in bblayers.conf', result.output)
1403        self.assertNotExists(os.path.join(appenddir, testrecipe), 'Patch directory should not be created')
1404        result = runCmd('git rev-parse HEAD', cwd=tempsrcdir)
1405        expectedlines = set(['SRCREV = "%s"\n' % result.output,
1406                             '\n',
1407                             'SRC_URI = "%s"\n' % git_uri,
1408                             '\n'])
1409        with open(bbappendfile, 'r') as f:
1410            self.assertEqual(expectedlines, set(f.readlines()))
1411        # Deleting isn't expected to work under these circumstances
1412
1413    def test_devtool_update_recipe_local_files(self):
1414        """Check that local source files are copied over instead of patched"""
1415        testrecipe = 'makedevs'
1416        recipefile = get_bb_var('FILE', testrecipe)
1417        # Setup srctree for modifying the recipe
1418        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
1419        self.track_for_cleanup(tempdir)
1420        self.track_for_cleanup(self.workspacedir)
1421        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
1422        # (don't bother with cleaning the recipe on teardown, we won't be
1423        # building it)
1424        result = runCmd('devtool modify %s -x %s' % (testrecipe, tempdir))
1425        # Check git repo
1426        self._check_src_repo(tempdir)
1427        # Try building just to ensure we haven't broken that
1428        bitbake("%s" % testrecipe)
1429        # Edit / commit local source
1430        runCmd('echo "/* Foobar */" >> makedevs.c', cwd=tempdir)
1431        runCmd('echo "Foo" > new-local', cwd=tempdir)
1432        runCmd('echo "Bar" > new-file', cwd=tempdir)
1433        runCmd('git add new-file', cwd=tempdir)
1434        runCmd('git commit -m "Add new file"', cwd=tempdir)
1435        runCmd('git add new-local', cwd=tempdir)
1436        runCmd('devtool update-recipe %s' % testrecipe)
1437        expected_status = [(' M', '.*/%s$' % os.path.basename(recipefile)),
1438                           (' M', '.*/makedevs/makedevs.c$'),
1439                           ('??', '.*/makedevs/new-local$'),
1440                           ('??', '.*/makedevs/0001-Add-new-file.patch$')]
1441        self._check_repo_status(os.path.dirname(recipefile), expected_status)
1442        # Now try to update recipe in another layer, so first, clean it
1443        runCmd('cd %s; git clean -fd .; git checkout .' % os.path.dirname(recipefile))
1444        # Create a temporary layer and add it to bblayers.conf
1445        self._create_temp_layer(templayerdir, True, 'templayer')
1446        # Update recipe in templayer
1447        result = runCmd('devtool update-recipe %s -a %s' % (testrecipe, templayerdir))
1448        self.assertNotIn('WARNING:', result.output)
1449        # Check recipe is still clean
1450        self._check_repo_status(os.path.dirname(recipefile), [])
1451        splitpath = os.path.dirname(recipefile).split(os.sep)
1452        appenddir = os.path.join(templayerdir, splitpath[-2], splitpath[-1])
1453        bbappendfile = self._check_bbappend(testrecipe, recipefile, appenddir)
1454        patchfile = os.path.join(appenddir, testrecipe, '0001-Add-new-file.patch')
1455        new_local_file = os.path.join(appenddir, testrecipe, 'new_local')
1456        local_file = os.path.join(appenddir, testrecipe, 'makedevs.c')
1457        self.assertExists(patchfile, 'Patch file 0001-Add-new-file.patch not created')
1458        self.assertExists(local_file, 'File makedevs.c not created')
1459        self.assertExists(patchfile, 'File new_local not created')
1460
1461    def _test_devtool_update_recipe_local_files_2(self):
1462        """Check local source files support when editing local files in Git"""
1463        testrecipe = 'devtool-test-local'
1464        recipefile = get_bb_var('FILE', testrecipe)
1465        recipedir = os.path.dirname(recipefile)
1466        result = runCmd('git status --porcelain .', cwd=recipedir)
1467        if result.output.strip():
1468            self.fail('Recipe directory for %s contains uncommitted changes' % testrecipe)
1469        # Setup srctree for modifying the recipe
1470        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
1471        self.track_for_cleanup(tempdir)
1472        self.track_for_cleanup(self.workspacedir)
1473        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
1474        result = runCmd('devtool modify %s -x %s' % (testrecipe, tempdir))
1475        # Check git repo
1476        self._check_src_repo(tempdir)
1477        # Edit / commit local sources
1478        runCmd('echo "# Foobar" >> file1', cwd=tempdir)
1479        runCmd('git commit -am "Edit existing file"', cwd=tempdir)
1480        runCmd('git rm file2', cwd=tempdir)
1481        runCmd('git commit -m"Remove file"', cwd=tempdir)
1482        runCmd('echo "Foo" > new-local', cwd=tempdir)
1483        runCmd('git add new-local', cwd=tempdir)
1484        runCmd('git commit -m "Add new local file"', cwd=tempdir)
1485        runCmd('echo "Gar" > new-file', cwd=tempdir)
1486        runCmd('git add new-file', cwd=tempdir)
1487        runCmd('git commit -m "Add new file"', cwd=tempdir)
1488        self.add_command_to_tearDown('cd %s; git clean -fd .; git checkout .' %
1489                                     os.path.dirname(recipefile))
1490        # Checkout unmodified file to working copy -> devtool should still pick
1491        # the modified version from HEAD
1492        runCmd('git checkout HEAD^ -- file1', cwd=tempdir)
1493        runCmd('devtool update-recipe %s' % testrecipe)
1494        expected_status = [(' M', '.*/%s$' % os.path.basename(recipefile)),
1495                           (' M', '.*/file1$'),
1496                           (' D', '.*/file2$'),
1497                           ('??', '.*/new-local$'),
1498                           ('??', '.*/0001-Add-new-file.patch$')]
1499        self._check_repo_status(os.path.dirname(recipefile), expected_status)
1500
1501    def test_devtool_update_recipe_with_gitignore(self):
1502        # First, modify the recipe
1503        testrecipe = 'devtool-test-ignored'
1504        bb_vars = get_bb_vars(['FILE'], testrecipe)
1505        recipefile = bb_vars['FILE']
1506        patchfile = os.path.join(os.path.dirname(recipefile), testrecipe, testrecipe + '.patch')
1507        newpatchfile = os.path.join(os.path.dirname(recipefile), testrecipe, testrecipe + '.patch.expected')
1508        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
1509        self.track_for_cleanup(tempdir)
1510        self.track_for_cleanup(self.workspacedir)
1511        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
1512        # (don't bother with cleaning the recipe on teardown, we won't be building it)
1513        result = runCmd('devtool modify %s' % testrecipe)
1514        self.add_command_to_tearDown('cd %s; rm %s/*; git checkout %s %s' % (os.path.dirname(recipefile), testrecipe, testrecipe, os.path.basename(recipefile)))
1515        result = runCmd('devtool finish --force-patch-refresh %s meta-selftest' % testrecipe)
1516        # Check recipe got changed as expected
1517        with open(newpatchfile, 'r') as f:
1518            desiredlines = f.readlines()
1519        with open(patchfile, 'r') as f:
1520            newlines = f.readlines()
1521        # Ignore the initial lines, because oe-selftest creates own meta-selftest repo
1522        # which changes the metadata subject which is added into the patch, but keep
1523        # .patch.expected as it is in case someone runs devtool finish --force-patch-refresh
1524        # devtool-test-ignored manually, then it should generate exactly the same .patch file
1525        self.assertEqual(desiredlines[5:], newlines[5:])
1526
1527    def test_devtool_update_recipe_long_filename(self):
1528        # First, modify the recipe
1529        testrecipe = 'devtool-test-long-filename'
1530        bb_vars = get_bb_vars(['FILE'], testrecipe)
1531        recipefile = bb_vars['FILE']
1532        patchfilename = '0001-I-ll-patch-you-only-if-devtool-lets-me-to-do-it-corr.patch'
1533        patchfile = os.path.join(os.path.dirname(recipefile), testrecipe, patchfilename)
1534        newpatchfile = os.path.join(os.path.dirname(recipefile), testrecipe, patchfilename + '.expected')
1535        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
1536        self.track_for_cleanup(tempdir)
1537        self.track_for_cleanup(self.workspacedir)
1538        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
1539        # (don't bother with cleaning the recipe on teardown, we won't be building it)
1540        result = runCmd('devtool modify %s' % testrecipe)
1541        self.add_command_to_tearDown('cd %s; rm %s/*; git checkout %s %s' % (os.path.dirname(recipefile), testrecipe, testrecipe, os.path.basename(recipefile)))
1542        result = runCmd('devtool finish --force-patch-refresh %s meta-selftest' % testrecipe)
1543        # Check recipe got changed as expected
1544        with open(newpatchfile, 'r') as f:
1545            desiredlines = f.readlines()
1546        with open(patchfile, 'r') as f:
1547            newlines = f.readlines()
1548        # Ignore the initial lines, because oe-selftest creates own meta-selftest repo
1549        # which changes the metadata subject which is added into the patch, but keep
1550        # .patch.expected as it is in case someone runs devtool finish --force-patch-refresh
1551        # devtool-test-ignored manually, then it should generate exactly the same .patch file
1552        self.assertEqual(desiredlines[5:], newlines[5:])
1553
1554    def test_devtool_update_recipe_local_files_3(self):
1555        # First, modify the recipe
1556        testrecipe = 'devtool-test-localonly'
1557        bb_vars = get_bb_vars(['FILE', 'SRC_URI'], testrecipe)
1558        recipefile = bb_vars['FILE']
1559        src_uri = bb_vars['SRC_URI']
1560        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
1561        self.track_for_cleanup(tempdir)
1562        self.track_for_cleanup(self.workspacedir)
1563        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
1564        # (don't bother with cleaning the recipe on teardown, we won't be building it)
1565        result = runCmd('devtool modify %s' % testrecipe)
1566        # Modify one file
1567        runCmd('echo "Another line" >> file2', cwd=os.path.join(self.workspacedir, 'sources', testrecipe))
1568        self.add_command_to_tearDown('cd %s; rm %s/*; git checkout %s %s' % (os.path.dirname(recipefile), testrecipe, testrecipe, os.path.basename(recipefile)))
1569        result = runCmd('devtool update-recipe %s' % testrecipe)
1570        expected_status = [(' M', '.*/%s/file2$' % testrecipe)]
1571        self._check_repo_status(os.path.dirname(recipefile), expected_status)
1572
1573    def test_devtool_update_recipe_local_patch_gz(self):
1574        # First, modify the recipe
1575        testrecipe = 'devtool-test-patch-gz'
1576        if get_bb_var('DISTRO') == 'poky-tiny':
1577            self.skipTest("The DISTRO 'poky-tiny' does not provide the dependencies needed by %s" % testrecipe)
1578        bb_vars = get_bb_vars(['FILE', 'SRC_URI'], testrecipe)
1579        recipefile = bb_vars['FILE']
1580        src_uri = bb_vars['SRC_URI']
1581        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
1582        self.track_for_cleanup(tempdir)
1583        self.track_for_cleanup(self.workspacedir)
1584        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
1585        # (don't bother with cleaning the recipe on teardown, we won't be building it)
1586        result = runCmd('devtool modify %s' % testrecipe)
1587        # Modify one file
1588        srctree = os.path.join(self.workspacedir, 'sources', testrecipe)
1589        runCmd('echo "Another line" >> README', cwd=srctree)
1590        runCmd('git commit -a --amend --no-edit --no-verify', cwd=srctree)
1591        self.add_command_to_tearDown('cd %s; rm %s/*; git checkout %s %s' % (os.path.dirname(recipefile), testrecipe, testrecipe, os.path.basename(recipefile)))
1592        result = runCmd('devtool update-recipe %s' % testrecipe)
1593        expected_status = [(' M', '.*/%s/readme.patch.gz$' % testrecipe)]
1594        self._check_repo_status(os.path.dirname(recipefile), expected_status)
1595        patch_gz = os.path.join(os.path.dirname(recipefile), testrecipe, 'readme.patch.gz')
1596        result = runCmd('file %s' % patch_gz)
1597        if 'gzip compressed data' not in result.output:
1598            self.fail('New patch file is not gzipped - file reports:\n%s' % result.output)
1599
1600    def test_devtool_update_recipe_local_files_subdir(self):
1601        # Try devtool update-recipe on a recipe that has a file with subdir= set in
1602        # SRC_URI such that it overwrites a file that was in an archive that
1603        # was also in SRC_URI
1604        # First, modify the recipe
1605        testrecipe = 'devtool-test-subdir'
1606        bb_vars = get_bb_vars(['FILE', 'SRC_URI'], testrecipe)
1607        recipefile = bb_vars['FILE']
1608        src_uri = bb_vars['SRC_URI']
1609        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
1610        self.track_for_cleanup(tempdir)
1611        self.track_for_cleanup(self.workspacedir)
1612        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
1613        # (don't bother with cleaning the recipe on teardown, we won't be building it)
1614        result = runCmd('devtool modify %s' % testrecipe)
1615        testfile = os.path.join(self.workspacedir, 'sources', testrecipe, 'testfile')
1616        self.assertExists(testfile, 'Extracted source could not be found')
1617        with open(testfile, 'r') as f:
1618            contents = f.read().rstrip()
1619        self.assertEqual(contents, 'Modified version', 'File has apparently not been overwritten as it should have been')
1620        # Test devtool update-recipe without modifying any files
1621        self.add_command_to_tearDown('cd %s; rm %s/*; git checkout %s %s' % (os.path.dirname(recipefile), testrecipe, testrecipe, os.path.basename(recipefile)))
1622        result = runCmd('devtool update-recipe %s' % testrecipe)
1623        expected_status = []
1624        self._check_repo_status(os.path.dirname(recipefile), expected_status)
1625
1626    def test_devtool_finish_modify_git_subdir(self):
1627        # Check preconditions
1628        testrecipe = 'dos2unix'
1629        self.append_config('ERROR_QA:remove:pn-dos2unix = "patch-status"\n')
1630        bb_vars = get_bb_vars(['SRC_URI', 'S', 'WORKDIR', 'FILE'], testrecipe)
1631        self.assertIn('git://', bb_vars['SRC_URI'], 'This test expects the %s recipe to be a git recipe' % testrecipe)
1632        workdir_git = '%s/git/' % bb_vars['WORKDIR']
1633        if not bb_vars['S'].startswith(workdir_git):
1634            self.fail('This test expects the %s recipe to be building from a subdirectory of the git repo' % testrecipe)
1635        subdir = bb_vars['S'].split(workdir_git, 1)[1]
1636        # Clean up anything in the workdir/sysroot/sstate cache
1637        bitbake('%s -c cleansstate' % testrecipe)
1638        # Try modifying a recipe
1639        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
1640        self.track_for_cleanup(tempdir)
1641        self.track_for_cleanup(self.workspacedir)
1642        self.add_command_to_tearDown('bitbake -c clean %s' % testrecipe)
1643        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
1644        result = runCmd('devtool modify %s -x %s' % (testrecipe, tempdir))
1645        testsrcfile = os.path.join(tempdir, subdir, 'dos2unix.c')
1646        self.assertExists(testsrcfile, 'Extracted source could not be found')
1647        self.assertExists(os.path.join(self.workspacedir, 'conf', 'layer.conf'), 'Workspace directory not created. devtool output: %s' % result.output)
1648        self.assertNotExists(os.path.join(tempdir, subdir, '.git'), 'Subdirectory has been initialised as a git repo')
1649        # Check git repo
1650        self._check_src_repo(tempdir)
1651        # Modify file
1652        runCmd("sed -i '1s:^:/* Add a comment */\\n:' %s" % testsrcfile)
1653        result = runCmd('git commit -a -m "Add a comment"', cwd=tempdir)
1654        # Now try updating original recipe
1655        recipefile = bb_vars['FILE']
1656        recipedir = os.path.dirname(recipefile)
1657        self.add_command_to_tearDown('cd %s; rm -f %s/*.patch; git checkout .' % (recipedir, testrecipe))
1658        result = runCmd('devtool update-recipe %s' % testrecipe)
1659        expected_status = [(' M', '.*/%s$' % os.path.basename(recipefile)),
1660                           ('??', '.*/%s/%s/$' % (testrecipe, testrecipe))]
1661        self._check_repo_status(os.path.dirname(recipefile), expected_status)
1662        result = runCmd('git diff %s' % os.path.basename(recipefile), cwd=os.path.dirname(recipefile))
1663        removelines = ['SRC_URI = "git://.*"']
1664        addlines = [
1665            'SRC_URI = "git://.* \\\\',
1666            'file://0001-Add-a-comment.patch;patchdir=.. \\\\',
1667            '"'
1668        ]
1669        self._check_diff(result.output, addlines, removelines)
1670        # Put things back so we can run devtool finish on a different layer
1671        runCmd('cd %s; rm -f %s/*.patch; git checkout .' % (recipedir, testrecipe))
1672        # Run devtool finish
1673        res = re.search('recipes-.*', recipedir)
1674        self.assertTrue(res, 'Unable to find recipe subdirectory')
1675        recipesubdir = res[0]
1676        self.add_command_to_tearDown('rm -rf %s' % os.path.join(self.testlayer_path, recipesubdir))
1677        result = runCmd('devtool finish %s meta-selftest' % testrecipe)
1678        # Check bbappend file contents
1679        appendfn = os.path.join(self.testlayer_path, recipesubdir, '%s_%%.bbappend' % testrecipe)
1680        with open(appendfn, 'r') as f:
1681            appendlines = f.readlines()
1682        expected_appendlines = [
1683            'FILESEXTRAPATHS:prepend := "${THISDIR}/${PN}:"\n',
1684            '\n',
1685            'SRC_URI += "file://0001-Add-a-comment.patch;patchdir=.."\n',
1686            '\n'
1687        ]
1688        self.assertEqual(appendlines, expected_appendlines)
1689        self.assertExists(os.path.join(os.path.dirname(appendfn), testrecipe, '0001-Add-a-comment.patch'))
1690        # Try building
1691        bitbake('%s -c patch' % testrecipe)
1692
1693    def test_devtool_git_submodules(self):
1694        # This tests if we can add a patch in a git submodule and extract it properly using devtool finish
1695        # Check preconditions
1696        self.assertTrue(not os.path.exists(self.workspacedir), 'This test cannot be run with a workspace directory under the build directory')
1697        self.track_for_cleanup(self.workspacedir)
1698        recipe = 'vulkan-samples'
1699        src_uri = get_bb_var('SRC_URI', recipe)
1700        self.assertIn('gitsm://', src_uri, 'This test expects the %s recipe to be a git recipe with submodules' % recipe)
1701        oldrecipefile = get_bb_var('FILE', recipe)
1702        recipedir = os.path.dirname(oldrecipefile)
1703        result = runCmd('git status --porcelain .', cwd=recipedir)
1704        if result.output.strip():
1705            self.fail('Recipe directory for %s contains uncommitted changes' % recipe)
1706        self.assertIn('/meta/', recipedir)
1707        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
1708        self.track_for_cleanup(tempdir)
1709        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
1710        result = runCmd('devtool modify %s %s' % (recipe, tempdir))
1711        self.assertExists(os.path.join(tempdir, 'CMakeLists.txt'), 'Extracted source could not be found')
1712        # Test devtool status
1713        result = runCmd('devtool status')
1714        self.assertIn(recipe, result.output)
1715        self.assertIn(tempdir, result.output)
1716        # Modify a source file in a submodule, (grab the first one)
1717        result = runCmd('git submodule --quiet foreach \'echo $sm_path\'', cwd=tempdir)
1718        submodule = result.output.splitlines()[0]
1719        submodule_path = os.path.join(tempdir, submodule)
1720        runCmd('echo "#This is a first comment" >> testfile', cwd=submodule_path)
1721        result = runCmd('git status --porcelain . ', cwd=submodule_path)
1722        self.assertIn("testfile", result.output)
1723        runCmd('git add testfile; git commit -m "Adding a new file"', cwd=submodule_path)
1724
1725        # Try finish to the original layer
1726        self.add_command_to_tearDown('rm -rf %s ; cd %s ; git checkout %s' % (recipedir, os.path.dirname(recipedir), recipedir))
1727        runCmd('devtool finish -f %s meta' % recipe)
1728        result = runCmd('devtool status')
1729        self.assertNotIn(recipe, result.output, 'Recipe should have been reset by finish but wasn\'t')
1730        self.assertNotExists(os.path.join(self.workspacedir, 'recipes', recipe), 'Recipe directory should not exist after finish')
1731        expected_status = [(' M', '.*/%s$' % os.path.basename(oldrecipefile)),
1732                           ('??', '.*/.*-Adding-a-new-file.patch$')]
1733        self._check_repo_status(recipedir, expected_status)
1734        # Make sure the patch is added to the recipe with the correct "patchdir" option
1735        result = runCmd('git diff .', cwd=recipedir)
1736        addlines = [
1737           'file://0001-Adding-a-new-file.patch;patchdir=%s \\\\' % submodule
1738        ]
1739        self._check_diff(result.output, addlines, [])
1740
1741class DevtoolExtractTests(DevtoolBase):
1742
1743    def test_devtool_extract(self):
1744        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
1745        # Try devtool extract
1746        self.track_for_cleanup(tempdir)
1747        self.track_for_cleanup(self.workspacedir)
1748        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
1749        result = runCmd('devtool extract matchbox-terminal %s' % tempdir)
1750        self.assertExists(os.path.join(tempdir, 'Makefile.am'), 'Extracted source could not be found')
1751        self._check_src_repo(tempdir)
1752
1753    def test_devtool_extract_virtual(self):
1754        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
1755        # Try devtool extract
1756        self.track_for_cleanup(tempdir)
1757        self.track_for_cleanup(self.workspacedir)
1758        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
1759        result = runCmd('devtool extract virtual/make %s' % tempdir)
1760        self.assertExists(os.path.join(tempdir, 'Makefile.am'), 'Extracted source could not be found')
1761        self._check_src_repo(tempdir)
1762
1763class DevtoolResetTests(DevtoolBase):
1764
1765    def test_devtool_reset_all(self):
1766        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
1767        self.track_for_cleanup(tempdir)
1768        self.track_for_cleanup(self.workspacedir)
1769        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
1770        testrecipe1 = 'mdadm'
1771        testrecipe2 = 'cronie'
1772        result = runCmd('devtool modify -x %s %s' % (testrecipe1, os.path.join(tempdir, testrecipe1)))
1773        result = runCmd('devtool modify -x %s %s' % (testrecipe2, os.path.join(tempdir, testrecipe2)))
1774        result = runCmd('devtool build %s' % testrecipe1)
1775        result = runCmd('devtool build %s' % testrecipe2)
1776        stampprefix1 = get_bb_var('STAMP', testrecipe1)
1777        self.assertTrue(stampprefix1, 'Unable to get STAMP value for recipe %s' % testrecipe1)
1778        stampprefix2 = get_bb_var('STAMP', testrecipe2)
1779        self.assertTrue(stampprefix2, 'Unable to get STAMP value for recipe %s' % testrecipe2)
1780        result = runCmd('devtool reset -a')
1781        self.assertIn(testrecipe1, result.output)
1782        self.assertIn(testrecipe2, result.output)
1783        result = runCmd('devtool status')
1784        self.assertNotIn(testrecipe1, result.output)
1785        self.assertNotIn(testrecipe2, result.output)
1786        matches1 = glob.glob(stampprefix1 + '*')
1787        self.assertFalse(matches1, 'Stamp files exist for recipe %s that should have been cleaned' % testrecipe1)
1788        matches2 = glob.glob(stampprefix2 + '*')
1789        self.assertFalse(matches2, 'Stamp files exist for recipe %s that should have been cleaned' % testrecipe2)
1790
1791    def test_devtool_reset_re_plus_plus(self):
1792        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
1793        self.track_for_cleanup(tempdir)
1794        self.track_for_cleanup(self.workspacedir)
1795        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
1796        testrecipe = 'devtool-test-reset-re++'
1797        result = runCmd('devtool modify %s' % testrecipe)
1798        result = runCmd('devtool reset -n %s' % testrecipe)
1799        self.assertIn(testrecipe, result.output)
1800        result = runCmd('devtool status')
1801        self.assertNotIn(testrecipe, result.output)
1802        self.assertNotExists(os.path.join(self.workspacedir, 'recipes', testrecipe), 'Recipe directory should not exist after resetting')
1803
1804class DevtoolDeployTargetTests(DevtoolBase):
1805
1806    @OETestTag("runqemu")
1807    def test_devtool_deploy_target(self):
1808        self._check_runqemu_prerequisites()
1809        self.assertTrue(not os.path.exists(self.workspacedir), 'This test cannot be run with a workspace directory under the build directory')
1810        # Definitions
1811        testrecipe = 'mdadm'
1812        testfile = '/sbin/mdadm'
1813        if "usrmerge" in get_bb_var('DISTRO_FEATURES'):
1814            testfile = '/usr/sbin/mdadm'
1815        testimage = 'oe-selftest-image'
1816        testcommand = '/sbin/mdadm --help'
1817        # Build an image to run
1818        bitbake("%s qemu-native qemu-helper-native" % testimage)
1819        deploy_dir_image = get_bb_var('DEPLOY_DIR_IMAGE')
1820        self.add_command_to_tearDown('bitbake -c clean %s' % testimage)
1821        self.add_command_to_tearDown('rm -f %s/%s*' % (deploy_dir_image, testimage))
1822        # Clean recipe so the first deploy will fail
1823        bitbake("%s -c clean" % testrecipe)
1824        # Try devtool modify
1825        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
1826        self.track_for_cleanup(tempdir)
1827        self.track_for_cleanup(self.workspacedir)
1828        self.add_command_to_tearDown('bitbake -c clean %s' % testrecipe)
1829        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
1830        result = runCmd('devtool modify %s -x %s' % (testrecipe, tempdir))
1831        # Test that deploy-target at this point fails (properly)
1832        result = runCmd('devtool deploy-target -n %s root@localhost' % testrecipe, ignore_status=True)
1833        self.assertNotEqual(result.output, 0, 'devtool deploy-target should have failed, output: %s' % result.output)
1834        self.assertNotIn(result.output, 'Traceback', 'devtool deploy-target should have failed with a proper error not a traceback, output: %s' % result.output)
1835        result = runCmd('devtool build %s' % testrecipe)
1836        # First try a dry-run of deploy-target
1837        result = runCmd('devtool deploy-target -n %s root@localhost' % testrecipe)
1838        self.assertIn('  %s' % testfile, result.output)
1839        # Boot the image
1840        with runqemu(testimage) as qemu:
1841            # Now really test deploy-target
1842            result = runCmd('devtool deploy-target -c %s root@%s' % (testrecipe, qemu.ip))
1843            # Run a test command to see if it was installed properly
1844            sshargs = '-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no'
1845            result = runCmd('ssh %s root@%s %s' % (sshargs, qemu.ip, testcommand))
1846            # Check if it deployed all of the files with the right ownership/perms
1847            # First look on the host - need to do this under pseudo to get the correct ownership/perms
1848            bb_vars = get_bb_vars(['D', 'FAKEROOTENV', 'FAKEROOTCMD'], testrecipe)
1849            installdir = bb_vars['D']
1850            fakerootenv = bb_vars['FAKEROOTENV']
1851            fakerootcmd = bb_vars['FAKEROOTCMD']
1852            result = runCmd('%s %s find . -type f -exec ls -l {} \\;' % (fakerootenv, fakerootcmd), cwd=installdir)
1853            filelist1 = self._process_ls_output(result.output)
1854
1855            # Now look on the target
1856            tempdir2 = tempfile.mkdtemp(prefix='devtoolqa')
1857            self.track_for_cleanup(tempdir2)
1858            tmpfilelist = os.path.join(tempdir2, 'files.txt')
1859            with open(tmpfilelist, 'w') as f:
1860                for line in filelist1:
1861                    splitline = line.split()
1862                    f.write(splitline[-1] + '\n')
1863            result = runCmd('cat %s | ssh -q %s root@%s \'xargs ls -l\'' % (tmpfilelist, sshargs, qemu.ip))
1864            filelist2 = self._process_ls_output(result.output)
1865            filelist1.sort(key=lambda item: item.split()[-1])
1866            filelist2.sort(key=lambda item: item.split()[-1])
1867            self.assertEqual(filelist1, filelist2)
1868            # Test undeploy-target
1869            result = runCmd('devtool undeploy-target -c %s root@%s' % (testrecipe, qemu.ip))
1870            result = runCmd('ssh %s root@%s %s' % (sshargs, qemu.ip, testcommand), ignore_status=True)
1871            self.assertNotEqual(result, 0, 'undeploy-target did not remove command as it should have')
1872
1873class DevtoolBuildImageTests(DevtoolBase):
1874
1875    def test_devtool_build_image(self):
1876        """Test devtool build-image plugin"""
1877        # Check preconditions
1878        self.assertTrue(not os.path.exists(self.workspacedir), 'This test cannot be run with a workspace directory under the build directory')
1879        image = 'core-image-minimal'
1880        self.track_for_cleanup(self.workspacedir)
1881        self.add_command_to_tearDown('bitbake -c clean %s' % image)
1882        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
1883        bitbake('%s -c clean' % image)
1884        # Add target and native recipes to workspace
1885        recipes = ['mdadm', 'parted-native']
1886        for recipe in recipes:
1887            tempdir = tempfile.mkdtemp(prefix='devtoolqa')
1888            self.track_for_cleanup(tempdir)
1889            self.add_command_to_tearDown('bitbake -c clean %s' % recipe)
1890            runCmd('devtool modify %s -x %s' % (recipe, tempdir))
1891        # Try to build image
1892        result = runCmd('devtool build-image %s' % image)
1893        self.assertNotEqual(result, 0, 'devtool build-image failed')
1894        # Check if image contains expected packages
1895        deploy_dir_image = get_bb_var('DEPLOY_DIR_IMAGE')
1896        image_link_name = get_bb_var('IMAGE_LINK_NAME', image)
1897        reqpkgs = [item for item in recipes if not item.endswith('-native')]
1898        with open(os.path.join(deploy_dir_image, image_link_name + '.manifest'), 'r') as f:
1899            for line in f:
1900                splitval = line.split()
1901                if splitval:
1902                    pkg = splitval[0]
1903                    if pkg in reqpkgs:
1904                        reqpkgs.remove(pkg)
1905        if reqpkgs:
1906            self.fail('The following packages were not present in the image as expected: %s' % ', '.join(reqpkgs))
1907
1908class DevtoolUpgradeTests(DevtoolBase):
1909
1910    def setUp(self):
1911        super().setUp()
1912        try:
1913            runCmd("git config --global user.name")
1914            runCmd("git config --global user.email")
1915        except:
1916            self.skip("Git user.name and user.email must be set")
1917
1918    def test_devtool_upgrade(self):
1919        # Check preconditions
1920        self.assertTrue(not os.path.exists(self.workspacedir), 'This test cannot be run with a workspace directory under the build directory')
1921        self.track_for_cleanup(self.workspacedir)
1922        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
1923        # Check parameters
1924        result = runCmd('devtool upgrade -h')
1925        for param in 'recipename srctree --version -V --branch -b --keep-temp --no-patch'.split():
1926            self.assertIn(param, result.output)
1927        # For the moment, we are using a real recipe.
1928        recipe = 'devtool-upgrade-test1'
1929        version = '1.6.0'
1930        oldrecipefile = get_bb_var('FILE', recipe)
1931        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
1932        self.track_for_cleanup(tempdir)
1933        # Check that recipe is not already under devtool control
1934        result = runCmd('devtool status')
1935        self.assertNotIn(recipe, result.output)
1936        # Check upgrade. Code does not check if new PV is older or newer that current PV, so, it may be that
1937        # we are downgrading instead of upgrading.
1938        result = runCmd('devtool upgrade %s %s -V %s' % (recipe, tempdir, version))
1939        # Check if srctree at least is populated
1940        self.assertTrue(len(os.listdir(tempdir)) > 0, 'srctree (%s) should be populated with new (%s) source code' % (tempdir, version))
1941        # Check new recipe subdirectory is present
1942        self.assertExists(os.path.join(self.workspacedir, 'recipes', recipe, '%s-%s' % (recipe, version)), 'Recipe folder should exist')
1943        # Check new recipe file is present
1944        newrecipefile = os.path.join(self.workspacedir, 'recipes', recipe, '%s_%s.bb' % (recipe, version))
1945        self.assertExists(newrecipefile, 'Recipe file should exist after upgrade')
1946        # Check devtool status and make sure recipe is present
1947        result = runCmd('devtool status')
1948        self.assertIn(recipe, result.output)
1949        self.assertIn(tempdir, result.output)
1950        # Check recipe got changed as expected
1951        with open(oldrecipefile + '.upgraded', 'r') as f:
1952            desiredlines = f.readlines()
1953        with open(newrecipefile, 'r') as f:
1954            newlines = f.readlines()
1955        self.assertEqual(desiredlines, newlines)
1956        # Check devtool reset recipe
1957        result = runCmd('devtool reset %s -n' % recipe)
1958        result = runCmd('devtool status')
1959        self.assertNotIn(recipe, result.output)
1960        self.assertNotExists(os.path.join(self.workspacedir, 'recipes', recipe), 'Recipe directory should not exist after resetting')
1961
1962    def test_devtool_upgrade_git(self):
1963        # Check preconditions
1964        self.assertTrue(not os.path.exists(self.workspacedir), 'This test cannot be run with a workspace directory under the build directory')
1965        self.track_for_cleanup(self.workspacedir)
1966        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
1967        recipe = 'devtool-upgrade-test2'
1968        commit = '6cc6077a36fe2648a5f993fe7c16c9632f946517'
1969        oldrecipefile = get_bb_var('FILE', recipe)
1970        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
1971        self.track_for_cleanup(tempdir)
1972        # Check that recipe is not already under devtool control
1973        result = runCmd('devtool status')
1974        self.assertNotIn(recipe, result.output)
1975        # Check upgrade
1976        result = runCmd('devtool upgrade %s %s -S %s' % (recipe, tempdir, commit))
1977        # Check if srctree at least is populated
1978        self.assertTrue(len(os.listdir(tempdir)) > 0, 'srctree (%s) should be populated with new (%s) source code' % (tempdir, commit))
1979        # Check new recipe file is present
1980        newrecipefile = os.path.join(self.workspacedir, 'recipes', recipe, os.path.basename(oldrecipefile))
1981        self.assertExists(newrecipefile, 'Recipe file should exist after upgrade')
1982        # Check devtool status and make sure recipe is present
1983        result = runCmd('devtool status')
1984        self.assertIn(recipe, result.output)
1985        self.assertIn(tempdir, result.output)
1986        # Check recipe got changed as expected
1987        with open(oldrecipefile + '.upgraded', 'r') as f:
1988            desiredlines = f.readlines()
1989        with open(newrecipefile, 'r') as f:
1990            newlines = f.readlines()
1991        self.assertEqual(desiredlines, newlines)
1992        # Check devtool reset recipe
1993        result = runCmd('devtool reset %s -n' % recipe)
1994        result = runCmd('devtool status')
1995        self.assertNotIn(recipe, result.output)
1996        self.assertNotExists(os.path.join(self.workspacedir, 'recipes', recipe), 'Recipe directory should not exist after resetting')
1997
1998    def test_devtool_upgrade_drop_md5sum(self):
1999        # Check preconditions
2000        self.assertTrue(not os.path.exists(self.workspacedir), 'This test cannot be run with a workspace directory under the build directory')
2001        self.track_for_cleanup(self.workspacedir)
2002        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
2003        # For the moment, we are using a real recipe.
2004        recipe = 'devtool-upgrade-test3'
2005        version = '1.6.0'
2006        oldrecipefile = get_bb_var('FILE', recipe)
2007        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
2008        self.track_for_cleanup(tempdir)
2009        # Check upgrade. Code does not check if new PV is older or newer that current PV, so, it may be that
2010        # we are downgrading instead of upgrading.
2011        result = runCmd('devtool upgrade %s %s -V %s' % (recipe, tempdir, version))
2012        # Check new recipe file is present
2013        newrecipefile = os.path.join(self.workspacedir, 'recipes', recipe, '%s_%s.bb' % (recipe, version))
2014        self.assertExists(newrecipefile, 'Recipe file should exist after upgrade')
2015        # Check recipe got changed as expected
2016        with open(oldrecipefile + '.upgraded', 'r') as f:
2017            desiredlines = f.readlines()
2018        with open(newrecipefile, 'r') as f:
2019            newlines = f.readlines()
2020        self.assertEqual(desiredlines, newlines)
2021
2022    def test_devtool_upgrade_all_checksums(self):
2023        # Check preconditions
2024        self.assertTrue(not os.path.exists(self.workspacedir), 'This test cannot be run with a workspace directory under the build directory')
2025        self.track_for_cleanup(self.workspacedir)
2026        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
2027        # For the moment, we are using a real recipe.
2028        recipe = 'devtool-upgrade-test4'
2029        version = '1.6.0'
2030        oldrecipefile = get_bb_var('FILE', recipe)
2031        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
2032        self.track_for_cleanup(tempdir)
2033        # Check upgrade. Code does not check if new PV is older or newer that current PV, so, it may be that
2034        # we are downgrading instead of upgrading.
2035        result = runCmd('devtool upgrade %s %s -V %s' % (recipe, tempdir, version))
2036        # Check new recipe file is present
2037        newrecipefile = os.path.join(self.workspacedir, 'recipes', recipe, '%s_%s.bb' % (recipe, version))
2038        self.assertExists(newrecipefile, 'Recipe file should exist after upgrade')
2039        # Check recipe got changed as expected
2040        with open(oldrecipefile + '.upgraded', 'r') as f:
2041            desiredlines = f.readlines()
2042        with open(newrecipefile, 'r') as f:
2043            newlines = f.readlines()
2044        self.assertEqual(desiredlines, newlines)
2045
2046    def test_devtool_upgrade_recipe_upgrade_extra_tasks(self):
2047        # Check preconditions
2048        self.assertTrue(not os.path.exists(self.workspacedir), 'This test cannot be run with a workspace directory under the build directory')
2049        self.track_for_cleanup(self.workspacedir)
2050        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
2051        recipe = 'python3-guessing-game'
2052        version = '0.2.0'
2053        commit = '40cf004c2772ffa20ea803fa3be1528a75be3e98'
2054        oldrecipefile = get_bb_var('FILE', recipe)
2055        oldcratesincfile = os.path.join(os.path.dirname(oldrecipefile), os.path.basename(oldrecipefile).strip('_git.bb') + '-crates.inc')
2056        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
2057        self.track_for_cleanup(tempdir)
2058        # Check that recipe is not already under devtool control
2059        result = runCmd('devtool status')
2060        self.assertNotIn(recipe, result.output)
2061        # Check upgrade
2062        result = runCmd('devtool upgrade %s %s --version %s --srcrev %s' % (recipe, tempdir, version, commit))
2063        # Check if srctree at least is populated
2064        self.assertTrue(len(os.listdir(tempdir)) > 0, 'srctree (%s) should be populated with new (%s) source code' % (tempdir, commit))
2065        # Check new recipe file and new -crates.inc files are present
2066        newrecipefile = os.path.join(self.workspacedir, 'recipes', recipe, os.path.basename(oldrecipefile))
2067        newcratesincfile = os.path.join(self.workspacedir, 'recipes', recipe, os.path.basename(oldcratesincfile))
2068        self.assertExists(newrecipefile, 'Recipe file should exist after upgrade')
2069        self.assertExists(newcratesincfile, 'Recipe crates.inc file should exist after upgrade')
2070        # Check devtool status and make sure recipe is present
2071        result = runCmd('devtool status')
2072        self.assertIn(recipe, result.output)
2073        self.assertIn(tempdir, result.output)
2074        # Check recipe got changed as expected
2075        with open(oldrecipefile + '.upgraded', 'r') as f:
2076            desiredlines = f.readlines()
2077        with open(newrecipefile, 'r') as f:
2078            newlines = f.readlines()
2079        self.assertEqual(desiredlines, newlines)
2080        # Check crates.inc got changed as expected
2081        with open(oldcratesincfile + '.upgraded', 'r') as f:
2082            desiredlines = f.readlines()
2083        with open(newcratesincfile, 'r') as f:
2084            newlines = f.readlines()
2085        self.assertEqual(desiredlines, newlines)
2086        # Check devtool reset recipe
2087        result = runCmd('devtool reset %s -n' % recipe)
2088        result = runCmd('devtool status')
2089        self.assertNotIn(recipe, result.output)
2090        self.assertNotExists(os.path.join(self.workspacedir, 'recipes', recipe), 'Recipe directory should not exist after resetting')
2091
2092    def test_devtool_layer_plugins(self):
2093        """Test that devtool can use plugins from other layers.
2094
2095        This test executes the selftest-reverse command from meta-selftest."""
2096
2097        self.track_for_cleanup(self.workspacedir)
2098        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
2099
2100        s = "Microsoft Made No Profit From Anyone's Zunes Yo"
2101        result = runCmd("devtool --quiet selftest-reverse \"%s\"" % s)
2102        self.assertEqual(result.output, s[::-1])
2103
2104    def _copy_file_with_cleanup(self, srcfile, basedstdir, *paths):
2105        dstdir = basedstdir
2106        self.assertExists(dstdir)
2107        for p in paths:
2108            dstdir = os.path.join(dstdir, p)
2109            if not os.path.exists(dstdir):
2110                try:
2111                    os.makedirs(dstdir)
2112                except PermissionError:
2113                    return False
2114                except OSError as e:
2115                    if e.errno == errno.EROFS:
2116                        return False
2117                    else:
2118                        raise e
2119                if p == "lib":
2120                    # Can race with other tests
2121                    self.add_command_to_tearDown('rmdir --ignore-fail-on-non-empty %s' % dstdir)
2122                else:
2123                    self.track_for_cleanup(dstdir)
2124        dstfile = os.path.join(dstdir, os.path.basename(srcfile))
2125        if srcfile != dstfile:
2126            try:
2127                shutil.copy(srcfile, dstfile)
2128            except PermissionError:
2129                return False
2130            self.track_for_cleanup(dstfile)
2131        return True
2132
2133    def test_devtool_load_plugin(self):
2134        """Test that devtool loads only the first found plugin in BBPATH."""
2135
2136        self.track_for_cleanup(self.workspacedir)
2137        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
2138
2139        devtool = runCmd("which devtool")
2140        fromname = runCmd("devtool --quiet pluginfile")
2141        srcfile = fromname.output
2142        bbpath = get_bb_var('BBPATH')
2143        searchpath = bbpath.split(':') + [os.path.dirname(devtool.output)]
2144        plugincontent = []
2145        with open(srcfile) as fh:
2146            plugincontent = fh.readlines()
2147        try:
2148            self.assertIn('meta-selftest', srcfile, 'wrong bbpath plugin found')
2149            searchpath = [
2150                path for path in searchpath
2151                if self._copy_file_with_cleanup(srcfile, path, 'lib', 'devtool')
2152            ]
2153            result = runCmd("devtool --quiet count")
2154            self.assertEqual(result.output, '1')
2155            result = runCmd("devtool --quiet multiloaded")
2156            self.assertEqual(result.output, "no")
2157            for path in searchpath:
2158                result = runCmd("devtool --quiet bbdir")
2159                self.assertEqual(os.path.realpath(result.output), os.path.realpath(path))
2160                os.unlink(os.path.join(result.output, 'lib', 'devtool', 'bbpath.py'))
2161        finally:
2162            with open(srcfile, 'w') as fh:
2163                fh.writelines(plugincontent)
2164
2165    def _setup_test_devtool_finish_upgrade(self):
2166        # Check preconditions
2167        self.assertTrue(not os.path.exists(self.workspacedir), 'This test cannot be run with a workspace directory under the build directory')
2168        self.track_for_cleanup(self.workspacedir)
2169        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
2170        # Use a "real" recipe from meta-selftest
2171        recipe = 'devtool-upgrade-test1'
2172        oldversion = '1.5.3'
2173        newversion = '1.6.0'
2174        oldrecipefile = get_bb_var('FILE', recipe)
2175        recipedir = os.path.dirname(oldrecipefile)
2176        result = runCmd('git status --porcelain .', cwd=recipedir)
2177        if result.output.strip():
2178            self.fail('Recipe directory for %s contains uncommitted changes' % recipe)
2179        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
2180        self.track_for_cleanup(tempdir)
2181        # Check that recipe is not already under devtool control
2182        result = runCmd('devtool status')
2183        self.assertNotIn(recipe, result.output)
2184        # Do the upgrade
2185        result = runCmd('devtool upgrade %s %s -V %s' % (recipe, tempdir, newversion))
2186        # Check devtool status and make sure recipe is present
2187        result = runCmd('devtool status')
2188        self.assertIn(recipe, result.output)
2189        self.assertIn(tempdir, result.output)
2190        # Make a change to the source
2191        result = runCmd('sed -i \'/^#include "pv.h"/a \\/* Here is a new comment *\\/\' src/pv/number.c', cwd=tempdir)
2192        result = runCmd('git status --porcelain', cwd=tempdir)
2193        self.assertIn('M src/pv/number.c', result.output)
2194        result = runCmd('git commit src/pv/number.c -m "Add a comment to the code"', cwd=tempdir)
2195        # Check if patch is there
2196        recipedir = os.path.dirname(oldrecipefile)
2197        olddir = os.path.join(recipedir, recipe + '-' + oldversion)
2198        patchfn = '0001-Add-a-note-line-to-the-quick-reference.patch'
2199        backportedpatchfn = 'backported.patch'
2200        self.assertExists(os.path.join(olddir, patchfn), 'Original patch file does not exist')
2201        self.assertExists(os.path.join(olddir, backportedpatchfn), 'Backported patch file does not exist')
2202        return recipe, oldrecipefile, recipedir, olddir, newversion, patchfn, backportedpatchfn
2203
2204    def test_devtool_finish_upgrade_origlayer(self):
2205        recipe, oldrecipefile, recipedir, olddir, newversion, patchfn, backportedpatchfn = self._setup_test_devtool_finish_upgrade()
2206        # Ensure the recipe is where we think it should be (so that cleanup doesn't trash things)
2207        self.assertIn('/meta-selftest/', recipedir)
2208        # Try finish to the original layer
2209        self.add_command_to_tearDown('rm -rf %s ; cd %s ; git checkout %s' % (recipedir, os.path.dirname(recipedir), recipedir))
2210        result = runCmd('devtool finish %s meta-selftest' % recipe)
2211        result = runCmd('devtool status')
2212        self.assertNotIn(recipe, result.output, 'Recipe should have been reset by finish but wasn\'t')
2213        self.assertNotExists(os.path.join(self.workspacedir, 'recipes', recipe), 'Recipe directory should not exist after finish')
2214        self.assertNotExists(oldrecipefile, 'Old recipe file should have been deleted but wasn\'t')
2215        self.assertNotExists(os.path.join(olddir, patchfn), 'Old patch file should have been deleted but wasn\'t')
2216        self.assertNotExists(os.path.join(olddir, backportedpatchfn), 'Old backported patch file should have been deleted but wasn\'t')
2217        newrecipefile = os.path.join(recipedir, '%s_%s.bb' % (recipe, newversion))
2218        newdir = os.path.join(recipedir, recipe + '-' + newversion)
2219        self.assertExists(newrecipefile, 'New recipe file should have been copied into existing layer but wasn\'t')
2220        self.assertExists(os.path.join(newdir, patchfn), 'Patch file should have been copied into new directory but wasn\'t')
2221        self.assertNotExists(os.path.join(newdir, backportedpatchfn), 'Backported patch file should not have been copied into new directory but was')
2222        self.assertExists(os.path.join(newdir, '0002-Add-a-comment-to-the-code.patch'), 'New patch file should have been created but wasn\'t')
2223        with open(newrecipefile, 'r') as f:
2224            newcontent = f.read()
2225        self.assertNotIn(backportedpatchfn, newcontent, "Backported patch should have been removed from the recipe but wasn't")
2226        self.assertIn(patchfn, newcontent, "Old patch should have not been removed from the recipe but was")
2227        self.assertIn("0002-Add-a-comment-to-the-code.patch", newcontent, "New patch should have been added to the recipe but wasn't")
2228        self.assertIn("http://www.ivarch.com/programs/sources/pv-${PV}.tar.gz", newcontent, "New recipe no longer has upstream source in SRC_URI")
2229
2230
2231    def test_devtool_finish_upgrade_otherlayer(self):
2232        recipe, oldrecipefile, recipedir, olddir, newversion, patchfn, backportedpatchfn = self._setup_test_devtool_finish_upgrade()
2233        # Ensure the recipe is where we think it should be (so that cleanup doesn't trash things)
2234        self.assertIn('/meta-selftest/', recipedir)
2235        # Try finish to a different layer - should create a bbappend
2236        # This cleanup isn't strictly necessary but do it anyway just in case it goes wrong and writes to here
2237        self.add_command_to_tearDown('rm -rf %s ; cd %s ; git checkout %s' % (recipedir, os.path.dirname(recipedir), recipedir))
2238        oe_core_dir = os.path.join(get_bb_var('COREBASE'), 'meta')
2239        newrecipedir = os.path.join(oe_core_dir, 'recipes-test', 'devtool')
2240        newrecipefile = os.path.join(newrecipedir, '%s_%s.bb' % (recipe, newversion))
2241        self.track_for_cleanup(newrecipedir)
2242        result = runCmd('devtool finish %s oe-core' % recipe)
2243        result = runCmd('devtool status')
2244        self.assertNotIn(recipe, result.output, 'Recipe should have been reset by finish but wasn\'t')
2245        self.assertNotExists(os.path.join(self.workspacedir, 'recipes', recipe), 'Recipe directory should not exist after finish')
2246        self.assertExists(oldrecipefile, 'Old recipe file should not have been deleted')
2247        self.assertExists(os.path.join(olddir, patchfn), 'Old patch file should not have been deleted')
2248        self.assertExists(os.path.join(olddir, backportedpatchfn), 'Old backported patch file should not have been deleted')
2249        newdir = os.path.join(newrecipedir, recipe + '-' + newversion)
2250        self.assertExists(newrecipefile, 'New recipe file should have been copied into existing layer but wasn\'t')
2251        self.assertExists(os.path.join(newdir, patchfn), 'Patch file should have been copied into new directory but wasn\'t')
2252        self.assertNotExists(os.path.join(newdir, backportedpatchfn), 'Backported patch file should not have been copied into new directory but was')
2253        self.assertExists(os.path.join(newdir, '0002-Add-a-comment-to-the-code.patch'), 'New patch file should have been created but wasn\'t')
2254        with open(newrecipefile, 'r') as f:
2255            newcontent = f.read()
2256        self.assertNotIn(backportedpatchfn, newcontent, "Backported patch should have been removed from the recipe but wasn't")
2257        self.assertIn(patchfn, newcontent, "Old patch should have not been removed from the recipe but was")
2258        self.assertIn("0002-Add-a-comment-to-the-code.patch", newcontent, "New patch should have been added to the recipe but wasn't")
2259        self.assertIn("http://www.ivarch.com/programs/sources/pv-${PV}.tar.gz", newcontent, "New recipe no longer has upstream source in SRC_URI")
2260
2261    def _setup_test_devtool_finish_modify(self):
2262        # Check preconditions
2263        self.assertTrue(not os.path.exists(self.workspacedir), 'This test cannot be run with a workspace directory under the build directory')
2264        # Try modifying a recipe
2265        self.track_for_cleanup(self.workspacedir)
2266        recipe = 'mdadm'
2267        oldrecipefile = get_bb_var('FILE', recipe)
2268        recipedir = os.path.dirname(oldrecipefile)
2269        result = runCmd('git status --porcelain .', cwd=recipedir)
2270        if result.output.strip():
2271            self.fail('Recipe directory for %s contains uncommitted changes' % recipe)
2272        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
2273        self.track_for_cleanup(tempdir)
2274        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
2275        result = runCmd('devtool modify %s %s' % (recipe, tempdir))
2276        self.assertExists(os.path.join(tempdir, 'Makefile'), 'Extracted source could not be found')
2277        # Test devtool status
2278        result = runCmd('devtool status')
2279        self.assertIn(recipe, result.output)
2280        self.assertIn(tempdir, result.output)
2281        # Make a change to the source
2282        result = runCmd('sed -i \'/^#include "mdadm.h"/a \\/* Here is a new comment *\\/\' maps.c', cwd=tempdir)
2283        result = runCmd('git status --porcelain', cwd=tempdir)
2284        self.assertIn('M maps.c', result.output)
2285        result = runCmd('git commit maps.c -m "Add a comment to the code"', cwd=tempdir)
2286        for entry in os.listdir(recipedir):
2287            filesdir = os.path.join(recipedir, entry)
2288            if os.path.isdir(filesdir):
2289                break
2290        else:
2291            self.fail('Unable to find recipe files directory for %s' % recipe)
2292        return recipe, oldrecipefile, recipedir, filesdir
2293
2294    def test_devtool_finish_modify_origlayer(self):
2295        recipe, oldrecipefile, recipedir, filesdir = self._setup_test_devtool_finish_modify()
2296        # Ensure the recipe is where we think it should be (so that cleanup doesn't trash things)
2297        self.assertIn('/meta/', recipedir)
2298        # Try finish to the original layer
2299        self.add_command_to_tearDown('rm -rf %s ; cd %s ; git checkout %s' % (recipedir, os.path.dirname(recipedir), recipedir))
2300        result = runCmd('devtool finish %s meta' % recipe)
2301        result = runCmd('devtool status')
2302        self.assertNotIn(recipe, result.output, 'Recipe should have been reset by finish but wasn\'t')
2303        self.assertNotExists(os.path.join(self.workspacedir, 'recipes', recipe), 'Recipe directory should not exist after finish')
2304        expected_status = [(' M', '.*/%s$' % os.path.basename(oldrecipefile)),
2305                           ('??', '.*/.*-Add-a-comment-to-the-code.patch$')]
2306        self._check_repo_status(recipedir, expected_status)
2307
2308    def test_devtool_finish_modify_otherlayer(self):
2309        recipe, oldrecipefile, recipedir, filesdir = self._setup_test_devtool_finish_modify()
2310        # Ensure the recipe is where we think it should be (so that cleanup doesn't trash things)
2311        self.assertIn('/meta/', recipedir)
2312        relpth = os.path.relpath(recipedir, os.path.join(get_bb_var('COREBASE'), 'meta'))
2313        appenddir = os.path.join(get_test_layer(), relpth)
2314        self.track_for_cleanup(appenddir)
2315        # Try finish to the original layer
2316        self.add_command_to_tearDown('rm -rf %s ; cd %s ; git checkout %s' % (recipedir, os.path.dirname(recipedir), recipedir))
2317        result = runCmd('devtool finish %s meta-selftest' % recipe)
2318        result = runCmd('devtool status')
2319        self.assertNotIn(recipe, result.output, 'Recipe should have been reset by finish but wasn\'t')
2320        self.assertNotExists(os.path.join(self.workspacedir, 'recipes', recipe), 'Recipe directory should not exist after finish')
2321        result = runCmd('git status --porcelain .', cwd=recipedir)
2322        if result.output.strip():
2323            self.fail('Recipe directory for %s contains the following unexpected changes after finish:\n%s' % (recipe, result.output.strip()))
2324        recipefn = os.path.splitext(os.path.basename(oldrecipefile))[0]
2325        recipefn = recipefn.split('_')[0] + '_%'
2326        appendfile = os.path.join(appenddir, recipefn + '.bbappend')
2327        self.assertExists(appendfile, 'bbappend %s should have been created but wasn\'t' % appendfile)
2328        newdir = os.path.join(appenddir, recipe)
2329        files = os.listdir(newdir)
2330        foundpatch = None
2331        for fn in files:
2332            if fnmatch.fnmatch(fn, '*-Add-a-comment-to-the-code.patch'):
2333                foundpatch = fn
2334        if not foundpatch:
2335            self.fail('No patch file created next to bbappend')
2336        files.remove(foundpatch)
2337        if files:
2338            self.fail('Unexpected file(s) copied next to bbappend: %s' % ', '.join(files))
2339
2340    def test_devtool_finish_update_patch(self):
2341        # This test uses a modified version of the sysdig recipe from meta-oe.
2342        # - The patches have been renamed.
2343        # - The dependencies are commented out since the recipe is not being
2344        #   built.
2345        #
2346        # The sysdig recipe is interesting in that it fetches two different Git
2347        # repositories, and there are patches for both. This leads to that
2348        # devtool will create ignore commits as it uses Git submodules to keep
2349        # track of the second repository.
2350        #
2351        # This test will verify that the ignored commits actually are ignored
2352        # when a commit in between is modified. It will also verify that the
2353        # updated patch keeps its original name.
2354
2355        # Check preconditions
2356        self.assertTrue(not os.path.exists(self.workspacedir), 'This test cannot be run with a workspace directory under the build directory')
2357        # Try modifying a recipe
2358        self.track_for_cleanup(self.workspacedir)
2359        recipe = 'sysdig-selftest'
2360        recipefile = get_bb_var('FILE', recipe)
2361        recipedir = os.path.dirname(recipefile)
2362        result = runCmd('git status --porcelain .', cwd=recipedir)
2363        if result.output.strip():
2364            self.fail('Recipe directory for %s contains uncommitted changes' % recipe)
2365        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
2366        self.track_for_cleanup(tempdir)
2367        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
2368        result = runCmd('devtool modify %s %s' % (recipe, tempdir))
2369        self.add_command_to_tearDown('cd %s; rm %s/*; git checkout %s %s' % (recipedir, recipe, recipe, os.path.basename(recipefile)))
2370        self.assertExists(os.path.join(tempdir, 'CMakeLists.txt'), 'Extracted source could not be found')
2371        # Make a change to one of the existing commits
2372        result = runCmd('echo "# A comment " >> CMakeLists.txt', cwd=tempdir)
2373        result = runCmd('git status --porcelain', cwd=tempdir)
2374        self.assertIn('M CMakeLists.txt', result.output)
2375        result = runCmd('git commit --fixup HEAD^ CMakeLists.txt', cwd=tempdir)
2376        result = runCmd('git show -s --format=%s', cwd=tempdir)
2377        self.assertIn('fixup! cmake: Pass PROBE_NAME via CFLAGS', result.output)
2378        result = runCmd('GIT_SEQUENCE_EDITOR=true git rebase -i --autosquash devtool-base', cwd=tempdir)
2379        result = runCmd('devtool finish %s meta-selftest' % recipe)
2380        result = runCmd('devtool status')
2381        self.assertNotIn(recipe, result.output, 'Recipe should have been reset by finish but wasn\'t')
2382        self.assertNotExists(os.path.join(self.workspacedir, 'recipes', recipe), 'Recipe directory should not exist after finish')
2383        expected_status = [(' M', '.*/0099-cmake-Pass-PROBE_NAME-via-CFLAGS.patch$')]
2384        self._check_repo_status(recipedir, expected_status)
2385
2386    def test_devtool_rename(self):
2387        # Check preconditions
2388        self.assertTrue(not os.path.exists(self.workspacedir), 'This test cannot be run with a workspace directory under the build directory')
2389        self.track_for_cleanup(self.workspacedir)
2390        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
2391
2392        # First run devtool add
2393        # We already have this recipe in OE-Core, but that doesn't matter
2394        recipename = 'i2c-tools'
2395        recipever = '3.1.2'
2396        recipefile = os.path.join(self.workspacedir, 'recipes', recipename, '%s_%s.bb' % (recipename, recipever))
2397        url = 'http://downloads.yoctoproject.org/mirror/sources/i2c-tools-%s.tar.bz2' % recipever
2398        def add_recipe():
2399            result = runCmd('devtool add %s' % url)
2400            self.assertExists(recipefile, 'Expected recipe file not created')
2401            self.assertExists(os.path.join(self.workspacedir, 'sources', recipename), 'Source directory not created')
2402            checkvars = {}
2403            checkvars['S'] = None
2404            checkvars['SRC_URI'] = url.replace(recipever, '${PV}')
2405            self._test_recipe_contents(recipefile, checkvars, [])
2406        add_recipe()
2407        # Now rename it - change both name and version
2408        newrecipename = 'mynewrecipe'
2409        newrecipever = '456'
2410        newrecipefile = os.path.join(self.workspacedir, 'recipes', newrecipename, '%s_%s.bb' % (newrecipename, newrecipever))
2411        result = runCmd('devtool rename %s %s -V %s' % (recipename, newrecipename, newrecipever))
2412        self.assertExists(newrecipefile, 'Recipe file not renamed')
2413        self.assertNotExists(os.path.join(self.workspacedir, 'recipes', recipename), 'Old recipe directory still exists')
2414        newsrctree = os.path.join(self.workspacedir, 'sources', newrecipename)
2415        self.assertExists(newsrctree, 'Source directory not renamed')
2416        checkvars = {}
2417        checkvars['S'] = '${WORKDIR}/%s-%s' % (recipename, recipever)
2418        checkvars['SRC_URI'] = url
2419        self._test_recipe_contents(newrecipefile, checkvars, [])
2420        # Try again - change just name this time
2421        result = runCmd('devtool reset -n %s' % newrecipename)
2422        add_recipe()
2423        newrecipefile = os.path.join(self.workspacedir, 'recipes', newrecipename, '%s_%s.bb' % (newrecipename, recipever))
2424        result = runCmd('devtool rename %s %s' % (recipename, newrecipename))
2425        self.assertExists(newrecipefile, 'Recipe file not renamed')
2426        self.assertNotExists(os.path.join(self.workspacedir, 'recipes', recipename), 'Old recipe directory still exists')
2427        self.assertExists(os.path.join(self.workspacedir, 'sources', newrecipename), 'Source directory not renamed')
2428        checkvars = {}
2429        checkvars['S'] = '${WORKDIR}/%s-${PV}' % recipename
2430        checkvars['SRC_URI'] = url.replace(recipever, '${PV}')
2431        self._test_recipe_contents(newrecipefile, checkvars, [])
2432        # Try again - change just version this time
2433        result = runCmd('devtool reset -n %s' % newrecipename)
2434        add_recipe()
2435        newrecipefile = os.path.join(self.workspacedir, 'recipes', recipename, '%s_%s.bb' % (recipename, newrecipever))
2436        result = runCmd('devtool rename %s -V %s' % (recipename, newrecipever))
2437        self.assertExists(newrecipefile, 'Recipe file not renamed')
2438        self.assertExists(os.path.join(self.workspacedir, 'sources', recipename), 'Source directory no longer exists')
2439        checkvars = {}
2440        checkvars['S'] = '${WORKDIR}/${BPN}-%s' % recipever
2441        checkvars['SRC_URI'] = url
2442        self._test_recipe_contents(newrecipefile, checkvars, [])
2443
2444    def test_devtool_virtual_kernel_modify(self):
2445        """
2446        Summary:        The purpose of this test case is to verify that
2447                        devtool modify works correctly when building
2448                        the kernel.
2449        Dependencies:   NA
2450        Steps:          1. Build kernel with bitbake.
2451                        2. Save the config file generated.
2452                        3. Clean the environment.
2453                        4. Use `devtool modify virtual/kernel` to validate following:
2454                           4.1 The source is checked out correctly.
2455                           4.2 The resulting configuration is the same as
2456                               what was get on step 2.
2457                           4.3 The Kernel can be build correctly.
2458                           4.4 Changes made on the source are reflected on the
2459                               subsequent builds.
2460                           4.5 Changes on the configuration are reflected on the
2461                               subsequent builds
2462         Expected:       devtool modify is able to checkout the source of the kernel
2463                         and modification to the source and configurations are reflected
2464                         when building the kernel.
2465        """
2466        kernel_provider = self.td['PREFERRED_PROVIDER_virtual/kernel']
2467
2468        # Clean up the environment
2469        bitbake('%s -c clean' % kernel_provider)
2470        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
2471        tempdir_cfg = tempfile.mkdtemp(prefix='config_qa')
2472        self.track_for_cleanup(tempdir)
2473        self.track_for_cleanup(tempdir_cfg)
2474        self.track_for_cleanup(self.workspacedir)
2475        self.add_command_to_tearDown('bitbake -c clean %s' % kernel_provider)
2476        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
2477        #Step 1
2478        #Here is just generated the config file instead of all the kernel to optimize the
2479        #time of executing this test case.
2480        bitbake('%s -c configure' % kernel_provider)
2481        bbconfig = os.path.join(get_bb_var('B', kernel_provider),'.config')
2482        #Step 2
2483        runCmd('cp %s %s' % (bbconfig, tempdir_cfg))
2484        self.assertExists(os.path.join(tempdir_cfg, '.config'), 'Could not copy .config file from kernel')
2485
2486        tmpconfig = os.path.join(tempdir_cfg, '.config')
2487        #Step 3
2488        bitbake('%s -c clean' % kernel_provider)
2489        #Step 4.1
2490        runCmd('devtool modify virtual/kernel -x %s' % tempdir)
2491        self.assertExists(os.path.join(tempdir, 'Makefile'), 'Extracted source could not be found')
2492        #Step 4.2
2493        configfile = os.path.join(tempdir,'.config')
2494        runCmd('diff %s %s' % (tmpconfig, configfile))
2495
2496        #Step 4.3
2497        #NOTE: virtual/kernel is mapped to kernel_provider
2498        runCmd('devtool build %s' % kernel_provider)
2499        kernelfile = os.path.join(get_bb_var('KBUILD_OUTPUT', kernel_provider), 'vmlinux')
2500        self.assertExists(kernelfile, 'Kernel was not build correctly')
2501
2502        #Modify the kernel source
2503        modfile = os.path.join(tempdir, 'init/version.c')
2504        # Moved to uts.h in 6.1 onwards
2505        modfile2 = os.path.join(tempdir, 'include/linux/uts.h')
2506        runCmd("sed -i 's/Linux/LiNuX/g' %s %s" % (modfile, modfile2))
2507
2508        #Modify the configuration
2509        codeconfigfile = os.path.join(tempdir, '.config.new')
2510        modconfopt = "CONFIG_SG_POOL=n"
2511        runCmd("sed -i 's/CONFIG_SG_POOL=y/%s/' %s" % (modconfopt, codeconfigfile))
2512
2513        #Build again kernel with devtool
2514        runCmd('devtool build %s' % kernel_provider)
2515
2516        #Step 4.4
2517        runCmd("grep '%s' %s" % ('LiNuX', kernelfile))
2518
2519        #Step 4.5
2520        runCmd("grep %s %s" % (modconfopt, codeconfigfile))
2521
2522
2523class DevtoolIdeSdkTests(DevtoolBase):
2524    def _write_bb_config(self, recipe_names):
2525        """Helper to write the bitbake local.conf file"""
2526        conf_lines = [
2527            'IMAGE_CLASSES += "image-combined-dbg"',
2528            'IMAGE_GEN_DEBUGFS = "1"',
2529            'IMAGE_INSTALL:append = " gdbserver %s"' % ' '.join(
2530                [r + '-ptest' for r in recipe_names])
2531        ]
2532        self.write_config("\n".join(conf_lines))
2533
2534    def _check_workspace(self):
2535        """Check if a workspace directory is available and setup the cleanup"""
2536        self.assertTrue(not os.path.exists(self.workspacedir),
2537                        'This test cannot be run with a workspace directory under the build directory')
2538        self.track_for_cleanup(self.workspacedir)
2539        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
2540
2541    def _workspace_scripts_dir(self, recipe_name):
2542        return os.path.realpath(os.path.join(self.builddir, 'workspace', 'ide-sdk', recipe_name, 'scripts'))
2543
2544    def _sources_scripts_dir(self, src_dir):
2545        return os.path.realpath(os.path.join(src_dir, 'oe-scripts'))
2546
2547    def _workspace_gdbinit_dir(self, recipe_name):
2548        return os.path.realpath(os.path.join(self.builddir, 'workspace', 'ide-sdk', recipe_name, 'scripts', 'gdbinit'))
2549
2550    def _sources_gdbinit_dir(self, src_dir):
2551        return os.path.realpath(os.path.join(src_dir, 'oe-gdbinit'))
2552
2553    def _devtool_ide_sdk_recipe(self, recipe_name, build_file, testimage):
2554        """Setup a recipe for working with devtool ide-sdk
2555
2556        Basically devtool modify -x followed by some tests
2557        """
2558        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
2559        self.track_for_cleanup(tempdir)
2560        self.add_command_to_tearDown('bitbake -c clean %s' % recipe_name)
2561
2562        result = runCmd('devtool modify %s -x %s --debug-build' % (recipe_name, tempdir))
2563        self.assertExists(os.path.join(tempdir, build_file),
2564                          'Extracted source could not be found')
2565        self.assertExists(os.path.join(self.workspacedir, 'conf',
2566                                       'layer.conf'), 'Workspace directory not created')
2567        matches = glob.glob(os.path.join(self.workspacedir,
2568                            'appends', recipe_name + '.bbappend'))
2569        self.assertTrue(matches, 'bbappend not created %s' % result.output)
2570
2571        # Test devtool status
2572        result = runCmd('devtool status')
2573        self.assertIn(recipe_name, result.output)
2574        self.assertIn(tempdir, result.output)
2575        self._check_src_repo(tempdir)
2576
2577        # Usually devtool ide-sdk would initiate the build of the SDK.
2578        # But there is a circular dependency with starting Qemu and passing the IP of runqemu to devtool ide-sdk.
2579        if testimage:
2580            bitbake("%s qemu-native qemu-helper-native" % testimage)
2581            deploy_dir_image = get_bb_var('DEPLOY_DIR_IMAGE')
2582            self.add_command_to_tearDown('bitbake -c clean %s' % testimage)
2583            self.add_command_to_tearDown(
2584                'rm -f %s/%s*' % (deploy_dir_image, testimage))
2585
2586        return tempdir
2587
2588    def _get_recipe_ids(self, recipe_name):
2589        """IDs needed to write recipe specific config entries into IDE config files"""
2590        package_arch = get_bb_var('PACKAGE_ARCH', recipe_name)
2591        recipe_id = recipe_name + "-" + package_arch
2592        recipe_id_pretty = recipe_name + ": " + package_arch
2593        return (recipe_id, recipe_id_pretty)
2594
2595    def _verify_install_script_code(self, tempdir, recipe_name):
2596        """Verify the scripts referred by the tasks.json file are fine.
2597
2598        This function does not depend on Qemu. Therefore it verifies the scripts
2599        exists and the delete step works as expected. But it does not try to
2600        deploy to Qemu.
2601        """
2602        recipe_id, recipe_id_pretty = self._get_recipe_ids(recipe_name)
2603        with open(os.path.join(tempdir, '.vscode', 'tasks.json')) as tasks_j:
2604            tasks_d = json.load(tasks_j)
2605        tasks = tasks_d["tasks"]
2606        task_install = next(
2607            (task for task in tasks if task["label"] == "install && deploy-target %s" % recipe_id_pretty), None)
2608        self.assertIsNot(task_install, None)
2609        # execute only the bb_run_do_install script since the deploy would require e.g. Qemu running.
2610        i_and_d_script = "install_and_deploy_" + recipe_id
2611        i_and_d_script_path = os.path.join(
2612            self._workspace_scripts_dir(recipe_name), i_and_d_script)
2613        self.assertExists(i_and_d_script_path)
2614
2615    def _devtool_ide_sdk_qemu(self, tempdir, qemu, recipe_name, example_exe):
2616        """Verify deployment and execution in Qemu system work for one recipe.
2617
2618        This function checks the entire SDK workflow: changing the code, recompiling
2619        it and deploying it back to Qemu, and checking that the changes have been
2620        incorporated into the provided binaries. It also runs the tests of the recipe.
2621        """
2622        recipe_id, _ = self._get_recipe_ids(recipe_name)
2623        i_and_d_script = "install_and_deploy_" + recipe_id
2624        install_deploy_cmd = os.path.join(
2625            self._workspace_scripts_dir(recipe_name), i_and_d_script)
2626        self.assertExists(install_deploy_cmd,
2627                          '%s script not found' % install_deploy_cmd)
2628        runCmd(install_deploy_cmd)
2629
2630        MAGIC_STRING_ORIG = "Magic: 123456789"
2631        MAGIC_STRING_NEW = "Magic: 987654321"
2632        ptest_cmd = "ptest-runner " + recipe_name
2633
2634        # validate that SSH is working
2635        status, _ = qemu.run("uname")
2636        self.assertEqual(
2637            status, 0, msg="Failed to connect to the SSH server on Qemu")
2638
2639        # Verify the unmodified example prints the magic string
2640        status, output = qemu.run(example_exe)
2641        self.assertEqual(status, 0, msg="%s failed: %s" %
2642                         (example_exe, output))
2643        self.assertIn(MAGIC_STRING_ORIG, output)
2644
2645        # Verify the unmodified ptests work
2646        status, output = qemu.run(ptest_cmd)
2647        self.assertEqual(status, 0, msg="%s failed: %s" % (ptest_cmd, output))
2648        self.assertIn("PASS: cpp-example-lib", output)
2649
2650        # Verify remote debugging works
2651        self._gdb_cross_debugging(
2652            qemu, recipe_name, example_exe, MAGIC_STRING_ORIG)
2653
2654        # Replace the Magic String in the code, compile and deploy to Qemu
2655        cpp_example_lib_hpp = os.path.join(tempdir, 'cpp-example-lib.hpp')
2656        with open(cpp_example_lib_hpp, 'r') as file:
2657            cpp_code = file.read()
2658            cpp_code = cpp_code.replace(MAGIC_STRING_ORIG, MAGIC_STRING_NEW)
2659        with open(cpp_example_lib_hpp, 'w') as file:
2660            file.write(cpp_code)
2661        runCmd(install_deploy_cmd, cwd=tempdir)
2662
2663        # Verify the modified example prints the modified magic string
2664        status, output = qemu.run(example_exe)
2665        self.assertEqual(status, 0, msg="%s failed: %s" %
2666                         (example_exe, output))
2667        self.assertNotIn(MAGIC_STRING_ORIG, output)
2668        self.assertIn(MAGIC_STRING_NEW, output)
2669
2670        # Verify the modified example ptests work
2671        status, output = qemu.run(ptest_cmd)
2672        self.assertEqual(status, 0, msg="%s failed: %s" % (ptest_cmd, output))
2673        self.assertIn("PASS: cpp-example-lib", output)
2674
2675        # Verify remote debugging works wit the modified magic string
2676        self._gdb_cross_debugging(
2677            qemu, recipe_name, example_exe, MAGIC_STRING_NEW)
2678
2679    def _gdb_cross(self):
2680        """Verify gdb-cross is provided by devtool ide-sdk"""
2681        target_arch = self.td["TARGET_ARCH"]
2682        target_sys = self.td["TARGET_SYS"]
2683        gdb_recipe = "gdb-cross-" + target_arch
2684        gdb_binary = target_sys + "-gdb"
2685
2686        native_sysroot = get_bb_var("RECIPE_SYSROOT_NATIVE", gdb_recipe)
2687        r = runCmd("%s --version" % gdb_binary,
2688                   native_sysroot=native_sysroot, target_sys=target_sys)
2689        self.assertEqual(r.status, 0)
2690        self.assertIn("GNU gdb", r.output)
2691
2692    def _gdb_cross_debugging(self, qemu, recipe_name, example_exe, magic_string):
2693        """Verify gdb-cross is working
2694
2695        Test remote debugging:
2696        break main
2697        run
2698        continue
2699        break CppExample::print_json()
2700        continue
2701        print CppExample::test_string.compare("cpp-example-lib Magic: 123456789")
2702        $1 = 0
2703        print CppExample::test_string.compare("cpp-example-lib Magic: 123456789aaa")
2704        $2 = -3
2705        list cpp-example-lib.hpp:13,13
2706        13	    inline static const std::string test_string = "cpp-example-lib Magic: 123456789";
2707        continue
2708        """
2709        sshargs = '-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no'
2710        gdbserver_script = os.path.join(self._workspace_scripts_dir(
2711            recipe_name), 'gdbserver_1234_usr-bin-' + example_exe + '_m')
2712        gdb_script = os.path.join(self._workspace_scripts_dir(
2713            recipe_name), 'gdb_1234_usr-bin-' + example_exe)
2714
2715        # Start a gdbserver
2716        r = runCmd(gdbserver_script)
2717        self.assertEqual(r.status, 0)
2718
2719        # Check there is a gdbserver running
2720        r = runCmd('ssh %s root@%s %s' % (sshargs, qemu.ip, 'ps'))
2721        self.assertEqual(r.status, 0)
2722        self.assertIn("gdbserver ", r.output)
2723
2724        # Check the pid file is correct
2725        test_cmd = "cat /proc/$(cat /tmp/gdbserver_1234_usr-bin-" + \
2726            example_exe + "/pid)/cmdline"
2727        r = runCmd('ssh %s root@%s %s' % (sshargs, qemu.ip, test_cmd))
2728        self.assertEqual(r.status, 0)
2729        self.assertIn("gdbserver", r.output)
2730
2731        # Test remote debugging works
2732        gdb_batch_cmd = " --batch -ex 'break main' -ex 'run'"
2733        gdb_batch_cmd += " -ex 'break CppExample::print_json()' -ex 'continue'"
2734        gdb_batch_cmd += " -ex 'print CppExample::test_string.compare(\"cpp-example-lib %s\")'" % magic_string
2735        gdb_batch_cmd += " -ex 'print CppExample::test_string.compare(\"cpp-example-lib %saaa\")'" % magic_string
2736        gdb_batch_cmd += " -ex 'list cpp-example-lib.hpp:13,13'"
2737        gdb_batch_cmd += " -ex 'continue'"
2738        r = runCmd(gdb_script + gdb_batch_cmd)
2739        self.logger.debug("%s %s returned: %s", gdb_script,
2740                          gdb_batch_cmd, r.output)
2741        self.assertEqual(r.status, 0)
2742        self.assertIn("Breakpoint 1, main", r.output)
2743        self.assertIn("$1 = 0", r.output)  # test.string.compare equal
2744        self.assertIn("$2 = -3", r.output)  # test.string.compare longer
2745        self.assertIn(
2746            'inline static const std::string test_string = "cpp-example-lib %s";' % magic_string, r.output)
2747        self.assertIn("exited normally", r.output)
2748
2749        # Stop the gdbserver
2750        r = runCmd(gdbserver_script + ' stop')
2751        self.assertEqual(r.status, 0)
2752
2753        # Check there is no gdbserver running
2754        r = runCmd('ssh %s root@%s %s' % (sshargs, qemu.ip, 'ps'))
2755        self.assertEqual(r.status, 0)
2756        self.assertNotIn("gdbserver ", r.output)
2757
2758    def _verify_cmake_preset(self, tempdir):
2759        """Verify the generated cmake preset works as expected
2760
2761        Check if compiling works
2762        Check if unit tests can be executed in qemu (not qemu-system)
2763        """
2764        with open(os.path.join(tempdir, 'CMakeUserPresets.json')) as cmake_preset_j:
2765            cmake_preset_d = json.load(cmake_preset_j)
2766        config_presets = cmake_preset_d["configurePresets"]
2767        self.assertEqual(len(config_presets), 1)
2768        cmake_exe = config_presets[0]["cmakeExecutable"]
2769        preset_name = config_presets[0]["name"]
2770
2771        # Verify the wrapper for cmake native is available
2772        self.assertExists(cmake_exe)
2773
2774        # Verify the cmake preset generated by devtool ide-sdk is available
2775        result = runCmd('%s --list-presets' % cmake_exe, cwd=tempdir)
2776        self.assertIn(preset_name, result.output)
2777
2778        # Verify cmake re-uses the o files compiled by bitbake
2779        result = runCmd('%s --build --preset %s' %
2780                        (cmake_exe, preset_name), cwd=tempdir)
2781        self.assertIn("ninja: no work to do.", result.output)
2782
2783        # Verify the unit tests work (in Qemu user mode)
2784        result = runCmd('%s --build --preset %s --target test' %
2785                        (cmake_exe, preset_name), cwd=tempdir)
2786        self.assertIn("100% tests passed", result.output)
2787
2788        # Verify re-building and testing works again
2789        result = runCmd('%s --build --preset %s --target clean' %
2790                        (cmake_exe, preset_name), cwd=tempdir)
2791        self.assertIn("Cleaning", result.output)
2792        result = runCmd('%s --build --preset %s' %
2793                        (cmake_exe, preset_name), cwd=tempdir)
2794        self.assertIn("Building", result.output)
2795        self.assertIn("Linking", result.output)
2796        result = runCmd('%s --build --preset %s --target test' %
2797                        (cmake_exe, preset_name), cwd=tempdir)
2798        self.assertIn("Running tests...", result.output)
2799        self.assertIn("100% tests passed", result.output)
2800
2801    @OETestTag("runqemu")
2802    def test_devtool_ide_sdk_none_qemu(self):
2803        """Start qemu-system and run tests for multiple recipes. ide=none is used."""
2804        recipe_names = ["cmake-example", "meson-example"]
2805        testimage = "oe-selftest-image"
2806
2807        self._check_workspace()
2808        self._write_bb_config(recipe_names)
2809        self._check_runqemu_prerequisites()
2810
2811        # Verify deployment to Qemu (system mode) works
2812        bitbake(testimage)
2813        with runqemu(testimage, runqemuparams="nographic") as qemu:
2814            # cmake-example recipe
2815            recipe_name = "cmake-example"
2816            example_exe = "cmake-example"
2817            build_file = "CMakeLists.txt"
2818            tempdir = self._devtool_ide_sdk_recipe(
2819                recipe_name, build_file, testimage)
2820            bitbake_sdk_cmd = 'devtool ide-sdk %s %s -t root@%s -c --ide=none' % (
2821                recipe_name, testimage, qemu.ip)
2822            runCmd(bitbake_sdk_cmd)
2823            self._gdb_cross()
2824            self._verify_cmake_preset(tempdir)
2825            self._devtool_ide_sdk_qemu(tempdir, qemu, recipe_name, example_exe)
2826            # Verify the oe-scripts sym-link is valid
2827            self.assertEqual(self._workspace_scripts_dir(
2828                recipe_name), self._sources_scripts_dir(tempdir))
2829
2830            # meson-example recipe
2831            recipe_name = "meson-example"
2832            example_exe = "mesonex"
2833            build_file = "meson.build"
2834            tempdir = self._devtool_ide_sdk_recipe(
2835                recipe_name, build_file, testimage)
2836            bitbake_sdk_cmd = 'devtool ide-sdk %s %s -t root@%s -c --ide=none' % (
2837                recipe_name, testimage, qemu.ip)
2838            runCmd(bitbake_sdk_cmd)
2839            self._gdb_cross()
2840            self._devtool_ide_sdk_qemu(tempdir, qemu, recipe_name, example_exe)
2841            # Verify the oe-scripts sym-link is valid
2842            self.assertEqual(self._workspace_scripts_dir(
2843                recipe_name), self._sources_scripts_dir(tempdir))
2844
2845    def test_devtool_ide_sdk_code_cmake(self):
2846        """Verify a cmake recipe works with ide=code mode"""
2847        recipe_name = "cmake-example"
2848        build_file = "CMakeLists.txt"
2849        testimage = "oe-selftest-image"
2850
2851        self._check_workspace()
2852        self._write_bb_config([recipe_name])
2853        tempdir = self._devtool_ide_sdk_recipe(
2854            recipe_name, build_file, testimage)
2855        bitbake_sdk_cmd = 'devtool ide-sdk %s %s -t root@192.168.17.17 -c --ide=code' % (
2856            recipe_name, testimage)
2857        runCmd(bitbake_sdk_cmd)
2858        self._verify_cmake_preset(tempdir)
2859        self._verify_install_script_code(tempdir,  recipe_name)
2860        self._gdb_cross()
2861
2862    def test_devtool_ide_sdk_code_meson(self):
2863        """Verify a meson recipe works with ide=code mode"""
2864        recipe_name = "meson-example"
2865        build_file = "meson.build"
2866        testimage = "oe-selftest-image"
2867
2868        self._check_workspace()
2869        self._write_bb_config([recipe_name])
2870        tempdir = self._devtool_ide_sdk_recipe(
2871            recipe_name, build_file, testimage)
2872        bitbake_sdk_cmd = 'devtool ide-sdk %s %s -t root@192.168.17.17 -c --ide=code' % (
2873            recipe_name, testimage)
2874        runCmd(bitbake_sdk_cmd)
2875
2876        with open(os.path.join(tempdir, '.vscode', 'settings.json')) as settings_j:
2877            settings_d = json.load(settings_j)
2878        meson_exe = settings_d["mesonbuild.mesonPath"]
2879        meson_build_folder = settings_d["mesonbuild.buildFolder"]
2880
2881        # Verify the wrapper for meson native is available
2882        self.assertExists(meson_exe)
2883
2884        # Verify meson re-uses the o files compiled by bitbake
2885        result = runCmd('%s compile -C  %s' %
2886                        (meson_exe, meson_build_folder), cwd=tempdir)
2887        self.assertIn("ninja: no work to do.", result.output)
2888
2889        # Verify the unit tests work (in Qemu)
2890        runCmd('%s test -C %s' % (meson_exe, meson_build_folder), cwd=tempdir)
2891
2892        # Verify re-building and testing works again
2893        result = runCmd('%s compile -C  %s --clean' %
2894                        (meson_exe, meson_build_folder), cwd=tempdir)
2895        self.assertIn("Cleaning...", result.output)
2896        result = runCmd('%s compile -C  %s' %
2897                        (meson_exe, meson_build_folder), cwd=tempdir)
2898        self.assertIn("Linking target", result.output)
2899        runCmd('%s test -C %s' % (meson_exe, meson_build_folder), cwd=tempdir)
2900
2901        self._verify_install_script_code(tempdir,  recipe_name)
2902        self._gdb_cross()
2903
2904    def test_devtool_ide_sdk_shared_sysroots(self):
2905        """Verify the shared sysroot SDK"""
2906
2907        # Handle the workspace (which is not needed by this test case)
2908        self._check_workspace()
2909
2910        result_init = runCmd(
2911            'devtool ide-sdk -m shared oe-selftest-image cmake-example meson-example --ide=code')
2912        bb_vars = get_bb_vars(
2913            ['REAL_MULTIMACH_TARGET_SYS', 'DEPLOY_DIR_IMAGE', 'COREBASE'], "meta-ide-support")
2914        environment_script = 'environment-setup-%s' % bb_vars['REAL_MULTIMACH_TARGET_SYS']
2915        deploydir = bb_vars['DEPLOY_DIR_IMAGE']
2916        environment_script_path = os.path.join(deploydir, environment_script)
2917        cpp_example_src = os.path.join(
2918            bb_vars['COREBASE'], 'meta-selftest', 'recipes-test', 'cpp', 'files')
2919
2920        # Verify the cross environment script is available
2921        self.assertExists(environment_script_path)
2922
2923        def runCmdEnv(cmd, cwd):
2924            cmd = '/bin/sh -c ". %s > /dev/null && %s"' % (
2925                environment_script_path, cmd)
2926            return runCmd(cmd, cwd)
2927
2928        # Verify building the C++ example works with CMake
2929        tempdir_cmake = tempfile.mkdtemp(prefix='devtoolqa')
2930        self.track_for_cleanup(tempdir_cmake)
2931
2932        result_cmake = runCmdEnv("which cmake", cwd=tempdir_cmake)
2933        cmake_native = os.path.normpath(result_cmake.output.strip())
2934        self.assertExists(cmake_native)
2935
2936        runCmdEnv('cmake %s' % cpp_example_src, cwd=tempdir_cmake)
2937        runCmdEnv('cmake --build %s' % tempdir_cmake, cwd=tempdir_cmake)
2938
2939        # Verify the printed note really referres to a cmake executable
2940        cmake_native_code = ""
2941        for line in result_init.output.splitlines():
2942            m = re.search(r'"cmake.cmakePath": "(.*)"', line)
2943            if m:
2944                cmake_native_code = m.group(1)
2945                break
2946        self.assertExists(cmake_native_code)
2947        self.assertEqual(cmake_native, cmake_native_code)
2948
2949        # Verify building the C++ example works with Meson
2950        tempdir_meson = tempfile.mkdtemp(prefix='devtoolqa')
2951        self.track_for_cleanup(tempdir_meson)
2952
2953        result_cmake = runCmdEnv("which meson", cwd=tempdir_meson)
2954        meson_native = os.path.normpath(result_cmake.output.strip())
2955        self.assertExists(meson_native)
2956
2957        runCmdEnv('meson setup %s' % tempdir_meson, cwd=cpp_example_src)
2958        runCmdEnv('meson compile', cwd=tempdir_meson)
2959
2960    def test_devtool_ide_sdk_plugins(self):
2961        """Test that devtool ide-sdk can use plugins from other layers."""
2962
2963        # We need a workspace layer and a modified recipe (but no image)
2964        modified_recipe_name = "meson-example"
2965        modified_build_file = "meson.build"
2966        testimage = "oe-selftest-image"
2967        shared_recipe_name = "cmake-example"
2968
2969        self._check_workspace()
2970        self._write_bb_config([modified_recipe_name])
2971        tempdir = self._devtool_ide_sdk_recipe(
2972            modified_recipe_name, modified_build_file, None)
2973
2974        IDE_RE = re.compile(r'.*--ide \{(.*)\}.*')
2975
2976        def get_ides_from_help(help_str):
2977            m = IDE_RE.search(help_str)
2978            return m.group(1).split(',')
2979
2980        # verify the default plugins are available but the foo plugin is not
2981        result = runCmd('devtool ide-sdk -h')
2982        found_ides = get_ides_from_help(result.output)
2983        self.assertIn('code', found_ides)
2984        self.assertIn('none', found_ides)
2985        self.assertNotIn('foo', found_ides)
2986
2987        shared_config_file = os.path.join(tempdir, 'shared-config.txt')
2988        shared_config_str = 'Dummy shared IDE config'
2989        modified_config_file = os.path.join(tempdir, 'modified-config.txt')
2990        modified_config_str = 'Dummy modified IDE config'
2991
2992        # Generate a foo plugin in the workspace layer
2993        plugin_dir = os.path.join(
2994            self.workspacedir, 'lib', 'devtool', 'ide_plugins')
2995        os.makedirs(plugin_dir)
2996        plugin_code = 'from devtool.ide_plugins import IdeBase\n\n'
2997        plugin_code += 'class IdeFoo(IdeBase):\n'
2998        plugin_code += '    def setup_shared_sysroots(self, shared_env):\n'
2999        plugin_code += '        with open("%s", "w") as config_file:\n' % shared_config_file
3000        plugin_code += '            config_file.write("%s")\n\n' % shared_config_str
3001        plugin_code += '    def setup_modified_recipe(self, args, image_recipe, modified_recipe):\n'
3002        plugin_code += '        with open("%s", "w") as config_file:\n' % modified_config_file
3003        plugin_code += '            config_file.write("%s")\n\n' % modified_config_str
3004        plugin_code += 'def register_ide_plugin(ide_plugins):\n'
3005        plugin_code += '    ide_plugins["foo"] = IdeFoo\n'
3006
3007        plugin_py = os.path.join(plugin_dir, 'ide_foo.py')
3008        with open(plugin_py, 'w') as plugin_file:
3009            plugin_file.write(plugin_code)
3010
3011        # Verify the foo plugin is available as well
3012        result = runCmd('devtool ide-sdk -h')
3013        found_ides = get_ides_from_help(result.output)
3014        self.assertIn('code', found_ides)
3015        self.assertIn('none', found_ides)
3016        self.assertIn('foo', found_ides)
3017
3018        # Verify the foo plugin generates a shared config
3019        result = runCmd(
3020            'devtool ide-sdk -m shared --skip-bitbake --ide foo %s' % shared_recipe_name)
3021        with open(shared_config_file) as shared_config:
3022            shared_config_new = shared_config.read()
3023        self.assertEqual(shared_config_str, shared_config_new)
3024
3025        # Verify the foo plugin generates a modified config
3026        result = runCmd('devtool ide-sdk --skip-bitbake --ide foo %s %s' %
3027                        (modified_recipe_name, testimage))
3028        with open(modified_config_file) as modified_config:
3029            modified_config_new = modified_config.read()
3030        self.assertEqual(modified_config_str, modified_config_new)
3031