xref: /openbmc/openbmc/poky/meta/lib/oeqa/selftest/cases/devtool.py (revision 96e4b4e121e0e2da1535d7d537d6a982a6ff5bc0)
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')
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        self.add_command_to_tearDown('cd %s; rm %s/*.patch; git checkout %s %s' % (os.path.dirname(recipefile), testrecipe, testrecipe, os.path.basename(recipefile)))
1189        result = runCmd('devtool update-recipe %s' % testrecipe)
1190        result = runCmd('git add minicom', cwd=os.path.dirname(recipefile))
1191        expected_status = [(' M', '.*/%s$' % os.path.basename(recipefile)),
1192                           ('A ', '.*/0001-Change-the-README.patch$'),
1193                           ('A ', '.*/0002-Add-a-new-file.patch$')]
1194        self._check_repo_status(os.path.dirname(recipefile), expected_status)
1195
1196    def test_devtool_update_recipe_git(self):
1197        # Check preconditions
1198        testrecipe = 'mtd-utils-selftest'
1199        bb_vars = get_bb_vars(['FILE', 'SRC_URI'], testrecipe)
1200        recipefile = bb_vars['FILE']
1201        src_uri = bb_vars['SRC_URI']
1202        self.assertIn('git://', src_uri, 'This test expects the %s recipe to be a git recipe' % testrecipe)
1203        patches = []
1204        for entry in src_uri.split():
1205            if entry.startswith('file://') and entry.endswith('.patch'):
1206                patches.append(entry[7:].split(';')[0])
1207        self.assertGreater(len(patches), 0, 'The %s recipe does not appear to contain any patches, so this test will not be effective' % testrecipe)
1208        self._check_repo_status(os.path.dirname(recipefile), [])
1209        # First, modify a recipe
1210        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
1211        self.track_for_cleanup(tempdir)
1212        self.track_for_cleanup(self.workspacedir)
1213        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
1214        # (don't bother with cleaning the recipe on teardown, we won't be building it)
1215        result = runCmd('devtool modify %s -x %s' % (testrecipe, tempdir))
1216        # Check git repo
1217        self._check_src_repo(tempdir)
1218        # Add a couple of commits
1219        # FIXME: this only tests adding, need to also test update and remove
1220        result = runCmd('echo "# Additional line" >> Makefile.am', cwd=tempdir)
1221        result = runCmd('git commit -a -m "Change the Makefile"', cwd=tempdir)
1222        result = runCmd('echo "A new file" > devtool-new-file', cwd=tempdir)
1223        result = runCmd('git add devtool-new-file', cwd=tempdir)
1224        result = runCmd('git commit -m "Add a new file"', cwd=tempdir)
1225        self.add_command_to_tearDown('cd %s; rm -rf %s; git checkout %s %s' % (os.path.dirname(recipefile), testrecipe, testrecipe, os.path.basename(recipefile)))
1226        result = runCmd('devtool update-recipe -m srcrev %s' % testrecipe)
1227        expected_status = [(' M', '.*/%s$' % os.path.basename(recipefile))] + \
1228                          [(' D', '.*/%s$' % patch) for patch in patches]
1229        self._check_repo_status(os.path.dirname(recipefile), expected_status)
1230
1231        result = runCmd('git diff %s' % os.path.basename(recipefile), cwd=os.path.dirname(recipefile))
1232        addlines = ['SRCREV = ".*"', 'SRC_URI = "git://git.infradead.org/mtd-utils.git;branch=master"']
1233        srcurilines = src_uri.split()
1234        srcurilines[0] = 'SRC_URI = "' + srcurilines[0]
1235        srcurilines.append('"')
1236        removelines = ['SRCREV = ".*"'] + srcurilines
1237        self._check_diff(result.output, addlines, removelines)
1238        # Now try with auto mode
1239        runCmd('cd %s; git checkout %s %s' % (os.path.dirname(recipefile), testrecipe, os.path.basename(recipefile)))
1240        result = runCmd('devtool update-recipe %s' % testrecipe)
1241        result = runCmd('git rev-parse --show-toplevel', cwd=os.path.dirname(recipefile))
1242        topleveldir = result.output.strip()
1243        relpatchpath = os.path.join(os.path.relpath(os.path.dirname(recipefile), topleveldir), testrecipe)
1244        expected_status = [(' M', os.path.relpath(recipefile, topleveldir)),
1245                           ('??', '%s/0001-Change-the-Makefile.patch' % relpatchpath),
1246                           ('??', '%s/0002-Add-a-new-file.patch' % relpatchpath)]
1247        self._check_repo_status(os.path.dirname(recipefile), expected_status)
1248
1249    def test_devtool_update_recipe_append(self):
1250        # Check preconditions
1251        testrecipe = 'mdadm'
1252        bb_vars = get_bb_vars(['FILE', 'SRC_URI'], testrecipe)
1253        recipefile = bb_vars['FILE']
1254        src_uri = bb_vars['SRC_URI']
1255        self.assertNotIn('git://', src_uri, 'This test expects the %s recipe to NOT be a git recipe' % testrecipe)
1256        self._check_repo_status(os.path.dirname(recipefile), [])
1257        # First, modify a recipe
1258        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
1259        tempsrcdir = os.path.join(tempdir, 'source')
1260        templayerdir = os.path.join(tempdir, 'layer')
1261        self.track_for_cleanup(tempdir)
1262        self.track_for_cleanup(self.workspacedir)
1263        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
1264        # (don't bother with cleaning the recipe on teardown, we won't be building it)
1265        result = runCmd('devtool modify %s -x %s' % (testrecipe, tempsrcdir))
1266        # Check git repo
1267        self._check_src_repo(tempsrcdir)
1268        # Add a commit
1269        result = runCmd("sed 's!\\(#define VERSION\\W*\"[^\"]*\\)\"!\\1-custom\"!' -i ReadMe.c", cwd=tempsrcdir)
1270        result = runCmd('git commit -a -m "Add our custom version"', cwd=tempsrcdir)
1271        self.add_command_to_tearDown('cd %s; rm -f %s/*.patch; git checkout .' % (os.path.dirname(recipefile), testrecipe))
1272        # Create a temporary layer and add it to bblayers.conf
1273        self._create_temp_layer(templayerdir, True, 'selftestupdaterecipe')
1274        # Create the bbappend
1275        result = runCmd('devtool update-recipe %s -a %s' % (testrecipe, templayerdir))
1276        self.assertNotIn('WARNING:', result.output)
1277        # Check recipe is still clean
1278        self._check_repo_status(os.path.dirname(recipefile), [])
1279        # Check bbappend was created
1280        splitpath = os.path.dirname(recipefile).split(os.sep)
1281        appenddir = os.path.join(templayerdir, splitpath[-2], splitpath[-1])
1282        bbappendfile = self._check_bbappend(testrecipe, recipefile, appenddir)
1283        patchfile = os.path.join(appenddir, testrecipe, '0001-Add-our-custom-version.patch')
1284        self.assertExists(patchfile, 'Patch file not created')
1285
1286        # Check bbappend contents
1287        expectedlines = ['FILESEXTRAPATHS:prepend := "${THISDIR}/${PN}:"\n',
1288                         '\n',
1289                         'SRC_URI += "file://0001-Add-our-custom-version.patch"\n',
1290                         '\n']
1291        with open(bbappendfile, 'r') as f:
1292            self.assertEqual(expectedlines, f.readlines())
1293
1294        # Check we can run it again and bbappend isn't modified
1295        result = runCmd('devtool update-recipe %s -a %s' % (testrecipe, templayerdir))
1296        with open(bbappendfile, 'r') as f:
1297            self.assertEqual(expectedlines, f.readlines())
1298        # Drop new commit and check patch gets deleted
1299        result = runCmd('git reset HEAD^ --hard', cwd=tempsrcdir)
1300        result = runCmd('devtool update-recipe %s -a %s' % (testrecipe, templayerdir))
1301        self.assertNotExists(patchfile, 'Patch file not deleted')
1302        expectedlines2 = ['FILESEXTRAPATHS:prepend := "${THISDIR}/${PN}:"\n',
1303                         '\n']
1304        with open(bbappendfile, 'r') as f:
1305            self.assertEqual(expectedlines2, f.readlines())
1306        # Put commit back and check we can run it if layer isn't in bblayers.conf
1307        os.remove(bbappendfile)
1308        result = runCmd("sed 's!\\(#define VERSION\\W*\"[^\"]*\\)\"!\\1-custom\"!' -i ReadMe.c", cwd=tempsrcdir)
1309        result = runCmd('git commit -a -m "Add our custom version"', cwd=tempsrcdir)
1310        result = runCmd('bitbake-layers remove-layer %s' % templayerdir, cwd=self.builddir)
1311        result = runCmd('devtool update-recipe %s -a %s' % (testrecipe, templayerdir))
1312        self.assertIn('WARNING: Specified layer is not currently enabled in bblayers.conf', result.output)
1313        self.assertExists(patchfile, 'Patch file not created (with disabled layer)')
1314        with open(bbappendfile, 'r') as f:
1315            self.assertEqual(expectedlines, f.readlines())
1316        # Deleting isn't expected to work under these circumstances
1317
1318    def test_devtool_update_recipe_append_git(self):
1319        # Check preconditions
1320        testrecipe = 'mtd-utils-selftest'
1321        bb_vars = get_bb_vars(['FILE', 'SRC_URI', 'LAYERSERIES_CORENAMES'], testrecipe)
1322        recipefile = bb_vars['FILE']
1323        src_uri = bb_vars['SRC_URI']
1324        corenames = bb_vars['LAYERSERIES_CORENAMES']
1325        self.assertIn('git://', src_uri, 'This test expects the %s recipe to be a git recipe' % testrecipe)
1326        for entry in src_uri.split():
1327            if entry.startswith('git://'):
1328                git_uri = entry
1329                break
1330        self._check_repo_status(os.path.dirname(recipefile), [])
1331        # First, modify a recipe
1332        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
1333        tempsrcdir = os.path.join(tempdir, 'source')
1334        templayerdir = os.path.join(tempdir, 'layer')
1335        self.track_for_cleanup(tempdir)
1336        self.track_for_cleanup(self.workspacedir)
1337        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
1338        # (don't bother with cleaning the recipe on teardown, we won't be building it)
1339        result = runCmd('devtool modify %s -x %s' % (testrecipe, tempsrcdir))
1340        # Check git repo
1341        self._check_src_repo(tempsrcdir)
1342        # Add a commit
1343        result = runCmd('echo "# Additional line" >> Makefile.am', cwd=tempsrcdir)
1344        result = runCmd('git commit -a -m "Change the Makefile"', cwd=tempsrcdir)
1345        self.add_command_to_tearDown('cd %s; rm -f %s/*.patch; git checkout .' % (os.path.dirname(recipefile), testrecipe))
1346        # Create a temporary layer
1347        os.makedirs(os.path.join(templayerdir, 'conf'))
1348        with open(os.path.join(templayerdir, 'conf', 'layer.conf'), 'w') as f:
1349            f.write('BBPATH .= ":${LAYERDIR}"\n')
1350            f.write('BBFILES += "${LAYERDIR}/recipes-*/*/*.bbappend"\n')
1351            f.write('BBFILE_COLLECTIONS += "oeselftesttemplayer"\n')
1352            f.write('BBFILE_PATTERN_oeselftesttemplayer = "^${LAYERDIR}/"\n')
1353            f.write('BBFILE_PRIORITY_oeselftesttemplayer = "999"\n')
1354            f.write('BBFILE_PATTERN_IGNORE_EMPTY_oeselftesttemplayer = "1"\n')
1355            f.write('LAYERSERIES_COMPAT_oeselftesttemplayer = "%s"\n' % corenames)
1356        self.add_command_to_tearDown('bitbake-layers remove-layer %s || true' % templayerdir)
1357        result = runCmd('bitbake-layers add-layer %s' % templayerdir, cwd=self.builddir)
1358        # Create the bbappend
1359        result = runCmd('devtool update-recipe -m srcrev %s -a %s' % (testrecipe, templayerdir))
1360        self.assertNotIn('WARNING:', result.output)
1361        # Check recipe is still clean
1362        self._check_repo_status(os.path.dirname(recipefile), [])
1363        # Check bbappend was created
1364        splitpath = os.path.dirname(recipefile).split(os.sep)
1365        appenddir = os.path.join(templayerdir, splitpath[-2], splitpath[-1])
1366        bbappendfile = self._check_bbappend(testrecipe, recipefile, appenddir)
1367        self.assertNotExists(os.path.join(appenddir, testrecipe), 'Patch directory should not be created')
1368
1369        # Check bbappend contents
1370        result = runCmd('git rev-parse HEAD', cwd=tempsrcdir)
1371        expectedlines = set(['SRCREV = "%s"\n' % result.output,
1372                             '\n',
1373                             'SRC_URI = "%s"\n' % git_uri,
1374                             '\n'])
1375        with open(bbappendfile, 'r') as f:
1376            self.assertEqual(expectedlines, set(f.readlines()))
1377
1378        # Check we can run it again and bbappend isn't modified
1379        result = runCmd('devtool update-recipe -m srcrev %s -a %s' % (testrecipe, templayerdir))
1380        with open(bbappendfile, 'r') as f:
1381            self.assertEqual(expectedlines, set(f.readlines()))
1382        # Drop new commit and check SRCREV changes
1383        result = runCmd('git reset HEAD^ --hard', cwd=tempsrcdir)
1384        result = runCmd('devtool update-recipe -m srcrev %s -a %s' % (testrecipe, templayerdir))
1385        self.assertNotExists(os.path.join(appenddir, testrecipe), 'Patch directory should not be created')
1386        result = runCmd('git rev-parse HEAD', cwd=tempsrcdir)
1387        expectedlines = set(['SRCREV = "%s"\n' % result.output,
1388                             '\n',
1389                             'SRC_URI = "%s"\n' % git_uri,
1390                             '\n'])
1391        with open(bbappendfile, 'r') as f:
1392            self.assertEqual(expectedlines, set(f.readlines()))
1393        # Put commit back and check we can run it if layer isn't in bblayers.conf
1394        os.remove(bbappendfile)
1395        result = runCmd('echo "# Additional line" >> Makefile.am', cwd=tempsrcdir)
1396        result = runCmd('git commit -a -m "Change the Makefile"', cwd=tempsrcdir)
1397        result = runCmd('bitbake-layers remove-layer %s' % templayerdir, cwd=self.builddir)
1398        result = runCmd('devtool update-recipe -m srcrev %s -a %s' % (testrecipe, templayerdir))
1399        self.assertIn('WARNING: Specified layer is not currently enabled in bblayers.conf', result.output)
1400        self.assertNotExists(os.path.join(appenddir, testrecipe), 'Patch directory should not be created')
1401        result = runCmd('git rev-parse HEAD', cwd=tempsrcdir)
1402        expectedlines = set(['SRCREV = "%s"\n' % result.output,
1403                             '\n',
1404                             'SRC_URI = "%s"\n' % git_uri,
1405                             '\n'])
1406        with open(bbappendfile, 'r') as f:
1407            self.assertEqual(expectedlines, set(f.readlines()))
1408        # Deleting isn't expected to work under these circumstances
1409
1410    def test_devtool_update_recipe_local_files(self):
1411        """Check that local source files are copied over instead of patched"""
1412        testrecipe = 'makedevs'
1413        recipefile = get_bb_var('FILE', testrecipe)
1414        # Setup srctree for modifying the recipe
1415        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
1416        self.track_for_cleanup(tempdir)
1417        self.track_for_cleanup(self.workspacedir)
1418        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
1419        # (don't bother with cleaning the recipe on teardown, we won't be
1420        # building it)
1421        result = runCmd('devtool modify %s -x %s' % (testrecipe, tempdir))
1422        # Check git repo
1423        self._check_src_repo(tempdir)
1424        # Try building just to ensure we haven't broken that
1425        bitbake("%s" % testrecipe)
1426        # Edit / commit local source
1427        runCmd('echo "/* Foobar */" >> makedevs.c', cwd=tempdir)
1428        runCmd('echo "Foo" > new-local', cwd=tempdir)
1429        runCmd('echo "Bar" > new-file', cwd=tempdir)
1430        runCmd('git add new-file', cwd=tempdir)
1431        runCmd('git commit -m "Add new file"', cwd=tempdir)
1432        runCmd('git add new-local', cwd=tempdir)
1433        runCmd('devtool update-recipe %s' % testrecipe)
1434        expected_status = [(' M', '.*/%s$' % os.path.basename(recipefile)),
1435                           (' M', '.*/makedevs/makedevs.c$'),
1436                           ('??', '.*/makedevs/new-local$'),
1437                           ('??', '.*/makedevs/0001-Add-new-file.patch$')]
1438        self._check_repo_status(os.path.dirname(recipefile), expected_status)
1439        # Now try to update recipe in another layer, so first, clean it
1440        runCmd('cd %s; git clean -fd .; git checkout .' % os.path.dirname(recipefile))
1441        # Create a temporary layer and add it to bblayers.conf
1442        self._create_temp_layer(templayerdir, True, 'templayer')
1443        # Update recipe in templayer
1444        result = runCmd('devtool update-recipe %s -a %s' % (testrecipe, templayerdir))
1445        self.assertNotIn('WARNING:', result.output)
1446        # Check recipe is still clean
1447        self._check_repo_status(os.path.dirname(recipefile), [])
1448        splitpath = os.path.dirname(recipefile).split(os.sep)
1449        appenddir = os.path.join(templayerdir, splitpath[-2], splitpath[-1])
1450        bbappendfile = self._check_bbappend(testrecipe, recipefile, appenddir)
1451        patchfile = os.path.join(appenddir, testrecipe, '0001-Add-new-file.patch')
1452        new_local_file = os.path.join(appenddir, testrecipe, 'new_local')
1453        local_file = os.path.join(appenddir, testrecipe, 'makedevs.c')
1454        self.assertExists(patchfile, 'Patch file 0001-Add-new-file.patch not created')
1455        self.assertExists(local_file, 'File makedevs.c not created')
1456        self.assertExists(patchfile, 'File new_local not created')
1457
1458    def _test_devtool_update_recipe_local_files_2(self):
1459        """Check local source files support when editing local files in Git"""
1460        testrecipe = 'devtool-test-local'
1461        recipefile = get_bb_var('FILE', testrecipe)
1462        recipedir = os.path.dirname(recipefile)
1463        result = runCmd('git status --porcelain .', cwd=recipedir)
1464        if result.output.strip():
1465            self.fail('Recipe directory for %s contains uncommitted changes' % testrecipe)
1466        # Setup srctree for modifying the recipe
1467        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
1468        self.track_for_cleanup(tempdir)
1469        self.track_for_cleanup(self.workspacedir)
1470        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
1471        result = runCmd('devtool modify %s -x %s' % (testrecipe, tempdir))
1472        # Check git repo
1473        self._check_src_repo(tempdir)
1474        # Edit / commit local sources
1475        runCmd('echo "# Foobar" >> file1', cwd=tempdir)
1476        runCmd('git commit -am "Edit existing file"', cwd=tempdir)
1477        runCmd('git rm file2', cwd=tempdir)
1478        runCmd('git commit -m"Remove file"', cwd=tempdir)
1479        runCmd('echo "Foo" > new-local', cwd=tempdir)
1480        runCmd('git add new-local', cwd=tempdir)
1481        runCmd('git commit -m "Add new local file"', cwd=tempdir)
1482        runCmd('echo "Gar" > new-file', cwd=tempdir)
1483        runCmd('git add new-file', cwd=tempdir)
1484        runCmd('git commit -m "Add new file"', cwd=tempdir)
1485        self.add_command_to_tearDown('cd %s; git clean -fd .; git checkout .' %
1486                                     os.path.dirname(recipefile))
1487        # Checkout unmodified file to working copy -> devtool should still pick
1488        # the modified version from HEAD
1489        runCmd('git checkout HEAD^ -- file1', cwd=tempdir)
1490        runCmd('devtool update-recipe %s' % testrecipe)
1491        expected_status = [(' M', '.*/%s$' % os.path.basename(recipefile)),
1492                           (' M', '.*/file1$'),
1493                           (' D', '.*/file2$'),
1494                           ('??', '.*/new-local$'),
1495                           ('??', '.*/0001-Add-new-file.patch$')]
1496        self._check_repo_status(os.path.dirname(recipefile), expected_status)
1497
1498    def test_devtool_update_recipe_with_gitignore(self):
1499        # First, modify the recipe
1500        testrecipe = 'devtool-test-ignored'
1501        bb_vars = get_bb_vars(['FILE'], testrecipe)
1502        recipefile = bb_vars['FILE']
1503        patchfile = os.path.join(os.path.dirname(recipefile), testrecipe, testrecipe + '.patch')
1504        newpatchfile = os.path.join(os.path.dirname(recipefile), testrecipe, testrecipe + '.patch.expected')
1505        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
1506        self.track_for_cleanup(tempdir)
1507        self.track_for_cleanup(self.workspacedir)
1508        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
1509        # (don't bother with cleaning the recipe on teardown, we won't be building it)
1510        result = runCmd('devtool modify %s' % testrecipe)
1511        self.add_command_to_tearDown('cd %s; rm %s/*; git checkout %s %s' % (os.path.dirname(recipefile), testrecipe, testrecipe, os.path.basename(recipefile)))
1512        result = runCmd('devtool finish --force-patch-refresh %s meta-selftest' % testrecipe)
1513        # Check recipe got changed as expected
1514        with open(newpatchfile, 'r') as f:
1515            desiredlines = f.readlines()
1516        with open(patchfile, 'r') as f:
1517            newlines = f.readlines()
1518        # Ignore the initial lines, because oe-selftest creates own meta-selftest repo
1519        # which changes the metadata subject which is added into the patch, but keep
1520        # .patch.expected as it is in case someone runs devtool finish --force-patch-refresh
1521        # devtool-test-ignored manually, then it should generate exactly the same .patch file
1522        self.assertEqual(desiredlines[5:], newlines[5:])
1523
1524    def test_devtool_update_recipe_long_filename(self):
1525        # First, modify the recipe
1526        testrecipe = 'devtool-test-long-filename'
1527        bb_vars = get_bb_vars(['FILE'], testrecipe)
1528        recipefile = bb_vars['FILE']
1529        patchfilename = '0001-I-ll-patch-you-only-if-devtool-lets-me-to-do-it-corr.patch'
1530        patchfile = os.path.join(os.path.dirname(recipefile), testrecipe, patchfilename)
1531        newpatchfile = os.path.join(os.path.dirname(recipefile), testrecipe, patchfilename + '.expected')
1532        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
1533        self.track_for_cleanup(tempdir)
1534        self.track_for_cleanup(self.workspacedir)
1535        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
1536        # (don't bother with cleaning the recipe on teardown, we won't be building it)
1537        result = runCmd('devtool modify %s' % testrecipe)
1538        self.add_command_to_tearDown('cd %s; rm %s/*; git checkout %s %s' % (os.path.dirname(recipefile), testrecipe, testrecipe, os.path.basename(recipefile)))
1539        result = runCmd('devtool finish --force-patch-refresh %s meta-selftest' % testrecipe)
1540        # Check recipe got changed as expected
1541        with open(newpatchfile, 'r') as f:
1542            desiredlines = f.readlines()
1543        with open(patchfile, 'r') as f:
1544            newlines = f.readlines()
1545        # Ignore the initial lines, because oe-selftest creates own meta-selftest repo
1546        # which changes the metadata subject which is added into the patch, but keep
1547        # .patch.expected as it is in case someone runs devtool finish --force-patch-refresh
1548        # devtool-test-ignored manually, then it should generate exactly the same .patch file
1549        self.assertEqual(desiredlines[5:], newlines[5:])
1550
1551    def test_devtool_update_recipe_local_files_3(self):
1552        # First, modify the recipe
1553        testrecipe = 'devtool-test-localonly'
1554        bb_vars = get_bb_vars(['FILE', 'SRC_URI'], testrecipe)
1555        recipefile = bb_vars['FILE']
1556        src_uri = bb_vars['SRC_URI']
1557        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
1558        self.track_for_cleanup(tempdir)
1559        self.track_for_cleanup(self.workspacedir)
1560        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
1561        # (don't bother with cleaning the recipe on teardown, we won't be building it)
1562        result = runCmd('devtool modify %s' % testrecipe)
1563        # Modify one file
1564        runCmd('echo "Another line" >> file2', cwd=os.path.join(self.workspacedir, 'sources', testrecipe))
1565        self.add_command_to_tearDown('cd %s; rm %s/*; git checkout %s %s' % (os.path.dirname(recipefile), testrecipe, testrecipe, os.path.basename(recipefile)))
1566        result = runCmd('devtool update-recipe %s' % testrecipe)
1567        expected_status = [(' M', '.*/%s/file2$' % testrecipe)]
1568        self._check_repo_status(os.path.dirname(recipefile), expected_status)
1569
1570    def test_devtool_update_recipe_local_patch_gz(self):
1571        # First, modify the recipe
1572        testrecipe = 'devtool-test-patch-gz'
1573        if get_bb_var('DISTRO') == 'poky-tiny':
1574            self.skipTest("The DISTRO 'poky-tiny' does not provide the dependencies needed by %s" % testrecipe)
1575        bb_vars = get_bb_vars(['FILE', 'SRC_URI'], testrecipe)
1576        recipefile = bb_vars['FILE']
1577        src_uri = bb_vars['SRC_URI']
1578        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
1579        self.track_for_cleanup(tempdir)
1580        self.track_for_cleanup(self.workspacedir)
1581        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
1582        # (don't bother with cleaning the recipe on teardown, we won't be building it)
1583        result = runCmd('devtool modify %s' % testrecipe)
1584        # Modify one file
1585        srctree = os.path.join(self.workspacedir, 'sources', testrecipe)
1586        runCmd('echo "Another line" >> README', cwd=srctree)
1587        runCmd('git commit -a --amend --no-edit --no-verify', cwd=srctree)
1588        self.add_command_to_tearDown('cd %s; rm %s/*; git checkout %s %s' % (os.path.dirname(recipefile), testrecipe, testrecipe, os.path.basename(recipefile)))
1589        result = runCmd('devtool update-recipe %s' % testrecipe)
1590        expected_status = [(' M', '.*/%s/readme.patch.gz$' % testrecipe)]
1591        self._check_repo_status(os.path.dirname(recipefile), expected_status)
1592        patch_gz = os.path.join(os.path.dirname(recipefile), testrecipe, 'readme.patch.gz')
1593        result = runCmd('file %s' % patch_gz)
1594        if 'gzip compressed data' not in result.output:
1595            self.fail('New patch file is not gzipped - file reports:\n%s' % result.output)
1596
1597    def test_devtool_update_recipe_local_files_subdir(self):
1598        # Try devtool update-recipe on a recipe that has a file with subdir= set in
1599        # SRC_URI such that it overwrites a file that was in an archive that
1600        # was also in SRC_URI
1601        # First, modify the recipe
1602        testrecipe = 'devtool-test-subdir'
1603        bb_vars = get_bb_vars(['FILE', 'SRC_URI'], testrecipe)
1604        recipefile = bb_vars['FILE']
1605        src_uri = bb_vars['SRC_URI']
1606        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
1607        self.track_for_cleanup(tempdir)
1608        self.track_for_cleanup(self.workspacedir)
1609        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
1610        # (don't bother with cleaning the recipe on teardown, we won't be building it)
1611        result = runCmd('devtool modify %s' % testrecipe)
1612        testfile = os.path.join(self.workspacedir, 'sources', testrecipe, 'testfile')
1613        self.assertExists(testfile, 'Extracted source could not be found')
1614        with open(testfile, 'r') as f:
1615            contents = f.read().rstrip()
1616        self.assertEqual(contents, 'Modified version', 'File has apparently not been overwritten as it should have been')
1617        # Test devtool update-recipe without modifying any files
1618        self.add_command_to_tearDown('cd %s; rm %s/*; git checkout %s %s' % (os.path.dirname(recipefile), testrecipe, testrecipe, os.path.basename(recipefile)))
1619        result = runCmd('devtool update-recipe %s' % testrecipe)
1620        expected_status = []
1621        self._check_repo_status(os.path.dirname(recipefile), expected_status)
1622
1623    def test_devtool_finish_modify_git_subdir(self):
1624        # Check preconditions
1625        testrecipe = 'dos2unix'
1626        self.append_config('ERROR_QA:remove:pn-dos2unix = "patch-status"\n')
1627        bb_vars = get_bb_vars(['SRC_URI', 'S', 'WORKDIR', 'FILE'], testrecipe)
1628        self.assertIn('git://', bb_vars['SRC_URI'], 'This test expects the %s recipe to be a git recipe' % testrecipe)
1629        workdir_git = '%s/git/' % bb_vars['WORKDIR']
1630        if not bb_vars['S'].startswith(workdir_git):
1631            self.fail('This test expects the %s recipe to be building from a subdirectory of the git repo' % testrecipe)
1632        subdir = bb_vars['S'].split(workdir_git, 1)[1]
1633        # Clean up anything in the workdir/sysroot/sstate cache
1634        bitbake('%s -c cleansstate' % testrecipe)
1635        # Try modifying a recipe
1636        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
1637        self.track_for_cleanup(tempdir)
1638        self.track_for_cleanup(self.workspacedir)
1639        self.add_command_to_tearDown('bitbake -c clean %s' % testrecipe)
1640        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
1641        result = runCmd('devtool modify %s -x %s' % (testrecipe, tempdir))
1642        testsrcfile = os.path.join(tempdir, subdir, 'dos2unix.c')
1643        self.assertExists(testsrcfile, 'Extracted source could not be found')
1644        self.assertExists(os.path.join(self.workspacedir, 'conf', 'layer.conf'), 'Workspace directory not created. devtool output: %s' % result.output)
1645        self.assertNotExists(os.path.join(tempdir, subdir, '.git'), 'Subdirectory has been initialised as a git repo')
1646        # Check git repo
1647        self._check_src_repo(tempdir)
1648        # Modify file
1649        runCmd("sed -i '1s:^:/* Add a comment */\\n:' %s" % testsrcfile)
1650        result = runCmd('git commit -a -m "Add a comment"', cwd=tempdir)
1651        # Now try updating original recipe
1652        recipefile = bb_vars['FILE']
1653        recipedir = os.path.dirname(recipefile)
1654        self.add_command_to_tearDown('cd %s; rm -f %s/*.patch; git checkout .' % (recipedir, testrecipe))
1655        result = runCmd('devtool update-recipe %s' % testrecipe)
1656        expected_status = [(' M', '.*/%s$' % os.path.basename(recipefile)),
1657                           ('??', '.*/%s/%s/$' % (testrecipe, testrecipe))]
1658        self._check_repo_status(os.path.dirname(recipefile), expected_status)
1659        result = runCmd('git diff %s' % os.path.basename(recipefile), cwd=os.path.dirname(recipefile))
1660        removelines = ['SRC_URI = "git://.*"']
1661        addlines = [
1662            'SRC_URI = "git://.* \\\\',
1663            'file://0001-Add-a-comment.patch;patchdir=.. \\\\',
1664            '"'
1665        ]
1666        self._check_diff(result.output, addlines, removelines)
1667        # Put things back so we can run devtool finish on a different layer
1668        runCmd('cd %s; rm -f %s/*.patch; git checkout .' % (recipedir, testrecipe))
1669        # Run devtool finish
1670        res = re.search('recipes-.*', recipedir)
1671        self.assertTrue(res, 'Unable to find recipe subdirectory')
1672        recipesubdir = res[0]
1673        self.add_command_to_tearDown('rm -rf %s' % os.path.join(self.testlayer_path, recipesubdir))
1674        result = runCmd('devtool finish %s meta-selftest' % testrecipe)
1675        # Check bbappend file contents
1676        appendfn = os.path.join(self.testlayer_path, recipesubdir, '%s_%%.bbappend' % testrecipe)
1677        with open(appendfn, 'r') as f:
1678            appendlines = f.readlines()
1679        expected_appendlines = [
1680            'FILESEXTRAPATHS:prepend := "${THISDIR}/${PN}:"\n',
1681            '\n',
1682            'SRC_URI += "file://0001-Add-a-comment.patch;patchdir=.."\n',
1683            '\n'
1684        ]
1685        self.assertEqual(appendlines, expected_appendlines)
1686        self.assertExists(os.path.join(os.path.dirname(appendfn), testrecipe, '0001-Add-a-comment.patch'))
1687        # Try building
1688        bitbake('%s -c patch' % testrecipe)
1689
1690    def test_devtool_git_submodules(self):
1691        # This tests if we can add a patch in a git submodule and extract it properly using devtool finish
1692        # Check preconditions
1693        self.assertTrue(not os.path.exists(self.workspacedir), 'This test cannot be run with a workspace directory under the build directory')
1694        self.track_for_cleanup(self.workspacedir)
1695        recipe = 'vulkan-samples'
1696        src_uri = get_bb_var('SRC_URI', recipe)
1697        self.assertIn('gitsm://', src_uri, 'This test expects the %s recipe to be a git recipe with submodules' % recipe)
1698        oldrecipefile = get_bb_var('FILE', recipe)
1699        recipedir = os.path.dirname(oldrecipefile)
1700        result = runCmd('git status --porcelain .', cwd=recipedir)
1701        if result.output.strip():
1702            self.fail('Recipe directory for %s contains uncommitted changes' % recipe)
1703        self.assertIn('/meta/', recipedir)
1704        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
1705        self.track_for_cleanup(tempdir)
1706        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
1707        result = runCmd('devtool modify %s %s' % (recipe, tempdir))
1708        self.assertExists(os.path.join(tempdir, 'CMakeLists.txt'), 'Extracted source could not be found')
1709        # Test devtool status
1710        result = runCmd('devtool status')
1711        self.assertIn(recipe, result.output)
1712        self.assertIn(tempdir, result.output)
1713        # Modify a source file in a submodule, (grab the first one)
1714        result = runCmd('git submodule --quiet foreach \'echo $sm_path\'', cwd=tempdir)
1715        submodule = result.output.splitlines()[0]
1716        submodule_path = os.path.join(tempdir, submodule)
1717        runCmd('echo "#This is a first comment" >> testfile', cwd=submodule_path)
1718        result = runCmd('git status --porcelain . ', cwd=submodule_path)
1719        self.assertIn("testfile", result.output)
1720        runCmd('git add testfile; git commit -m "Adding a new file"', cwd=submodule_path)
1721
1722        # Try finish to the original layer
1723        self.add_command_to_tearDown('rm -rf %s ; cd %s ; git checkout %s' % (recipedir, os.path.dirname(recipedir), recipedir))
1724        runCmd('devtool finish -f %s meta' % recipe)
1725        result = runCmd('devtool status')
1726        self.assertNotIn(recipe, result.output, 'Recipe should have been reset by finish but wasn\'t')
1727        self.assertNotExists(os.path.join(self.workspacedir, 'recipes', recipe), 'Recipe directory should not exist after finish')
1728        expected_status = [(' M', '.*/%s$' % os.path.basename(oldrecipefile)),
1729                           ('??', '.*/.*-Adding-a-new-file.patch$')]
1730        self._check_repo_status(recipedir, expected_status)
1731        # Make sure the patch is added to the recipe with the correct "patchdir" option
1732        result = runCmd('git diff .', cwd=recipedir)
1733        addlines = [
1734           'file://0001-Adding-a-new-file.patch;patchdir=%s \\\\' % submodule
1735        ]
1736        self._check_diff(result.output, addlines, [])
1737
1738class DevtoolExtractTests(DevtoolBase):
1739
1740    def test_devtool_extract(self):
1741        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
1742        # Try devtool extract
1743        self.track_for_cleanup(tempdir)
1744        self.track_for_cleanup(self.workspacedir)
1745        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
1746        result = runCmd('devtool extract matchbox-terminal %s' % tempdir)
1747        self.assertExists(os.path.join(tempdir, 'Makefile.am'), 'Extracted source could not be found')
1748        self._check_src_repo(tempdir)
1749
1750    def test_devtool_extract_virtual(self):
1751        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
1752        # Try devtool extract
1753        self.track_for_cleanup(tempdir)
1754        self.track_for_cleanup(self.workspacedir)
1755        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
1756        result = runCmd('devtool extract virtual/make %s' % tempdir)
1757        self.assertExists(os.path.join(tempdir, 'Makefile.am'), 'Extracted source could not be found')
1758        self._check_src_repo(tempdir)
1759
1760    def test_devtool_reset_all(self):
1761        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
1762        self.track_for_cleanup(tempdir)
1763        self.track_for_cleanup(self.workspacedir)
1764        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
1765        testrecipe1 = 'mdadm'
1766        testrecipe2 = 'cronie'
1767        result = runCmd('devtool modify -x %s %s' % (testrecipe1, os.path.join(tempdir, testrecipe1)))
1768        result = runCmd('devtool modify -x %s %s' % (testrecipe2, os.path.join(tempdir, testrecipe2)))
1769        result = runCmd('devtool build %s' % testrecipe1)
1770        result = runCmd('devtool build %s' % testrecipe2)
1771        stampprefix1 = get_bb_var('STAMP', testrecipe1)
1772        self.assertTrue(stampprefix1, 'Unable to get STAMP value for recipe %s' % testrecipe1)
1773        stampprefix2 = get_bb_var('STAMP', testrecipe2)
1774        self.assertTrue(stampprefix2, 'Unable to get STAMP value for recipe %s' % testrecipe2)
1775        result = runCmd('devtool reset -a')
1776        self.assertIn(testrecipe1, result.output)
1777        self.assertIn(testrecipe2, result.output)
1778        result = runCmd('devtool status')
1779        self.assertNotIn(testrecipe1, result.output)
1780        self.assertNotIn(testrecipe2, result.output)
1781        matches1 = glob.glob(stampprefix1 + '*')
1782        self.assertFalse(matches1, 'Stamp files exist for recipe %s that should have been cleaned' % testrecipe1)
1783        matches2 = glob.glob(stampprefix2 + '*')
1784        self.assertFalse(matches2, 'Stamp files exist for recipe %s that should have been cleaned' % testrecipe2)
1785
1786    @OETestTag("runqemu")
1787    def test_devtool_deploy_target(self):
1788        self._check_runqemu_prerequisites()
1789        self.assertTrue(not os.path.exists(self.workspacedir), 'This test cannot be run with a workspace directory under the build directory')
1790        # Definitions
1791        testrecipe = 'mdadm'
1792        testfile = '/sbin/mdadm'
1793        if "usrmerge" in get_bb_var('DISTRO_FEATURES'):
1794            testfile = '/usr/sbin/mdadm'
1795        testimage = 'oe-selftest-image'
1796        testcommand = '/sbin/mdadm --help'
1797        # Build an image to run
1798        bitbake("%s qemu-native qemu-helper-native" % testimage)
1799        deploy_dir_image = get_bb_var('DEPLOY_DIR_IMAGE')
1800        self.add_command_to_tearDown('bitbake -c clean %s' % testimage)
1801        self.add_command_to_tearDown('rm -f %s/%s*' % (deploy_dir_image, testimage))
1802        # Clean recipe so the first deploy will fail
1803        bitbake("%s -c clean" % testrecipe)
1804        # Try devtool modify
1805        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
1806        self.track_for_cleanup(tempdir)
1807        self.track_for_cleanup(self.workspacedir)
1808        self.add_command_to_tearDown('bitbake -c clean %s' % testrecipe)
1809        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
1810        result = runCmd('devtool modify %s -x %s' % (testrecipe, tempdir))
1811        # Test that deploy-target at this point fails (properly)
1812        result = runCmd('devtool deploy-target -n %s root@localhost' % testrecipe, ignore_status=True)
1813        self.assertNotEqual(result.output, 0, 'devtool deploy-target should have failed, output: %s' % result.output)
1814        self.assertNotIn(result.output, 'Traceback', 'devtool deploy-target should have failed with a proper error not a traceback, output: %s' % result.output)
1815        result = runCmd('devtool build %s' % testrecipe)
1816        # First try a dry-run of deploy-target
1817        result = runCmd('devtool deploy-target -n %s root@localhost' % testrecipe)
1818        self.assertIn('  %s' % testfile, result.output)
1819        # Boot the image
1820        with runqemu(testimage) as qemu:
1821            # Now really test deploy-target
1822            result = runCmd('devtool deploy-target -c %s root@%s' % (testrecipe, qemu.ip))
1823            # Run a test command to see if it was installed properly
1824            sshargs = '-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no'
1825            result = runCmd('ssh %s root@%s %s' % (sshargs, qemu.ip, testcommand))
1826            # Check if it deployed all of the files with the right ownership/perms
1827            # First look on the host - need to do this under pseudo to get the correct ownership/perms
1828            bb_vars = get_bb_vars(['D', 'FAKEROOTENV', 'FAKEROOTCMD'], testrecipe)
1829            installdir = bb_vars['D']
1830            fakerootenv = bb_vars['FAKEROOTENV']
1831            fakerootcmd = bb_vars['FAKEROOTCMD']
1832            result = runCmd('%s %s find . -type f -exec ls -l {} \\;' % (fakerootenv, fakerootcmd), cwd=installdir)
1833            filelist1 = self._process_ls_output(result.output)
1834
1835            # Now look on the target
1836            tempdir2 = tempfile.mkdtemp(prefix='devtoolqa')
1837            self.track_for_cleanup(tempdir2)
1838            tmpfilelist = os.path.join(tempdir2, 'files.txt')
1839            with open(tmpfilelist, 'w') as f:
1840                for line in filelist1:
1841                    splitline = line.split()
1842                    f.write(splitline[-1] + '\n')
1843            result = runCmd('cat %s | ssh -q %s root@%s \'xargs ls -l\'' % (tmpfilelist, sshargs, qemu.ip))
1844            filelist2 = self._process_ls_output(result.output)
1845            filelist1.sort(key=lambda item: item.split()[-1])
1846            filelist2.sort(key=lambda item: item.split()[-1])
1847            self.assertEqual(filelist1, filelist2)
1848            # Test undeploy-target
1849            result = runCmd('devtool undeploy-target -c %s root@%s' % (testrecipe, qemu.ip))
1850            result = runCmd('ssh %s root@%s %s' % (sshargs, qemu.ip, testcommand), ignore_status=True)
1851            self.assertNotEqual(result, 0, 'undeploy-target did not remove command as it should have')
1852
1853    def test_devtool_build_image(self):
1854        """Test devtool build-image plugin"""
1855        # Check preconditions
1856        self.assertTrue(not os.path.exists(self.workspacedir), 'This test cannot be run with a workspace directory under the build directory')
1857        image = 'core-image-minimal'
1858        self.track_for_cleanup(self.workspacedir)
1859        self.add_command_to_tearDown('bitbake -c clean %s' % image)
1860        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
1861        bitbake('%s -c clean' % image)
1862        # Add target and native recipes to workspace
1863        recipes = ['mdadm', 'parted-native']
1864        for recipe in recipes:
1865            tempdir = tempfile.mkdtemp(prefix='devtoolqa')
1866            self.track_for_cleanup(tempdir)
1867            self.add_command_to_tearDown('bitbake -c clean %s' % recipe)
1868            runCmd('devtool modify %s -x %s' % (recipe, tempdir))
1869        # Try to build image
1870        result = runCmd('devtool build-image %s' % image)
1871        self.assertNotEqual(result, 0, 'devtool build-image failed')
1872        # Check if image contains expected packages
1873        deploy_dir_image = get_bb_var('DEPLOY_DIR_IMAGE')
1874        image_link_name = get_bb_var('IMAGE_LINK_NAME', image)
1875        reqpkgs = [item for item in recipes if not item.endswith('-native')]
1876        with open(os.path.join(deploy_dir_image, image_link_name + '.manifest'), 'r') as f:
1877            for line in f:
1878                splitval = line.split()
1879                if splitval:
1880                    pkg = splitval[0]
1881                    if pkg in reqpkgs:
1882                        reqpkgs.remove(pkg)
1883        if reqpkgs:
1884            self.fail('The following packages were not present in the image as expected: %s' % ', '.join(reqpkgs))
1885
1886class DevtoolUpgradeTests(DevtoolBase):
1887
1888    def setUp(self):
1889        super().setUp()
1890        try:
1891            runCmd("git config --global user.name")
1892            runCmd("git config --global user.email")
1893        except:
1894            self.skip("Git user.name and user.email must be set")
1895
1896    def test_devtool_upgrade(self):
1897        # Check preconditions
1898        self.assertTrue(not os.path.exists(self.workspacedir), 'This test cannot be run with a workspace directory under the build directory')
1899        self.track_for_cleanup(self.workspacedir)
1900        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
1901        # Check parameters
1902        result = runCmd('devtool upgrade -h')
1903        for param in 'recipename srctree --version -V --branch -b --keep-temp --no-patch'.split():
1904            self.assertIn(param, result.output)
1905        # For the moment, we are using a real recipe.
1906        recipe = 'devtool-upgrade-test1'
1907        version = '1.6.0'
1908        oldrecipefile = get_bb_var('FILE', recipe)
1909        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
1910        self.track_for_cleanup(tempdir)
1911        # Check that recipe is not already under devtool control
1912        result = runCmd('devtool status')
1913        self.assertNotIn(recipe, result.output)
1914        # Check upgrade. Code does not check if new PV is older or newer that current PV, so, it may be that
1915        # we are downgrading instead of upgrading.
1916        result = runCmd('devtool upgrade %s %s -V %s' % (recipe, tempdir, version))
1917        # Check if srctree at least is populated
1918        self.assertTrue(len(os.listdir(tempdir)) > 0, 'srctree (%s) should be populated with new (%s) source code' % (tempdir, version))
1919        # Check new recipe subdirectory is present
1920        self.assertExists(os.path.join(self.workspacedir, 'recipes', recipe, '%s-%s' % (recipe, version)), 'Recipe folder should exist')
1921        # Check new recipe file is present
1922        newrecipefile = os.path.join(self.workspacedir, 'recipes', recipe, '%s_%s.bb' % (recipe, version))
1923        self.assertExists(newrecipefile, 'Recipe file should exist after upgrade')
1924        # Check devtool status and make sure recipe is present
1925        result = runCmd('devtool status')
1926        self.assertIn(recipe, result.output)
1927        self.assertIn(tempdir, result.output)
1928        # Check recipe got changed as expected
1929        with open(oldrecipefile + '.upgraded', 'r') as f:
1930            desiredlines = f.readlines()
1931        with open(newrecipefile, 'r') as f:
1932            newlines = f.readlines()
1933        self.assertEqual(desiredlines, newlines)
1934        # Check devtool reset recipe
1935        result = runCmd('devtool reset %s -n' % recipe)
1936        result = runCmd('devtool status')
1937        self.assertNotIn(recipe, result.output)
1938        self.assertNotExists(os.path.join(self.workspacedir, 'recipes', recipe), 'Recipe directory should not exist after resetting')
1939
1940    def test_devtool_upgrade_git(self):
1941        # Check preconditions
1942        self.assertTrue(not os.path.exists(self.workspacedir), 'This test cannot be run with a workspace directory under the build directory')
1943        self.track_for_cleanup(self.workspacedir)
1944        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
1945        recipe = 'devtool-upgrade-test2'
1946        commit = '6cc6077a36fe2648a5f993fe7c16c9632f946517'
1947        oldrecipefile = get_bb_var('FILE', recipe)
1948        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
1949        self.track_for_cleanup(tempdir)
1950        # Check that recipe is not already under devtool control
1951        result = runCmd('devtool status')
1952        self.assertNotIn(recipe, result.output)
1953        # Check upgrade
1954        result = runCmd('devtool upgrade %s %s -S %s' % (recipe, tempdir, commit))
1955        # Check if srctree at least is populated
1956        self.assertTrue(len(os.listdir(tempdir)) > 0, 'srctree (%s) should be populated with new (%s) source code' % (tempdir, commit))
1957        # Check new recipe file is present
1958        newrecipefile = os.path.join(self.workspacedir, 'recipes', recipe, os.path.basename(oldrecipefile))
1959        self.assertExists(newrecipefile, 'Recipe file should exist after upgrade')
1960        # Check devtool status and make sure recipe is present
1961        result = runCmd('devtool status')
1962        self.assertIn(recipe, result.output)
1963        self.assertIn(tempdir, result.output)
1964        # Check recipe got changed as expected
1965        with open(oldrecipefile + '.upgraded', 'r') as f:
1966            desiredlines = f.readlines()
1967        with open(newrecipefile, 'r') as f:
1968            newlines = f.readlines()
1969        self.assertEqual(desiredlines, newlines)
1970        # Check devtool reset recipe
1971        result = runCmd('devtool reset %s -n' % recipe)
1972        result = runCmd('devtool status')
1973        self.assertNotIn(recipe, result.output)
1974        self.assertNotExists(os.path.join(self.workspacedir, 'recipes', recipe), 'Recipe directory should not exist after resetting')
1975
1976    def test_devtool_upgrade_drop_md5sum(self):
1977        # Check preconditions
1978        self.assertTrue(not os.path.exists(self.workspacedir), 'This test cannot be run with a workspace directory under the build directory')
1979        self.track_for_cleanup(self.workspacedir)
1980        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
1981        # For the moment, we are using a real recipe.
1982        recipe = 'devtool-upgrade-test3'
1983        version = '1.6.0'
1984        oldrecipefile = get_bb_var('FILE', recipe)
1985        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
1986        self.track_for_cleanup(tempdir)
1987        # Check upgrade. Code does not check if new PV is older or newer that current PV, so, it may be that
1988        # we are downgrading instead of upgrading.
1989        result = runCmd('devtool upgrade %s %s -V %s' % (recipe, tempdir, version))
1990        # Check new recipe file is present
1991        newrecipefile = os.path.join(self.workspacedir, 'recipes', recipe, '%s_%s.bb' % (recipe, version))
1992        self.assertExists(newrecipefile, 'Recipe file should exist after upgrade')
1993        # Check recipe got changed as expected
1994        with open(oldrecipefile + '.upgraded', 'r') as f:
1995            desiredlines = f.readlines()
1996        with open(newrecipefile, 'r') as f:
1997            newlines = f.readlines()
1998        self.assertEqual(desiredlines, newlines)
1999
2000    def test_devtool_upgrade_all_checksums(self):
2001        # Check preconditions
2002        self.assertTrue(not os.path.exists(self.workspacedir), 'This test cannot be run with a workspace directory under the build directory')
2003        self.track_for_cleanup(self.workspacedir)
2004        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
2005        # For the moment, we are using a real recipe.
2006        recipe = 'devtool-upgrade-test4'
2007        version = '1.6.0'
2008        oldrecipefile = get_bb_var('FILE', recipe)
2009        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
2010        self.track_for_cleanup(tempdir)
2011        # Check upgrade. Code does not check if new PV is older or newer that current PV, so, it may be that
2012        # we are downgrading instead of upgrading.
2013        result = runCmd('devtool upgrade %s %s -V %s' % (recipe, tempdir, version))
2014        # Check new recipe file is present
2015        newrecipefile = os.path.join(self.workspacedir, 'recipes', recipe, '%s_%s.bb' % (recipe, version))
2016        self.assertExists(newrecipefile, 'Recipe file should exist after upgrade')
2017        # Check recipe got changed as expected
2018        with open(oldrecipefile + '.upgraded', 'r') as f:
2019            desiredlines = f.readlines()
2020        with open(newrecipefile, 'r') as f:
2021            newlines = f.readlines()
2022        self.assertEqual(desiredlines, newlines)
2023
2024    def test_devtool_upgrade_recipe_upgrade_extra_tasks(self):
2025        # Check preconditions
2026        self.assertTrue(not os.path.exists(self.workspacedir), 'This test cannot be run with a workspace directory under the build directory')
2027        self.track_for_cleanup(self.workspacedir)
2028        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
2029        recipe = 'python3-guessing-game'
2030        version = '0.2.0'
2031        commit = '40cf004c2772ffa20ea803fa3be1528a75be3e98'
2032        oldrecipefile = get_bb_var('FILE', recipe)
2033        oldcratesincfile = os.path.join(os.path.dirname(oldrecipefile), os.path.basename(oldrecipefile).strip('_git.bb') + '-crates.inc')
2034        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
2035        self.track_for_cleanup(tempdir)
2036        # Check that recipe is not already under devtool control
2037        result = runCmd('devtool status')
2038        self.assertNotIn(recipe, result.output)
2039        # Check upgrade
2040        result = runCmd('devtool upgrade %s %s --version %s --srcrev %s' % (recipe, tempdir, version, commit))
2041        # Check if srctree at least is populated
2042        self.assertTrue(len(os.listdir(tempdir)) > 0, 'srctree (%s) should be populated with new (%s) source code' % (tempdir, commit))
2043        # Check new recipe file and new -crates.inc files are present
2044        newrecipefile = os.path.join(self.workspacedir, 'recipes', recipe, os.path.basename(oldrecipefile))
2045        newcratesincfile = os.path.join(self.workspacedir, 'recipes', recipe, os.path.basename(oldcratesincfile))
2046        self.assertExists(newrecipefile, 'Recipe file should exist after upgrade')
2047        self.assertExists(newcratesincfile, 'Recipe crates.inc file should exist after upgrade')
2048        # Check devtool status and make sure recipe is present
2049        result = runCmd('devtool status')
2050        self.assertIn(recipe, result.output)
2051        self.assertIn(tempdir, result.output)
2052        # Check recipe got changed as expected
2053        with open(oldrecipefile + '.upgraded', 'r') as f:
2054            desiredlines = f.readlines()
2055        with open(newrecipefile, 'r') as f:
2056            newlines = f.readlines()
2057        self.assertEqual(desiredlines, newlines)
2058        # Check crates.inc got changed as expected
2059        with open(oldcratesincfile + '.upgraded', 'r') as f:
2060            desiredlines = f.readlines()
2061        with open(newcratesincfile, 'r') as f:
2062            newlines = f.readlines()
2063        self.assertEqual(desiredlines, newlines)
2064        # Check devtool reset recipe
2065        result = runCmd('devtool reset %s -n' % recipe)
2066        result = runCmd('devtool status')
2067        self.assertNotIn(recipe, result.output)
2068        self.assertNotExists(os.path.join(self.workspacedir, 'recipes', recipe), 'Recipe directory should not exist after resetting')
2069
2070    def test_devtool_layer_plugins(self):
2071        """Test that devtool can use plugins from other layers.
2072
2073        This test executes the selftest-reverse command from meta-selftest."""
2074
2075        self.track_for_cleanup(self.workspacedir)
2076        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
2077
2078        s = "Microsoft Made No Profit From Anyone's Zunes Yo"
2079        result = runCmd("devtool --quiet selftest-reverse \"%s\"" % s)
2080        self.assertEqual(result.output, s[::-1])
2081
2082    def _copy_file_with_cleanup(self, srcfile, basedstdir, *paths):
2083        dstdir = basedstdir
2084        self.assertExists(dstdir)
2085        for p in paths:
2086            dstdir = os.path.join(dstdir, p)
2087            if not os.path.exists(dstdir):
2088                try:
2089                    os.makedirs(dstdir)
2090                except PermissionError:
2091                    return False
2092                except OSError as e:
2093                    if e.errno == errno.EROFS:
2094                        return False
2095                    else:
2096                        raise e
2097                if p == "lib":
2098                    # Can race with other tests
2099                    self.add_command_to_tearDown('rmdir --ignore-fail-on-non-empty %s' % dstdir)
2100                else:
2101                    self.track_for_cleanup(dstdir)
2102        dstfile = os.path.join(dstdir, os.path.basename(srcfile))
2103        if srcfile != dstfile:
2104            try:
2105                shutil.copy(srcfile, dstfile)
2106            except PermissionError:
2107                return False
2108            self.track_for_cleanup(dstfile)
2109        return True
2110
2111    def test_devtool_load_plugin(self):
2112        """Test that devtool loads only the first found plugin in BBPATH."""
2113
2114        self.track_for_cleanup(self.workspacedir)
2115        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
2116
2117        devtool = runCmd("which devtool")
2118        fromname = runCmd("devtool --quiet pluginfile")
2119        srcfile = fromname.output
2120        bbpath = get_bb_var('BBPATH')
2121        searchpath = bbpath.split(':') + [os.path.dirname(devtool.output)]
2122        plugincontent = []
2123        with open(srcfile) as fh:
2124            plugincontent = fh.readlines()
2125        try:
2126            self.assertIn('meta-selftest', srcfile, 'wrong bbpath plugin found')
2127            searchpath = [
2128                path for path in searchpath
2129                if self._copy_file_with_cleanup(srcfile, path, 'lib', 'devtool')
2130            ]
2131            result = runCmd("devtool --quiet count")
2132            self.assertEqual(result.output, '1')
2133            result = runCmd("devtool --quiet multiloaded")
2134            self.assertEqual(result.output, "no")
2135            for path in searchpath:
2136                result = runCmd("devtool --quiet bbdir")
2137                self.assertEqual(os.path.realpath(result.output), os.path.realpath(path))
2138                os.unlink(os.path.join(result.output, 'lib', 'devtool', 'bbpath.py'))
2139        finally:
2140            with open(srcfile, 'w') as fh:
2141                fh.writelines(plugincontent)
2142
2143    def _setup_test_devtool_finish_upgrade(self):
2144        # Check preconditions
2145        self.assertTrue(not os.path.exists(self.workspacedir), 'This test cannot be run with a workspace directory under the build directory')
2146        self.track_for_cleanup(self.workspacedir)
2147        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
2148        # Use a "real" recipe from meta-selftest
2149        recipe = 'devtool-upgrade-test1'
2150        oldversion = '1.5.3'
2151        newversion = '1.6.0'
2152        oldrecipefile = get_bb_var('FILE', recipe)
2153        recipedir = os.path.dirname(oldrecipefile)
2154        result = runCmd('git status --porcelain .', cwd=recipedir)
2155        if result.output.strip():
2156            self.fail('Recipe directory for %s contains uncommitted changes' % recipe)
2157        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
2158        self.track_for_cleanup(tempdir)
2159        # Check that recipe is not already under devtool control
2160        result = runCmd('devtool status')
2161        self.assertNotIn(recipe, result.output)
2162        # Do the upgrade
2163        result = runCmd('devtool upgrade %s %s -V %s' % (recipe, tempdir, newversion))
2164        # Check devtool status and make sure recipe is present
2165        result = runCmd('devtool status')
2166        self.assertIn(recipe, result.output)
2167        self.assertIn(tempdir, result.output)
2168        # Make a change to the source
2169        result = runCmd('sed -i \'/^#include "pv.h"/a \\/* Here is a new comment *\\/\' src/pv/number.c', cwd=tempdir)
2170        result = runCmd('git status --porcelain', cwd=tempdir)
2171        self.assertIn('M src/pv/number.c', result.output)
2172        result = runCmd('git commit src/pv/number.c -m "Add a comment to the code"', cwd=tempdir)
2173        # Check if patch is there
2174        recipedir = os.path.dirname(oldrecipefile)
2175        olddir = os.path.join(recipedir, recipe + '-' + oldversion)
2176        patchfn = '0001-Add-a-note-line-to-the-quick-reference.patch'
2177        backportedpatchfn = 'backported.patch'
2178        self.assertExists(os.path.join(olddir, patchfn), 'Original patch file does not exist')
2179        self.assertExists(os.path.join(olddir, backportedpatchfn), 'Backported patch file does not exist')
2180        return recipe, oldrecipefile, recipedir, olddir, newversion, patchfn, backportedpatchfn
2181
2182    def test_devtool_finish_upgrade_origlayer(self):
2183        recipe, oldrecipefile, recipedir, olddir, newversion, patchfn, backportedpatchfn = self._setup_test_devtool_finish_upgrade()
2184        # Ensure the recipe is where we think it should be (so that cleanup doesn't trash things)
2185        self.assertIn('/meta-selftest/', recipedir)
2186        # Try finish to the original layer
2187        self.add_command_to_tearDown('rm -rf %s ; cd %s ; git checkout %s' % (recipedir, os.path.dirname(recipedir), recipedir))
2188        result = runCmd('devtool finish %s meta-selftest' % recipe)
2189        result = runCmd('devtool status')
2190        self.assertNotIn(recipe, result.output, 'Recipe should have been reset by finish but wasn\'t')
2191        self.assertNotExists(os.path.join(self.workspacedir, 'recipes', recipe), 'Recipe directory should not exist after finish')
2192        self.assertNotExists(oldrecipefile, 'Old recipe file should have been deleted but wasn\'t')
2193        self.assertNotExists(os.path.join(olddir, patchfn), 'Old patch file should have been deleted but wasn\'t')
2194        self.assertNotExists(os.path.join(olddir, backportedpatchfn), 'Old backported patch file should have been deleted but wasn\'t')
2195        newrecipefile = os.path.join(recipedir, '%s_%s.bb' % (recipe, newversion))
2196        newdir = os.path.join(recipedir, recipe + '-' + newversion)
2197        self.assertExists(newrecipefile, 'New recipe file should have been copied into existing layer but wasn\'t')
2198        self.assertExists(os.path.join(newdir, patchfn), 'Patch file should have been copied into new directory but wasn\'t')
2199        self.assertNotExists(os.path.join(newdir, backportedpatchfn), 'Backported patch file should not have been copied into new directory but was')
2200        self.assertExists(os.path.join(newdir, '0002-Add-a-comment-to-the-code.patch'), 'New patch file should have been created but wasn\'t')
2201        with open(newrecipefile, 'r') as f:
2202            newcontent = f.read()
2203        self.assertNotIn(backportedpatchfn, newcontent, "Backported patch should have been removed from the recipe but wasn't")
2204        self.assertIn(patchfn, newcontent, "Old patch should have not been removed from the recipe but was")
2205        self.assertIn("0002-Add-a-comment-to-the-code.patch", newcontent, "New patch should have been added to the recipe but wasn't")
2206        self.assertIn("http://www.ivarch.com/programs/sources/pv-${PV}.tar.gz", newcontent, "New recipe no longer has upstream source in SRC_URI")
2207
2208
2209    def test_devtool_finish_upgrade_otherlayer(self):
2210        recipe, oldrecipefile, recipedir, olddir, newversion, patchfn, backportedpatchfn = self._setup_test_devtool_finish_upgrade()
2211        # Ensure the recipe is where we think it should be (so that cleanup doesn't trash things)
2212        self.assertIn('/meta-selftest/', recipedir)
2213        # Try finish to a different layer - should create a bbappend
2214        # This cleanup isn't strictly necessary but do it anyway just in case it goes wrong and writes to here
2215        self.add_command_to_tearDown('rm -rf %s ; cd %s ; git checkout %s' % (recipedir, os.path.dirname(recipedir), recipedir))
2216        oe_core_dir = os.path.join(get_bb_var('COREBASE'), 'meta')
2217        newrecipedir = os.path.join(oe_core_dir, 'recipes-test', 'devtool')
2218        newrecipefile = os.path.join(newrecipedir, '%s_%s.bb' % (recipe, newversion))
2219        self.track_for_cleanup(newrecipedir)
2220        result = runCmd('devtool finish %s oe-core' % recipe)
2221        result = runCmd('devtool status')
2222        self.assertNotIn(recipe, result.output, 'Recipe should have been reset by finish but wasn\'t')
2223        self.assertNotExists(os.path.join(self.workspacedir, 'recipes', recipe), 'Recipe directory should not exist after finish')
2224        self.assertExists(oldrecipefile, 'Old recipe file should not have been deleted')
2225        self.assertExists(os.path.join(olddir, patchfn), 'Old patch file should not have been deleted')
2226        self.assertExists(os.path.join(olddir, backportedpatchfn), 'Old backported patch file should not have been deleted')
2227        newdir = os.path.join(newrecipedir, recipe + '-' + newversion)
2228        self.assertExists(newrecipefile, 'New recipe file should have been copied into existing layer but wasn\'t')
2229        self.assertExists(os.path.join(newdir, patchfn), 'Patch file should have been copied into new directory but wasn\'t')
2230        self.assertNotExists(os.path.join(newdir, backportedpatchfn), 'Backported patch file should not have been copied into new directory but was')
2231        self.assertExists(os.path.join(newdir, '0002-Add-a-comment-to-the-code.patch'), 'New patch file should have been created but wasn\'t')
2232        with open(newrecipefile, 'r') as f:
2233            newcontent = f.read()
2234        self.assertNotIn(backportedpatchfn, newcontent, "Backported patch should have been removed from the recipe but wasn't")
2235        self.assertIn(patchfn, newcontent, "Old patch should have not been removed from the recipe but was")
2236        self.assertIn("0002-Add-a-comment-to-the-code.patch", newcontent, "New patch should have been added to the recipe but wasn't")
2237        self.assertIn("http://www.ivarch.com/programs/sources/pv-${PV}.tar.gz", newcontent, "New recipe no longer has upstream source in SRC_URI")
2238
2239    def _setup_test_devtool_finish_modify(self):
2240        # Check preconditions
2241        self.assertTrue(not os.path.exists(self.workspacedir), 'This test cannot be run with a workspace directory under the build directory')
2242        # Try modifying a recipe
2243        self.track_for_cleanup(self.workspacedir)
2244        recipe = 'mdadm'
2245        oldrecipefile = get_bb_var('FILE', recipe)
2246        recipedir = os.path.dirname(oldrecipefile)
2247        result = runCmd('git status --porcelain .', cwd=recipedir)
2248        if result.output.strip():
2249            self.fail('Recipe directory for %s contains uncommitted changes' % recipe)
2250        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
2251        self.track_for_cleanup(tempdir)
2252        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
2253        result = runCmd('devtool modify %s %s' % (recipe, tempdir))
2254        self.assertExists(os.path.join(tempdir, 'Makefile'), 'Extracted source could not be found')
2255        # Test devtool status
2256        result = runCmd('devtool status')
2257        self.assertIn(recipe, result.output)
2258        self.assertIn(tempdir, result.output)
2259        # Make a change to the source
2260        result = runCmd('sed -i \'/^#include "mdadm.h"/a \\/* Here is a new comment *\\/\' maps.c', cwd=tempdir)
2261        result = runCmd('git status --porcelain', cwd=tempdir)
2262        self.assertIn('M maps.c', result.output)
2263        result = runCmd('git commit maps.c -m "Add a comment to the code"', cwd=tempdir)
2264        for entry in os.listdir(recipedir):
2265            filesdir = os.path.join(recipedir, entry)
2266            if os.path.isdir(filesdir):
2267                break
2268        else:
2269            self.fail('Unable to find recipe files directory for %s' % recipe)
2270        return recipe, oldrecipefile, recipedir, filesdir
2271
2272    def test_devtool_finish_modify_origlayer(self):
2273        recipe, oldrecipefile, recipedir, filesdir = self._setup_test_devtool_finish_modify()
2274        # Ensure the recipe is where we think it should be (so that cleanup doesn't trash things)
2275        self.assertIn('/meta/', recipedir)
2276        # Try finish to the original layer
2277        self.add_command_to_tearDown('rm -rf %s ; cd %s ; git checkout %s' % (recipedir, os.path.dirname(recipedir), recipedir))
2278        result = runCmd('devtool finish %s meta' % recipe)
2279        result = runCmd('devtool status')
2280        self.assertNotIn(recipe, result.output, 'Recipe should have been reset by finish but wasn\'t')
2281        self.assertNotExists(os.path.join(self.workspacedir, 'recipes', recipe), 'Recipe directory should not exist after finish')
2282        expected_status = [(' M', '.*/%s$' % os.path.basename(oldrecipefile)),
2283                           ('??', '.*/.*-Add-a-comment-to-the-code.patch$')]
2284        self._check_repo_status(recipedir, expected_status)
2285
2286    def test_devtool_finish_modify_otherlayer(self):
2287        recipe, oldrecipefile, recipedir, filesdir = self._setup_test_devtool_finish_modify()
2288        # Ensure the recipe is where we think it should be (so that cleanup doesn't trash things)
2289        self.assertIn('/meta/', recipedir)
2290        relpth = os.path.relpath(recipedir, os.path.join(get_bb_var('COREBASE'), 'meta'))
2291        appenddir = os.path.join(get_test_layer(), relpth)
2292        self.track_for_cleanup(appenddir)
2293        # Try finish to the original layer
2294        self.add_command_to_tearDown('rm -rf %s ; cd %s ; git checkout %s' % (recipedir, os.path.dirname(recipedir), recipedir))
2295        result = runCmd('devtool finish %s meta-selftest' % recipe)
2296        result = runCmd('devtool status')
2297        self.assertNotIn(recipe, result.output, 'Recipe should have been reset by finish but wasn\'t')
2298        self.assertNotExists(os.path.join(self.workspacedir, 'recipes', recipe), 'Recipe directory should not exist after finish')
2299        result = runCmd('git status --porcelain .', cwd=recipedir)
2300        if result.output.strip():
2301            self.fail('Recipe directory for %s contains the following unexpected changes after finish:\n%s' % (recipe, result.output.strip()))
2302        recipefn = os.path.splitext(os.path.basename(oldrecipefile))[0]
2303        recipefn = recipefn.split('_')[0] + '_%'
2304        appendfile = os.path.join(appenddir, recipefn + '.bbappend')
2305        self.assertExists(appendfile, 'bbappend %s should have been created but wasn\'t' % appendfile)
2306        newdir = os.path.join(appenddir, recipe)
2307        files = os.listdir(newdir)
2308        foundpatch = None
2309        for fn in files:
2310            if fnmatch.fnmatch(fn, '*-Add-a-comment-to-the-code.patch'):
2311                foundpatch = fn
2312        if not foundpatch:
2313            self.fail('No patch file created next to bbappend')
2314        files.remove(foundpatch)
2315        if files:
2316            self.fail('Unexpected file(s) copied next to bbappend: %s' % ', '.join(files))
2317
2318    def test_devtool_finish_update_patch(self):
2319        # This test uses a modified version of the sysdig recipe from meta-oe.
2320        # - The patches have been renamed.
2321        # - The dependencies are commented out since the recipe is not being
2322        #   built.
2323        #
2324        # The sysdig recipe is interesting in that it fetches two different Git
2325        # repositories, and there are patches for both. This leads to that
2326        # devtool will create ignore commits as it uses Git submodules to keep
2327        # track of the second repository.
2328        #
2329        # This test will verify that the ignored commits actually are ignored
2330        # when a commit in between is modified. It will also verify that the
2331        # updated patch keeps its original name.
2332
2333        # Check preconditions
2334        self.assertTrue(not os.path.exists(self.workspacedir), 'This test cannot be run with a workspace directory under the build directory')
2335        # Try modifying a recipe
2336        self.track_for_cleanup(self.workspacedir)
2337        recipe = 'sysdig-selftest'
2338        recipefile = get_bb_var('FILE', recipe)
2339        recipedir = os.path.dirname(recipefile)
2340        result = runCmd('git status --porcelain .', cwd=recipedir)
2341        if result.output.strip():
2342            self.fail('Recipe directory for %s contains uncommitted changes' % recipe)
2343        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
2344        self.track_for_cleanup(tempdir)
2345        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
2346        result = runCmd('devtool modify %s %s' % (recipe, tempdir))
2347        self.add_command_to_tearDown('cd %s; rm %s/*; git checkout %s %s' % (recipedir, recipe, recipe, os.path.basename(recipefile)))
2348        self.assertExists(os.path.join(tempdir, 'CMakeLists.txt'), 'Extracted source could not be found')
2349        # Make a change to one of the existing commits
2350        result = runCmd('echo "# A comment " >> CMakeLists.txt', cwd=tempdir)
2351        result = runCmd('git status --porcelain', cwd=tempdir)
2352        self.assertIn('M CMakeLists.txt', result.output)
2353        result = runCmd('git commit --fixup HEAD^ CMakeLists.txt', cwd=tempdir)
2354        result = runCmd('git show -s --format=%s', cwd=tempdir)
2355        self.assertIn('fixup! cmake: Pass PROBE_NAME via CFLAGS', result.output)
2356        result = runCmd('GIT_SEQUENCE_EDITOR=true git rebase -i --autosquash devtool-base', cwd=tempdir)
2357        result = runCmd('devtool finish %s meta-selftest' % recipe)
2358        result = runCmd('devtool status')
2359        self.assertNotIn(recipe, result.output, 'Recipe should have been reset by finish but wasn\'t')
2360        self.assertNotExists(os.path.join(self.workspacedir, 'recipes', recipe), 'Recipe directory should not exist after finish')
2361        expected_status = [(' M', '.*/0099-cmake-Pass-PROBE_NAME-via-CFLAGS.patch$')]
2362        self._check_repo_status(recipedir, expected_status)
2363
2364    def test_devtool_rename(self):
2365        # Check preconditions
2366        self.assertTrue(not os.path.exists(self.workspacedir), 'This test cannot be run with a workspace directory under the build directory')
2367        self.track_for_cleanup(self.workspacedir)
2368        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
2369
2370        # First run devtool add
2371        # We already have this recipe in OE-Core, but that doesn't matter
2372        recipename = 'i2c-tools'
2373        recipever = '3.1.2'
2374        recipefile = os.path.join(self.workspacedir, 'recipes', recipename, '%s_%s.bb' % (recipename, recipever))
2375        url = 'http://downloads.yoctoproject.org/mirror/sources/i2c-tools-%s.tar.bz2' % recipever
2376        def add_recipe():
2377            result = runCmd('devtool add %s' % url)
2378            self.assertExists(recipefile, 'Expected recipe file not created')
2379            self.assertExists(os.path.join(self.workspacedir, 'sources', recipename), 'Source directory not created')
2380            checkvars = {}
2381            checkvars['S'] = None
2382            checkvars['SRC_URI'] = url.replace(recipever, '${PV}')
2383            self._test_recipe_contents(recipefile, checkvars, [])
2384        add_recipe()
2385        # Now rename it - change both name and version
2386        newrecipename = 'mynewrecipe'
2387        newrecipever = '456'
2388        newrecipefile = os.path.join(self.workspacedir, 'recipes', newrecipename, '%s_%s.bb' % (newrecipename, newrecipever))
2389        result = runCmd('devtool rename %s %s -V %s' % (recipename, newrecipename, newrecipever))
2390        self.assertExists(newrecipefile, 'Recipe file not renamed')
2391        self.assertNotExists(os.path.join(self.workspacedir, 'recipes', recipename), 'Old recipe directory still exists')
2392        newsrctree = os.path.join(self.workspacedir, 'sources', newrecipename)
2393        self.assertExists(newsrctree, 'Source directory not renamed')
2394        checkvars = {}
2395        checkvars['S'] = '${WORKDIR}/%s-%s' % (recipename, recipever)
2396        checkvars['SRC_URI'] = url
2397        self._test_recipe_contents(newrecipefile, checkvars, [])
2398        # Try again - change just name this time
2399        result = runCmd('devtool reset -n %s' % newrecipename)
2400        add_recipe()
2401        newrecipefile = os.path.join(self.workspacedir, 'recipes', newrecipename, '%s_%s.bb' % (newrecipename, recipever))
2402        result = runCmd('devtool rename %s %s' % (recipename, newrecipename))
2403        self.assertExists(newrecipefile, 'Recipe file not renamed')
2404        self.assertNotExists(os.path.join(self.workspacedir, 'recipes', recipename), 'Old recipe directory still exists')
2405        self.assertExists(os.path.join(self.workspacedir, 'sources', newrecipename), 'Source directory not renamed')
2406        checkvars = {}
2407        checkvars['S'] = '${WORKDIR}/%s-${PV}' % recipename
2408        checkvars['SRC_URI'] = url.replace(recipever, '${PV}')
2409        self._test_recipe_contents(newrecipefile, checkvars, [])
2410        # Try again - change just version this time
2411        result = runCmd('devtool reset -n %s' % newrecipename)
2412        add_recipe()
2413        newrecipefile = os.path.join(self.workspacedir, 'recipes', recipename, '%s_%s.bb' % (recipename, newrecipever))
2414        result = runCmd('devtool rename %s -V %s' % (recipename, newrecipever))
2415        self.assertExists(newrecipefile, 'Recipe file not renamed')
2416        self.assertExists(os.path.join(self.workspacedir, 'sources', recipename), 'Source directory no longer exists')
2417        checkvars = {}
2418        checkvars['S'] = '${WORKDIR}/${BPN}-%s' % recipever
2419        checkvars['SRC_URI'] = url
2420        self._test_recipe_contents(newrecipefile, checkvars, [])
2421
2422    def test_devtool_virtual_kernel_modify(self):
2423        """
2424        Summary:        The purpose of this test case is to verify that
2425                        devtool modify works correctly when building
2426                        the kernel.
2427        Dependencies:   NA
2428        Steps:          1. Build kernel with bitbake.
2429                        2. Save the config file generated.
2430                        3. Clean the environment.
2431                        4. Use `devtool modify virtual/kernel` to validate following:
2432                           4.1 The source is checked out correctly.
2433                           4.2 The resulting configuration is the same as
2434                               what was get on step 2.
2435                           4.3 The Kernel can be build correctly.
2436                           4.4 Changes made on the source are reflected on the
2437                               subsequent builds.
2438                           4.5 Changes on the configuration are reflected on the
2439                               subsequent builds
2440         Expected:       devtool modify is able to checkout the source of the kernel
2441                         and modification to the source and configurations are reflected
2442                         when building the kernel.
2443        """
2444        kernel_provider = self.td['PREFERRED_PROVIDER_virtual/kernel']
2445
2446        # Clean up the environment
2447        bitbake('%s -c clean' % kernel_provider)
2448        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
2449        tempdir_cfg = tempfile.mkdtemp(prefix='config_qa')
2450        self.track_for_cleanup(tempdir)
2451        self.track_for_cleanup(tempdir_cfg)
2452        self.track_for_cleanup(self.workspacedir)
2453        self.add_command_to_tearDown('bitbake -c clean %s' % kernel_provider)
2454        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
2455        #Step 1
2456        #Here is just generated the config file instead of all the kernel to optimize the
2457        #time of executing this test case.
2458        bitbake('%s -c configure' % kernel_provider)
2459        bbconfig = os.path.join(get_bb_var('B', kernel_provider),'.config')
2460        #Step 2
2461        runCmd('cp %s %s' % (bbconfig, tempdir_cfg))
2462        self.assertExists(os.path.join(tempdir_cfg, '.config'), 'Could not copy .config file from kernel')
2463
2464        tmpconfig = os.path.join(tempdir_cfg, '.config')
2465        #Step 3
2466        bitbake('%s -c clean' % kernel_provider)
2467        #Step 4.1
2468        runCmd('devtool modify virtual/kernel -x %s' % tempdir)
2469        self.assertExists(os.path.join(tempdir, 'Makefile'), 'Extracted source could not be found')
2470        #Step 4.2
2471        configfile = os.path.join(tempdir,'.config')
2472        runCmd('diff %s %s' % (tmpconfig, configfile))
2473
2474        #Step 4.3
2475        #NOTE: virtual/kernel is mapped to kernel_provider
2476        runCmd('devtool build %s' % kernel_provider)
2477        kernelfile = os.path.join(get_bb_var('KBUILD_OUTPUT', kernel_provider), 'vmlinux')
2478        self.assertExists(kernelfile, 'Kernel was not build correctly')
2479
2480        #Modify the kernel source
2481        modfile = os.path.join(tempdir, 'init/version.c')
2482        # Moved to uts.h in 6.1 onwards
2483        modfile2 = os.path.join(tempdir, 'include/linux/uts.h')
2484        runCmd("sed -i 's/Linux/LiNuX/g' %s %s" % (modfile, modfile2))
2485
2486        #Modify the configuration
2487        codeconfigfile = os.path.join(tempdir, '.config.new')
2488        modconfopt = "CONFIG_SG_POOL=n"
2489        runCmd("sed -i 's/CONFIG_SG_POOL=y/%s/' %s" % (modconfopt, codeconfigfile))
2490
2491        #Build again kernel with devtool
2492        runCmd('devtool build %s' % kernel_provider)
2493
2494        #Step 4.4
2495        runCmd("grep '%s' %s" % ('LiNuX', kernelfile))
2496
2497        #Step 4.5
2498        runCmd("grep %s %s" % (modconfopt, codeconfigfile))
2499
2500
2501class DevtoolIdeSdkTests(DevtoolBase):
2502    def _write_bb_config(self, recipe_names):
2503        """Helper to write the bitbake local.conf file"""
2504        conf_lines = [
2505            'IMAGE_CLASSES += "image-combined-dbg"',
2506            'IMAGE_GEN_DEBUGFS = "1"',
2507            'IMAGE_INSTALL:append = " gdbserver %s"' % ' '.join(
2508                [r + '-ptest' for r in recipe_names])
2509        ]
2510        self.write_config("\n".join(conf_lines))
2511
2512    def _check_workspace(self):
2513        """Check if a workspace directory is available and setup the cleanup"""
2514        self.assertTrue(not os.path.exists(self.workspacedir),
2515                        'This test cannot be run with a workspace directory under the build directory')
2516        self.track_for_cleanup(self.workspacedir)
2517        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
2518
2519    def _workspace_scripts_dir(self, recipe_name):
2520        return os.path.realpath(os.path.join(self.builddir, 'workspace', 'ide-sdk', recipe_name, 'scripts'))
2521
2522    def _sources_scripts_dir(self, src_dir):
2523        return os.path.realpath(os.path.join(src_dir, 'oe-scripts'))
2524
2525    def _workspace_gdbinit_dir(self, recipe_name):
2526        return os.path.realpath(os.path.join(self.builddir, 'workspace', 'ide-sdk', recipe_name, 'scripts', 'gdbinit'))
2527
2528    def _sources_gdbinit_dir(self, src_dir):
2529        return os.path.realpath(os.path.join(src_dir, 'oe-gdbinit'))
2530
2531    def _devtool_ide_sdk_recipe(self, recipe_name, build_file, testimage):
2532        """Setup a recipe for working with devtool ide-sdk
2533
2534        Basically devtool modify -x followed by some tests
2535        """
2536        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
2537        self.track_for_cleanup(tempdir)
2538        self.add_command_to_tearDown('bitbake -c clean %s' % recipe_name)
2539
2540        result = runCmd('devtool modify %s -x %s --debug-build' % (recipe_name, tempdir))
2541        self.assertExists(os.path.join(tempdir, build_file),
2542                          'Extracted source could not be found')
2543        self.assertExists(os.path.join(self.workspacedir, 'conf',
2544                                       'layer.conf'), 'Workspace directory not created')
2545        matches = glob.glob(os.path.join(self.workspacedir,
2546                            'appends', recipe_name + '.bbappend'))
2547        self.assertTrue(matches, 'bbappend not created %s' % result.output)
2548
2549        # Test devtool status
2550        result = runCmd('devtool status')
2551        self.assertIn(recipe_name, result.output)
2552        self.assertIn(tempdir, result.output)
2553        self._check_src_repo(tempdir)
2554
2555        # Usually devtool ide-sdk would initiate the build of the SDK.
2556        # But there is a circular dependency with starting Qemu and passing the IP of runqemu to devtool ide-sdk.
2557        if testimage:
2558            bitbake("%s qemu-native qemu-helper-native" % testimage)
2559            deploy_dir_image = get_bb_var('DEPLOY_DIR_IMAGE')
2560            self.add_command_to_tearDown('bitbake -c clean %s' % testimage)
2561            self.add_command_to_tearDown(
2562                'rm -f %s/%s*' % (deploy_dir_image, testimage))
2563
2564        return tempdir
2565
2566    def _get_recipe_ids(self, recipe_name):
2567        """IDs needed to write recipe specific config entries into IDE config files"""
2568        package_arch = get_bb_var('PACKAGE_ARCH', recipe_name)
2569        recipe_id = recipe_name + "-" + package_arch
2570        recipe_id_pretty = recipe_name + ": " + package_arch
2571        return (recipe_id, recipe_id_pretty)
2572
2573    def _verify_install_script_code(self, tempdir, recipe_name):
2574        """Verify the scripts referred by the tasks.json file are fine.
2575
2576        This function does not depend on Qemu. Therefore it verifies the scripts
2577        exists and the delete step works as expected. But it does not try to
2578        deploy to Qemu.
2579        """
2580        recipe_id, recipe_id_pretty = self._get_recipe_ids(recipe_name)
2581        with open(os.path.join(tempdir, '.vscode', 'tasks.json')) as tasks_j:
2582            tasks_d = json.load(tasks_j)
2583        tasks = tasks_d["tasks"]
2584        task_install = next(
2585            (task for task in tasks if task["label"] == "install && deploy-target %s" % recipe_id_pretty), None)
2586        self.assertIsNot(task_install, None)
2587        # execute only the bb_run_do_install script since the deploy would require e.g. Qemu running.
2588        i_and_d_script = "install_and_deploy_" + recipe_id
2589        i_and_d_script_path = os.path.join(
2590            self._workspace_scripts_dir(recipe_name), i_and_d_script)
2591        self.assertExists(i_and_d_script_path)
2592
2593    def _devtool_ide_sdk_qemu(self, tempdir, qemu, recipe_name, example_exe):
2594        """Verify deployment and execution in Qemu system work for one recipe.
2595
2596        This function checks the entire SDK workflow: changing the code, recompiling
2597        it and deploying it back to Qemu, and checking that the changes have been
2598        incorporated into the provided binaries. It also runs the tests of the recipe.
2599        """
2600        recipe_id, _ = self._get_recipe_ids(recipe_name)
2601        i_and_d_script = "install_and_deploy_" + recipe_id
2602        install_deploy_cmd = os.path.join(
2603            self._workspace_scripts_dir(recipe_name), i_and_d_script)
2604        self.assertExists(install_deploy_cmd,
2605                          '%s script not found' % install_deploy_cmd)
2606        runCmd(install_deploy_cmd)
2607
2608        MAGIC_STRING_ORIG = "Magic: 123456789"
2609        MAGIC_STRING_NEW = "Magic: 987654321"
2610        ptest_cmd = "ptest-runner " + recipe_name
2611
2612        # validate that SSH is working
2613        status, _ = qemu.run("uname")
2614        self.assertEqual(
2615            status, 0, msg="Failed to connect to the SSH server on Qemu")
2616
2617        # Verify the unmodified example prints the magic string
2618        status, output = qemu.run(example_exe)
2619        self.assertEqual(status, 0, msg="%s failed: %s" %
2620                         (example_exe, output))
2621        self.assertIn(MAGIC_STRING_ORIG, output)
2622
2623        # Verify the unmodified ptests work
2624        status, output = qemu.run(ptest_cmd)
2625        self.assertEqual(status, 0, msg="%s failed: %s" % (ptest_cmd, output))
2626        self.assertIn("PASS: cpp-example-lib", output)
2627
2628        # Verify remote debugging works
2629        self._gdb_cross_debugging(
2630            qemu, recipe_name, example_exe, MAGIC_STRING_ORIG)
2631
2632        # Replace the Magic String in the code, compile and deploy to Qemu
2633        cpp_example_lib_hpp = os.path.join(tempdir, 'cpp-example-lib.hpp')
2634        with open(cpp_example_lib_hpp, 'r') as file:
2635            cpp_code = file.read()
2636            cpp_code = cpp_code.replace(MAGIC_STRING_ORIG, MAGIC_STRING_NEW)
2637        with open(cpp_example_lib_hpp, 'w') as file:
2638            file.write(cpp_code)
2639        runCmd(install_deploy_cmd, cwd=tempdir)
2640
2641        # Verify the modified example prints the modified magic string
2642        status, output = qemu.run(example_exe)
2643        self.assertEqual(status, 0, msg="%s failed: %s" %
2644                         (example_exe, output))
2645        self.assertNotIn(MAGIC_STRING_ORIG, output)
2646        self.assertIn(MAGIC_STRING_NEW, output)
2647
2648        # Verify the modified example ptests work
2649        status, output = qemu.run(ptest_cmd)
2650        self.assertEqual(status, 0, msg="%s failed: %s" % (ptest_cmd, output))
2651        self.assertIn("PASS: cpp-example-lib", output)
2652
2653        # Verify remote debugging works wit the modified magic string
2654        self._gdb_cross_debugging(
2655            qemu, recipe_name, example_exe, MAGIC_STRING_NEW)
2656
2657    def _gdb_cross(self):
2658        """Verify gdb-cross is provided by devtool ide-sdk"""
2659        target_arch = self.td["TARGET_ARCH"]
2660        target_sys = self.td["TARGET_SYS"]
2661        gdb_recipe = "gdb-cross-" + target_arch
2662        gdb_binary = target_sys + "-gdb"
2663
2664        native_sysroot = get_bb_var("RECIPE_SYSROOT_NATIVE", gdb_recipe)
2665        r = runCmd("%s --version" % gdb_binary,
2666                   native_sysroot=native_sysroot, target_sys=target_sys)
2667        self.assertEqual(r.status, 0)
2668        self.assertIn("GNU gdb", r.output)
2669
2670    def _gdb_cross_debugging(self, qemu, recipe_name, example_exe, magic_string):
2671        """Verify gdb-cross is working
2672
2673        Test remote debugging:
2674        break main
2675        run
2676        continue
2677        break CppExample::print_json()
2678        continue
2679        print CppExample::test_string.compare("cpp-example-lib Magic: 123456789")
2680        $1 = 0
2681        print CppExample::test_string.compare("cpp-example-lib Magic: 123456789aaa")
2682        $2 = -3
2683        list cpp-example-lib.hpp:13,13
2684        13	    inline static const std::string test_string = "cpp-example-lib Magic: 123456789";
2685        continue
2686        """
2687        sshargs = '-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no'
2688        gdbserver_script = os.path.join(self._workspace_scripts_dir(
2689            recipe_name), 'gdbserver_1234_usr-bin-' + example_exe + '_m')
2690        gdb_script = os.path.join(self._workspace_scripts_dir(
2691            recipe_name), 'gdb_1234_usr-bin-' + example_exe)
2692
2693        # Start a gdbserver
2694        r = runCmd(gdbserver_script)
2695        self.assertEqual(r.status, 0)
2696
2697        # Check there is a gdbserver running
2698        r = runCmd('ssh %s root@%s %s' % (sshargs, qemu.ip, 'ps'))
2699        self.assertEqual(r.status, 0)
2700        self.assertIn("gdbserver ", r.output)
2701
2702        # Check the pid file is correct
2703        test_cmd = "cat /proc/$(cat /tmp/gdbserver_1234_usr-bin-" + \
2704            example_exe + "/pid)/cmdline"
2705        r = runCmd('ssh %s root@%s %s' % (sshargs, qemu.ip, test_cmd))
2706        self.assertEqual(r.status, 0)
2707        self.assertIn("gdbserver", r.output)
2708
2709        # Test remote debugging works
2710        gdb_batch_cmd = " --batch -ex 'break main' -ex 'run'"
2711        gdb_batch_cmd += " -ex 'break CppExample::print_json()' -ex 'continue'"
2712        gdb_batch_cmd += " -ex 'print CppExample::test_string.compare(\"cpp-example-lib %s\")'" % magic_string
2713        gdb_batch_cmd += " -ex 'print CppExample::test_string.compare(\"cpp-example-lib %saaa\")'" % magic_string
2714        gdb_batch_cmd += " -ex 'list cpp-example-lib.hpp:13,13'"
2715        gdb_batch_cmd += " -ex 'continue'"
2716        r = runCmd(gdb_script + gdb_batch_cmd)
2717        self.logger.debug("%s %s returned: %s", gdb_script,
2718                          gdb_batch_cmd, r.output)
2719        self.assertEqual(r.status, 0)
2720        self.assertIn("Breakpoint 1, main", r.output)
2721        self.assertIn("$1 = 0", r.output)  # test.string.compare equal
2722        self.assertIn("$2 = -3", r.output)  # test.string.compare longer
2723        self.assertIn(
2724            'inline static const std::string test_string = "cpp-example-lib %s";' % magic_string, r.output)
2725        self.assertIn("exited normally", r.output)
2726
2727        # Stop the gdbserver
2728        r = runCmd(gdbserver_script + ' stop')
2729        self.assertEqual(r.status, 0)
2730
2731        # Check there is no gdbserver running
2732        r = runCmd('ssh %s root@%s %s' % (sshargs, qemu.ip, 'ps'))
2733        self.assertEqual(r.status, 0)
2734        self.assertNotIn("gdbserver ", r.output)
2735
2736    def _verify_cmake_preset(self, tempdir):
2737        """Verify the generated cmake preset works as expected
2738
2739        Check if compiling works
2740        Check if unit tests can be executed in qemu (not qemu-system)
2741        """
2742        with open(os.path.join(tempdir, 'CMakeUserPresets.json')) as cmake_preset_j:
2743            cmake_preset_d = json.load(cmake_preset_j)
2744        config_presets = cmake_preset_d["configurePresets"]
2745        self.assertEqual(len(config_presets), 1)
2746        cmake_exe = config_presets[0]["cmakeExecutable"]
2747        preset_name = config_presets[0]["name"]
2748
2749        # Verify the wrapper for cmake native is available
2750        self.assertExists(cmake_exe)
2751
2752        # Verify the cmake preset generated by devtool ide-sdk is available
2753        result = runCmd('%s --list-presets' % cmake_exe, cwd=tempdir)
2754        self.assertIn(preset_name, result.output)
2755
2756        # Verify cmake re-uses the o files compiled by bitbake
2757        result = runCmd('%s --build --preset %s' %
2758                        (cmake_exe, preset_name), cwd=tempdir)
2759        self.assertIn("ninja: no work to do.", result.output)
2760
2761        # Verify the unit tests work (in Qemu user mode)
2762        result = runCmd('%s --build --preset %s --target test' %
2763                        (cmake_exe, preset_name), cwd=tempdir)
2764        self.assertIn("100% tests passed", result.output)
2765
2766        # Verify re-building and testing works again
2767        result = runCmd('%s --build --preset %s --target clean' %
2768                        (cmake_exe, preset_name), cwd=tempdir)
2769        self.assertIn("Cleaning", result.output)
2770        result = runCmd('%s --build --preset %s' %
2771                        (cmake_exe, preset_name), cwd=tempdir)
2772        self.assertIn("Building", result.output)
2773        self.assertIn("Linking", result.output)
2774        result = runCmd('%s --build --preset %s --target test' %
2775                        (cmake_exe, preset_name), cwd=tempdir)
2776        self.assertIn("Running tests...", result.output)
2777        self.assertIn("100% tests passed", result.output)
2778
2779    @OETestTag("runqemu")
2780    def test_devtool_ide_sdk_none_qemu(self):
2781        """Start qemu-system and run tests for multiple recipes. ide=none is used."""
2782        recipe_names = ["cmake-example", "meson-example"]
2783        testimage = "oe-selftest-image"
2784
2785        self._check_workspace()
2786        self._write_bb_config(recipe_names)
2787        self._check_runqemu_prerequisites()
2788
2789        # Verify deployment to Qemu (system mode) works
2790        bitbake(testimage)
2791        with runqemu(testimage, runqemuparams="nographic") as qemu:
2792            # cmake-example recipe
2793            recipe_name = "cmake-example"
2794            example_exe = "cmake-example"
2795            build_file = "CMakeLists.txt"
2796            tempdir = self._devtool_ide_sdk_recipe(
2797                recipe_name, build_file, testimage)
2798            bitbake_sdk_cmd = 'devtool ide-sdk %s %s -t root@%s -c --ide=none' % (
2799                recipe_name, testimage, qemu.ip)
2800            runCmd(bitbake_sdk_cmd)
2801            self._gdb_cross()
2802            self._verify_cmake_preset(tempdir)
2803            self._devtool_ide_sdk_qemu(tempdir, qemu, recipe_name, example_exe)
2804            # Verify the oe-scripts sym-link is valid
2805            self.assertEqual(self._workspace_scripts_dir(
2806                recipe_name), self._sources_scripts_dir(tempdir))
2807
2808            # meson-example recipe
2809            recipe_name = "meson-example"
2810            example_exe = "mesonex"
2811            build_file = "meson.build"
2812            tempdir = self._devtool_ide_sdk_recipe(
2813                recipe_name, build_file, testimage)
2814            bitbake_sdk_cmd = 'devtool ide-sdk %s %s -t root@%s -c --ide=none' % (
2815                recipe_name, testimage, qemu.ip)
2816            runCmd(bitbake_sdk_cmd)
2817            self._gdb_cross()
2818            self._devtool_ide_sdk_qemu(tempdir, qemu, recipe_name, example_exe)
2819            # Verify the oe-scripts sym-link is valid
2820            self.assertEqual(self._workspace_scripts_dir(
2821                recipe_name), self._sources_scripts_dir(tempdir))
2822
2823    def test_devtool_ide_sdk_code_cmake(self):
2824        """Verify a cmake recipe works with ide=code mode"""
2825        recipe_name = "cmake-example"
2826        build_file = "CMakeLists.txt"
2827        testimage = "oe-selftest-image"
2828
2829        self._check_workspace()
2830        self._write_bb_config([recipe_name])
2831        tempdir = self._devtool_ide_sdk_recipe(
2832            recipe_name, build_file, testimage)
2833        bitbake_sdk_cmd = 'devtool ide-sdk %s %s -t root@192.168.17.17 -c --ide=code' % (
2834            recipe_name, testimage)
2835        runCmd(bitbake_sdk_cmd)
2836        self._verify_cmake_preset(tempdir)
2837        self._verify_install_script_code(tempdir,  recipe_name)
2838        self._gdb_cross()
2839
2840    def test_devtool_ide_sdk_code_meson(self):
2841        """Verify a meson recipe works with ide=code mode"""
2842        recipe_name = "meson-example"
2843        build_file = "meson.build"
2844        testimage = "oe-selftest-image"
2845
2846        self._check_workspace()
2847        self._write_bb_config([recipe_name])
2848        tempdir = self._devtool_ide_sdk_recipe(
2849            recipe_name, build_file, testimage)
2850        bitbake_sdk_cmd = 'devtool ide-sdk %s %s -t root@192.168.17.17 -c --ide=code' % (
2851            recipe_name, testimage)
2852        runCmd(bitbake_sdk_cmd)
2853
2854        with open(os.path.join(tempdir, '.vscode', 'settings.json')) as settings_j:
2855            settings_d = json.load(settings_j)
2856        meson_exe = settings_d["mesonbuild.mesonPath"]
2857        meson_build_folder = settings_d["mesonbuild.buildFolder"]
2858
2859        # Verify the wrapper for meson native is available
2860        self.assertExists(meson_exe)
2861
2862        # Verify meson re-uses the o files compiled by bitbake
2863        result = runCmd('%s compile -C  %s' %
2864                        (meson_exe, meson_build_folder), cwd=tempdir)
2865        self.assertIn("ninja: no work to do.", result.output)
2866
2867        # Verify the unit tests work (in Qemu)
2868        runCmd('%s test -C %s' % (meson_exe, meson_build_folder), cwd=tempdir)
2869
2870        # Verify re-building and testing works again
2871        result = runCmd('%s compile -C  %s --clean' %
2872                        (meson_exe, meson_build_folder), cwd=tempdir)
2873        self.assertIn("Cleaning...", result.output)
2874        result = runCmd('%s compile -C  %s' %
2875                        (meson_exe, meson_build_folder), cwd=tempdir)
2876        self.assertIn("Linking target", result.output)
2877        runCmd('%s test -C %s' % (meson_exe, meson_build_folder), cwd=tempdir)
2878
2879        self._verify_install_script_code(tempdir,  recipe_name)
2880        self._gdb_cross()
2881
2882    def test_devtool_ide_sdk_shared_sysroots(self):
2883        """Verify the shared sysroot SDK"""
2884
2885        # Handle the workspace (which is not needed by this test case)
2886        self._check_workspace()
2887
2888        result_init = runCmd(
2889            'devtool ide-sdk -m shared oe-selftest-image cmake-example meson-example --ide=code')
2890        bb_vars = get_bb_vars(
2891            ['REAL_MULTIMACH_TARGET_SYS', 'DEPLOY_DIR_IMAGE', 'COREBASE'], "meta-ide-support")
2892        environment_script = 'environment-setup-%s' % bb_vars['REAL_MULTIMACH_TARGET_SYS']
2893        deploydir = bb_vars['DEPLOY_DIR_IMAGE']
2894        environment_script_path = os.path.join(deploydir, environment_script)
2895        cpp_example_src = os.path.join(
2896            bb_vars['COREBASE'], 'meta-selftest', 'recipes-test', 'cpp', 'files')
2897
2898        # Verify the cross environment script is available
2899        self.assertExists(environment_script_path)
2900
2901        def runCmdEnv(cmd, cwd):
2902            cmd = '/bin/sh -c ". %s > /dev/null && %s"' % (
2903                environment_script_path, cmd)
2904            return runCmd(cmd, cwd)
2905
2906        # Verify building the C++ example works with CMake
2907        tempdir_cmake = tempfile.mkdtemp(prefix='devtoolqa')
2908        self.track_for_cleanup(tempdir_cmake)
2909
2910        result_cmake = runCmdEnv("which cmake", cwd=tempdir_cmake)
2911        cmake_native = os.path.normpath(result_cmake.output.strip())
2912        self.assertExists(cmake_native)
2913
2914        runCmdEnv('cmake %s' % cpp_example_src, cwd=tempdir_cmake)
2915        runCmdEnv('cmake --build %s' % tempdir_cmake, cwd=tempdir_cmake)
2916
2917        # Verify the printed note really referres to a cmake executable
2918        cmake_native_code = ""
2919        for line in result_init.output.splitlines():
2920            m = re.search(r'"cmake.cmakePath": "(.*)"', line)
2921            if m:
2922                cmake_native_code = m.group(1)
2923                break
2924        self.assertExists(cmake_native_code)
2925        self.assertEqual(cmake_native, cmake_native_code)
2926
2927        # Verify building the C++ example works with Meson
2928        tempdir_meson = tempfile.mkdtemp(prefix='devtoolqa')
2929        self.track_for_cleanup(tempdir_meson)
2930
2931        result_cmake = runCmdEnv("which meson", cwd=tempdir_meson)
2932        meson_native = os.path.normpath(result_cmake.output.strip())
2933        self.assertExists(meson_native)
2934
2935        runCmdEnv('meson setup %s' % tempdir_meson, cwd=cpp_example_src)
2936        runCmdEnv('meson compile', cwd=tempdir_meson)
2937
2938    def test_devtool_ide_sdk_plugins(self):
2939        """Test that devtool ide-sdk can use plugins from other layers."""
2940
2941        # We need a workspace layer and a modified recipe (but no image)
2942        modified_recipe_name = "meson-example"
2943        modified_build_file = "meson.build"
2944        testimage = "oe-selftest-image"
2945        shared_recipe_name = "cmake-example"
2946
2947        self._check_workspace()
2948        self._write_bb_config([modified_recipe_name])
2949        tempdir = self._devtool_ide_sdk_recipe(
2950            modified_recipe_name, modified_build_file, None)
2951
2952        IDE_RE = re.compile(r'.*--ide \{(.*)\}.*')
2953
2954        def get_ides_from_help(help_str):
2955            m = IDE_RE.search(help_str)
2956            return m.group(1).split(',')
2957
2958        # verify the default plugins are available but the foo plugin is not
2959        result = runCmd('devtool ide-sdk -h')
2960        found_ides = get_ides_from_help(result.output)
2961        self.assertIn('code', found_ides)
2962        self.assertIn('none', found_ides)
2963        self.assertNotIn('foo', found_ides)
2964
2965        shared_config_file = os.path.join(tempdir, 'shared-config.txt')
2966        shared_config_str = 'Dummy shared IDE config'
2967        modified_config_file = os.path.join(tempdir, 'modified-config.txt')
2968        modified_config_str = 'Dummy modified IDE config'
2969
2970        # Generate a foo plugin in the workspace layer
2971        plugin_dir = os.path.join(
2972            self.workspacedir, 'lib', 'devtool', 'ide_plugins')
2973        os.makedirs(plugin_dir)
2974        plugin_code = 'from devtool.ide_plugins import IdeBase\n\n'
2975        plugin_code += 'class IdeFoo(IdeBase):\n'
2976        plugin_code += '    def setup_shared_sysroots(self, shared_env):\n'
2977        plugin_code += '        with open("%s", "w") as config_file:\n' % shared_config_file
2978        plugin_code += '            config_file.write("%s")\n\n' % shared_config_str
2979        plugin_code += '    def setup_modified_recipe(self, args, image_recipe, modified_recipe):\n'
2980        plugin_code += '        with open("%s", "w") as config_file:\n' % modified_config_file
2981        plugin_code += '            config_file.write("%s")\n\n' % modified_config_str
2982        plugin_code += 'def register_ide_plugin(ide_plugins):\n'
2983        plugin_code += '    ide_plugins["foo"] = IdeFoo\n'
2984
2985        plugin_py = os.path.join(plugin_dir, 'ide_foo.py')
2986        with open(plugin_py, 'w') as plugin_file:
2987            plugin_file.write(plugin_code)
2988
2989        # Verify the foo plugin is available as well
2990        result = runCmd('devtool ide-sdk -h')
2991        found_ides = get_ides_from_help(result.output)
2992        self.assertIn('code', found_ides)
2993        self.assertIn('none', found_ides)
2994        self.assertIn('foo', found_ides)
2995
2996        # Verify the foo plugin generates a shared config
2997        result = runCmd(
2998            'devtool ide-sdk -m shared --skip-bitbake --ide foo %s' % shared_recipe_name)
2999        with open(shared_config_file) as shared_config:
3000            shared_config_new = shared_config.read()
3001        self.assertEqual(shared_config_str, shared_config_new)
3002
3003        # Verify the foo plugin generates a modified config
3004        result = runCmd('devtool ide-sdk --skip-bitbake --ide foo %s %s' %
3005                        (modified_recipe_name, testimage))
3006        with open(modified_config_file) as modified_config:
3007            modified_config_new = modified_config.read()
3008        self.assertEqual(modified_config_str, modified_config_new)
3009