diff options
Diffstat (limited to 'meta/lib/oeqa/selftest')
80 files changed, 9535 insertions, 1440 deletions
diff --git a/meta/lib/oeqa/selftest/case.py b/meta/lib/oeqa/selftest/case.py index ac3308d8a4..da35b25f68 100644 --- a/meta/lib/oeqa/selftest/case.py +++ b/meta/lib/oeqa/selftest/case.py @@ -6,7 +6,6 @@ import sys import os -import shutil import glob import errno from unittest.util import safe_repr @@ -30,9 +29,7 @@ class OESelftestTestCase(OETestCase): cls.builddir = cls.tc.config_paths['builddir'] cls.localconf_path = cls.tc.config_paths['localconf'] - cls.localconf_backup = cls.tc.config_paths['localconf_class_backup'] cls.local_bblayers_path = cls.tc.config_paths['bblayers'] - cls.local_bblayers_backup = cls.tc.config_paths['bblayers_class_backup'] cls.testinc_path = os.path.join(cls.tc.config_paths['builddir'], "conf/selftest.inc") @@ -43,8 +40,7 @@ class OESelftestTestCase(OETestCase): cls._track_for_cleanup = [ cls.testinc_path, cls.testinc_bblayers_path, - cls.machineinc_path, cls.localconf_backup, - cls.local_bblayers_backup] + cls.machineinc_path] cls.add_include() @@ -102,30 +98,6 @@ class OESelftestTestCase(OETestCase): def setUp(self): super(OESelftestTestCase, self).setUp() os.chdir(self.builddir) - # Check if local.conf or bblayers.conf files backup exists - # from a previous failed test and restore them - if os.path.isfile(self.localconf_backup) or os.path.isfile( - self.local_bblayers_backup): - self.logger.debug("\ -Found a local.conf and/or bblayers.conf backup from a previously aborted test.\ -Restoring these files now, but tests should be re-executed from a clean environment\ -to ensure accurate results.") - try: - shutil.copyfile(self.localconf_backup, self.localconf_path) - except OSError as e: - if e.errno != errno.ENOENT: - raise - try: - shutil.copyfile(self.local_bblayers_backup, - self.local_bblayers_path) - except OSError as e: - if e.errno != errno.ENOENT: - raise - else: - # backup local.conf and bblayers.conf - shutil.copyfile(self.localconf_path, self.localconf_backup) - shutil.copyfile(self.local_bblayers_path, self.local_bblayers_backup) - self.logger.debug("Creating local.conf and bblayers.conf backups.") # we don't know what the previous test left around in config or inc files # if it failed so we need a fresh start try: @@ -145,10 +117,6 @@ to ensure accurate results.") if e.errno != errno.ENOENT: raise - if self.tc.custommachine: - machine_conf = 'MACHINE ??= "%s"\n' % self.tc.custommachine - self.set_machine_config(machine_conf) - # tests might need their own setup # but if they overwrite this one they have to call # super each time, so let's give them an alternative @@ -206,19 +174,11 @@ to ensure accurate results.") self.logger.debug("Writing to: %s\n%s\n" % (dest_path, data)) ftools.write_file(dest_path, data) - if not multiconfig and self.tc.custommachine and 'MACHINE' in data: - machine = get_bb_var('MACHINE') - self.logger.warning('MACHINE overridden: %s' % machine) - def append_config(self, data): """Append to <builddir>/conf/selftest.inc""" self.logger.debug("Appending to: %s\n%s\n" % (self.testinc_path, data)) ftools.append_file(self.testinc_path, data) - if self.tc.custommachine and 'MACHINE' in data: - machine = get_bb_var('MACHINE') - self.logger.warning('MACHINE overridden: %s' % machine) - def remove_config(self, data): """Remove data from <builddir>/conf/selftest.inc""" self.logger.debug("Removing from: %s\n%s\n" % (self.testinc_path, data)) @@ -277,6 +237,13 @@ to ensure accurate results.") self.logger.debug("Writing to: %s\n%s\n" % (self.machineinc_path, data)) ftools.write_file(self.machineinc_path, data) + def disable_class(self, classname): + destfile = "%s/classes/%s.bbclass" % (self.builddir, classname) + os.makedirs(os.path.dirname(destfile), exist_ok=True) + self.track_for_cleanup(destfile) + self.logger.debug("Creating empty class: %s\n" % (destfile)) + ftools.write_file(destfile, "") + # check does path exist def assertExists(self, expr, msg=None): if not os.path.exists(expr): diff --git a/meta/lib/oeqa/selftest/cases/_sstatetests_noauto.py b/meta/lib/oeqa/selftest/cases/_sstatetests_noauto.py index f7c356ad09..2c9584d329 100644 --- a/meta/lib/oeqa/selftest/cases/_sstatetests_noauto.py +++ b/meta/lib/oeqa/selftest/cases/_sstatetests_noauto.py @@ -1,4 +1,6 @@ # +# Copyright OpenEmbedded Contributors +# # SPDX-License-Identifier: MIT # @@ -6,8 +8,8 @@ import os import shutil import oeqa.utils.ftools as ftools -from oeqa.utils.commands import runCmd, bitbake, get_bb_var, get_test_layer -from oeqa.selftest.cases.sstate import SStateBase +from oeqa.utils.commands import runCmd, bitbake, get_bb_var +from oeqa.selftest.cases.sstatetests import SStateBase class RebuildFromSState(SStateBase): @@ -90,7 +92,7 @@ class RebuildFromSState(SStateBase): self.assertFalse(failed_cleansstate, msg="The following recipes have failed cleansstate(all others have passed both cleansstate and rebuild from sstate tests): %s" % ' '.join(map(str, failed_cleansstate))) def test_sstate_relocation(self): - self.run_test_sstate_rebuild(['core-image-sato-sdk'], relocate=True, rebuild_dependencies=True) + self.run_test_sstate_rebuild(['core-image-weston-sdk'], relocate=True, rebuild_dependencies=True) def test_sstate_rebuild(self): - self.run_test_sstate_rebuild(['core-image-sato-sdk'], relocate=False, rebuild_dependencies=True) + self.run_test_sstate_rebuild(['core-image-weston-sdk'], relocate=False, rebuild_dependencies=True) diff --git a/meta/lib/oeqa/selftest/cases/archiver.py b/meta/lib/oeqa/selftest/cases/archiver.py index 6bd0e06ec4..3cb888c506 100644 --- a/meta/lib/oeqa/selftest/cases/archiver.py +++ b/meta/lib/oeqa/selftest/cases/archiver.py @@ -1,9 +1,12 @@ # +# Copyright OpenEmbedded Contributors +# # SPDX-License-Identifier: MIT # import os import glob +import re from oeqa.utils.commands import bitbake, get_bb_vars from oeqa.selftest.case import OESelftestTestCase @@ -19,8 +22,8 @@ class Archiver(OESelftestTestCase): AutomatedBy: Daniel Istrate <daniel.alexandrux.istrate@intel.com> """ - include_recipe = 'busybox' - exclude_recipe = 'zlib' + include_recipe = 'selftest-ed' + exclude_recipe = 'initscripts' features = 'INHERIT += "archiver"\n' features += 'ARCHIVER_MODE[src] = "original"\n' @@ -35,11 +38,11 @@ class Archiver(OESelftestTestCase): src_path = os.path.join(bb_vars['DEPLOY_DIR_SRC'], bb_vars['TARGET_SYS']) # Check that include_recipe was included - included_present = len(glob.glob(src_path + '/%s-*' % include_recipe)) + included_present = len(glob.glob(src_path + '/%s-*/*' % include_recipe)) self.assertTrue(included_present, 'Recipe %s was not included.' % include_recipe) # Check that exclude_recipe was excluded - excluded_present = len(glob.glob(src_path + '/%s-*' % exclude_recipe)) + excluded_present = len(glob.glob(src_path + '/%s-*/*' % exclude_recipe)) self.assertFalse(excluded_present, 'Recipe %s was not excluded.' % exclude_recipe) def test_archiver_filters_by_type(self): @@ -51,8 +54,8 @@ class Archiver(OESelftestTestCase): Author: André Draszik <adraszik@tycoint.com> """ - target_recipe = 'initscripts' - native_recipe = 'zlib-native' + target_recipe = 'selftest-ed' + native_recipe = 'selftest-ed-native' features = 'INHERIT += "archiver"\n' features += 'ARCHIVER_MODE[src] = "original"\n' @@ -67,11 +70,11 @@ class Archiver(OESelftestTestCase): src_path_native = os.path.join(bb_vars['DEPLOY_DIR_SRC'], bb_vars['BUILD_SYS']) # Check that target_recipe was included - included_present = len(glob.glob(src_path_target + '/%s-*' % target_recipe)) + included_present = len(glob.glob(src_path_target + '/%s-*/*' % target_recipe)) self.assertTrue(included_present, 'Recipe %s was not included.' % target_recipe) # Check that native_recipe was excluded - excluded_present = len(glob.glob(src_path_native + '/%s-*' % native_recipe)) + excluded_present = len(glob.glob(src_path_native + '/%s-*/*' % native_recipe)) self.assertFalse(excluded_present, 'Recipe %s was not excluded.' % native_recipe) def test_archiver_filters_by_type_and_name(self): @@ -86,8 +89,8 @@ class Archiver(OESelftestTestCase): Author: André Draszik <adraszik@tycoint.com> """ - target_recipes = [ 'initscripts', 'zlib' ] - native_recipes = [ 'update-rc.d-native', 'zlib-native' ] + target_recipes = [ 'initscripts', 'selftest-ed' ] + native_recipes = [ 'update-rc.d-native', 'selftest-ed-native' ] features = 'INHERIT += "archiver"\n' features += 'ARCHIVER_MODE[src] = "original"\n' @@ -104,20 +107,51 @@ class Archiver(OESelftestTestCase): src_path_native = os.path.join(bb_vars['DEPLOY_DIR_SRC'], bb_vars['BUILD_SYS']) # Check that target_recipe[0] and native_recipes[1] were included - included_present = len(glob.glob(src_path_target + '/%s-*' % target_recipes[0])) + included_present = len(glob.glob(src_path_target + '/%s-*/*' % target_recipes[0])) self.assertTrue(included_present, 'Recipe %s was not included.' % target_recipes[0]) - included_present = len(glob.glob(src_path_native + '/%s-*' % native_recipes[1])) + included_present = len(glob.glob(src_path_native + '/%s-*/*' % native_recipes[1])) self.assertTrue(included_present, 'Recipe %s was not included.' % native_recipes[1]) # Check that native_recipes[0] and target_recipes[1] were excluded - excluded_present = len(glob.glob(src_path_native + '/%s-*' % native_recipes[0])) + excluded_present = len(glob.glob(src_path_native + '/%s-*/*' % native_recipes[0])) self.assertFalse(excluded_present, 'Recipe %s was not excluded.' % native_recipes[0]) - excluded_present = len(glob.glob(src_path_target + '/%s-*' % target_recipes[1])) + excluded_present = len(glob.glob(src_path_target + '/%s-*/*' % target_recipes[1])) self.assertFalse(excluded_present, 'Recipe %s was not excluded.' % target_recipes[1]) + def test_archiver_multiconfig_shared_unpack_and_patch(self): + """ + Test that shared recipes in original mode with diff enabled works in multiconfig, + otherwise it will not build when using the same TMP dir. + """ + + features = 'BBMULTICONFIG = "mc1 mc2"\n' + features += 'INHERIT += "archiver"\n' + features += 'ARCHIVER_MODE[src] = "original"\n' + features += 'ARCHIVER_MODE[diff] = "1"\n' + self.write_config(features) + + # We can use any machine in multiconfig as long as they are different + self.write_config('MACHINE = "qemuarm"\n', 'mc1') + self.write_config('MACHINE = "qemux86"\n', 'mc2') + task = 'do_unpack_and_patch' + # Use gcc-source as it is a shared recipe (appends the pv to the pn) + pn = 'gcc-source-%s' % get_bb_vars(['PV'], 'gcc')['PV'] + + # Generate the tasks signatures + bitbake('mc:mc1:%s mc:mc2:%s -c %s -S lockedsigs' % (pn, pn, task)) + + # Check the tasks signatures + # To be machine agnostic the tasks needs to generate the same signature for each machine + locked_sigs_inc = "%s/locked-sigs.inc" % self.builddir + locked_sigs = open(locked_sigs_inc).read() + task_sigs = re.findall(r"%s:%s:.*" % (pn, task), locked_sigs) + uniq_sigs = set(task_sigs) + self.assertFalse(len(uniq_sigs) - 1, \ + 'The task "%s" of the recipe "%s" has different signatures in "%s" for each machine in multiconfig' \ + % (task, pn, locked_sigs_inc)) def test_archiver_srpm_mode(self): """ @@ -126,12 +160,13 @@ class Archiver(OESelftestTestCase): features = 'INHERIT += "archiver"\n' features += 'ARCHIVER_MODE[srpm] = "1"\n' + features += 'PACKAGE_CLASSES = "package_rpm"\n' self.write_config(features) - bitbake('-n core-image-sato') + bitbake('-n selftest-nopackages selftest-ed') def _test_archiver_mode(self, mode, target_file_name, extra_config=None): - target = "selftest-ed" + target = 'selftest-ed-native' features = 'INHERIT += "archiver"\n' features += 'ARCHIVER_MODE[src] = "%s"\n' % (mode) @@ -142,8 +177,8 @@ class Archiver(OESelftestTestCase): bitbake('-c clean %s' % (target)) bitbake('-c deploy_archives %s' % (target)) - bb_vars = get_bb_vars(['DEPLOY_DIR_SRC', 'TARGET_SYS']) - glob_str = os.path.join(bb_vars['DEPLOY_DIR_SRC'], bb_vars['TARGET_SYS'], '%s-*' % (target)) + bb_vars = get_bb_vars(['DEPLOY_DIR_SRC', 'BUILD_SYS']) + glob_str = os.path.join(bb_vars['DEPLOY_DIR_SRC'], bb_vars['BUILD_SYS'], '%s-*' % (target)) glob_result = glob.glob(glob_str) self.assertTrue(glob_result, 'Missing archiver directory for %s' % (target)) @@ -152,46 +187,159 @@ class Archiver(OESelftestTestCase): def test_archiver_mode_original(self): """ - Test that the archiver works in with `ARCHIVER_MODE[src] = "original"`. + Test that the archiver works with `ARCHIVER_MODE[src] = "original"`. """ self._test_archiver_mode('original', 'ed-1.14.1.tar.lz') def test_archiver_mode_patched(self): """ - Test that the archiver works in with `ARCHIVER_MODE[src] = "patched"`. + Test that the archiver works with `ARCHIVER_MODE[src] = "patched"`. """ - self._test_archiver_mode('patched', 'selftest-ed-1.14.1-r0-patched.tar.gz') + self._test_archiver_mode('patched', 'selftest-ed-native-1.14.1-r0-patched.tar.xz') def test_archiver_mode_configured(self): """ - Test that the archiver works in with `ARCHIVER_MODE[src] = "configured"`. + Test that the archiver works with `ARCHIVER_MODE[src] = "configured"`. """ - self._test_archiver_mode('configured', 'selftest-ed-1.14.1-r0-configured.tar.gz') + self._test_archiver_mode('configured', 'selftest-ed-native-1.14.1-r0-configured.tar.xz') def test_archiver_mode_recipe(self): """ - Test that the archiver works in with `ARCHIVER_MODE[recipe] = "1"`. + Test that the archiver works with `ARCHIVER_MODE[recipe] = "1"`. """ - self._test_archiver_mode('patched', 'selftest-ed-1.14.1-r0-recipe.tar.gz', + self._test_archiver_mode('patched', 'selftest-ed-native-1.14.1-r0-recipe.tar.xz', 'ARCHIVER_MODE[recipe] = "1"\n') def test_archiver_mode_diff(self): """ - Test that the archiver works in with `ARCHIVER_MODE[diff] = "1"`. + Test that the archiver works with `ARCHIVER_MODE[diff] = "1"`. Exclusions controlled by `ARCHIVER_MODE[diff-exclude]` are not yet tested. """ - self._test_archiver_mode('patched', 'selftest-ed-1.14.1-r0-diff.gz', + self._test_archiver_mode('patched', 'selftest-ed-native-1.14.1-r0-diff.gz', 'ARCHIVER_MODE[diff] = "1"\n') def test_archiver_mode_dumpdata(self): """ - Test that the archiver works in with `ARCHIVER_MODE[dumpdata] = "1"`. + Test that the archiver works with `ARCHIVER_MODE[dumpdata] = "1"`. """ - self._test_archiver_mode('patched', 'selftest-ed-1.14.1-r0-showdata.dump', + self._test_archiver_mode('patched', 'selftest-ed-native-1.14.1-r0-showdata.dump', 'ARCHIVER_MODE[dumpdata] = "1"\n') + + def test_archiver_mode_mirror(self): + """ + Test that the archiver works with `ARCHIVER_MODE[src] = "mirror"`. + """ + + self._test_archiver_mode('mirror', 'ed-1.14.1.tar.lz', + 'BB_GENERATE_MIRROR_TARBALLS = "1"\n') + + def test_archiver_mode_mirror_excludes(self): + """ + Test that the archiver works with `ARCHIVER_MODE[src] = "mirror"` and + correctly excludes an archive when its URL matches + `ARCHIVER_MIRROR_EXCLUDE`. + """ + + target='selftest-ed' + target_file_name = 'ed-1.14.1.tar.lz' + + features = 'INHERIT += "archiver"\n' + features += 'ARCHIVER_MODE[src] = "mirror"\n' + features += 'BB_GENERATE_MIRROR_TARBALLS = "1"\n' + features += 'ARCHIVER_MIRROR_EXCLUDE = "${GNU_MIRROR}"\n' + self.write_config(features) + + bitbake('-c clean %s' % (target)) + bitbake('-c deploy_archives %s' % (target)) + + bb_vars = get_bb_vars(['DEPLOY_DIR_SRC', 'TARGET_SYS']) + glob_str = os.path.join(bb_vars['DEPLOY_DIR_SRC'], bb_vars['TARGET_SYS'], '%s-*' % (target)) + glob_result = glob.glob(glob_str) + self.assertTrue(glob_result, 'Missing archiver directory for %s' % (target)) + + archive_path = os.path.join(glob_result[0], target_file_name) + self.assertFalse(os.path.exists(archive_path), 'Failed to exclude archive file %s' % (target_file_name)) + + def test_archiver_mode_mirror_combined(self): + """ + Test that the archiver works with `ARCHIVER_MODE[src] = "mirror"` + and `ARCHIVER_MODE[mirror] = "combined"`. Archives for multiple recipes + should all end up in the 'mirror' directory. + """ + + features = 'INHERIT += "archiver"\n' + features += 'ARCHIVER_MODE[src] = "mirror"\n' + features += 'ARCHIVER_MODE[mirror] = "combined"\n' + features += 'BB_GENERATE_MIRROR_TARBALLS = "1"\n' + features += 'COPYLEFT_LICENSE_INCLUDE = "*"\n' + self.write_config(features) + + for target in ['selftest-ed', 'selftest-hardlink']: + bitbake('-c clean %s' % (target)) + bitbake('-c deploy_archives %s' % (target)) + + bb_vars = get_bb_vars(['DEPLOY_DIR_SRC']) + for target_file_name in ['ed-1.14.1.tar.lz', 'hello.c']: + glob_str = os.path.join(bb_vars['DEPLOY_DIR_SRC'], 'mirror', target_file_name) + glob_result = glob.glob(glob_str) + self.assertTrue(glob_result, 'Missing archive file %s' % (target_file_name)) + + def test_archiver_mode_mirror_gitsm(self): + """ + Test that the archiver correctly handles git submodules with + `ARCHIVER_MODE[src] = "mirror"`. + """ + features = 'INHERIT += "archiver"\n' + features += 'ARCHIVER_MODE[src] = "mirror"\n' + features += 'ARCHIVER_MODE[mirror] = "combined"\n' + features += 'BB_GENERATE_MIRROR_TARBALLS = "1"\n' + features += 'COPYLEFT_LICENSE_INCLUDE = "*"\n' + self.write_config(features) + + bitbake('-c clean git-submodule-test') + bitbake('-c deploy_archives -f git-submodule-test') + + bb_vars = get_bb_vars(['DEPLOY_DIR_SRC']) + for target_file_name in [ + 'git2_git.yoctoproject.org.git-submodule-test.tar.gz', + 'git2_git.yoctoproject.org.bitbake-gitsm-test1.tar.gz', + 'git2_git.yoctoproject.org.bitbake-gitsm-test2.tar.gz', + 'git2_git.openembedded.org.bitbake.tar.gz' + ]: + target_path = os.path.join(bb_vars['DEPLOY_DIR_SRC'], 'mirror', target_file_name) + self.assertTrue(os.path.exists(target_path)) + + def test_archiver_mode_mirror_gitsm_shallow(self): + """ + Test that the archiver correctly handles git submodules with + `ARCHIVER_MODE[src] = "mirror"`. + """ + features = 'INHERIT += "archiver"\n' + features += 'ARCHIVER_MODE[src] = "mirror"\n' + features += 'ARCHIVER_MODE[mirror] = "combined"\n' + features += 'BB_GENERATE_MIRROR_TARBALLS = "1"\n' + features += 'COPYLEFT_LICENSE_INCLUDE = "*"\n' + features += 'BB_GIT_SHALLOW = "1"\n' + features += 'BB_GENERATE_SHALLOW_TARBALLS = "1"\n' + features += 'DL_DIR = "${TOPDIR}/downloads-shallow"\n' + self.write_config(features) + + bitbake('-c clean git-submodule-test') + bitbake('-c deploy_archives -f git-submodule-test') + + bb_vars = get_bb_vars(['DEPLOY_DIR_SRC']) + for target_file_name in [ + 'gitsmshallow_git.yoctoproject.org.git-submodule-test_a2885dd-1_master.tar.gz', + 'gitsmshallow_git.yoctoproject.org.bitbake-gitsm-test1_bare_120f4c7-1.tar.gz', + 'gitsmshallow_git.yoctoproject.org.bitbake-gitsm-test2_bare_f66699e-1.tar.gz', + 'gitsmshallow_git.openembedded.org.bitbake_bare_52a144a-1.tar.gz', + 'gitsmshallow_git.openembedded.org.bitbake_bare_c39b997-1.tar.gz' + ]: + target_path = os.path.join(bb_vars['DEPLOY_DIR_SRC'], 'mirror', target_file_name) + self.assertTrue(os.path.exists(target_path)) diff --git a/meta/lib/oeqa/selftest/cases/baremetal.py b/meta/lib/oeqa/selftest/cases/baremetal.py new file mode 100644 index 0000000000..cadaea2f1a --- /dev/null +++ b/meta/lib/oeqa/selftest/cases/baremetal.py @@ -0,0 +1,14 @@ + +# +# Copyright OpenEmbedded Contributors +# +# SPDX-License-Identifier: MIT +# + +from oeqa.selftest.case import OESelftestTestCase +from oeqa.utils.commands import bitbake + +class BaremetalTest(OESelftestTestCase): + def test_baremetal(self): + self.write_config('TCLIBC = "baremetal"') + bitbake('baremetal-helloworld') diff --git a/meta/lib/oeqa/selftest/cases/bbclasses.py b/meta/lib/oeqa/selftest/cases/bbclasses.py new file mode 100644 index 0000000000..10545ebe65 --- /dev/null +++ b/meta/lib/oeqa/selftest/cases/bbclasses.py @@ -0,0 +1,106 @@ +# +# Copyright OpenEmbedded Contributors +# +# SPDX-License-Identifier: MIT +# + +from oeqa.selftest.case import OESelftestTestCase +from oeqa.utils.commands import get_bb_vars, bitbake + +class Systemd(OESelftestTestCase): + """ + Tests related to the systemd bbclass. + """ + + def getVars(self, recipe): + self.bb_vars = get_bb_vars( + [ + 'BPN', + 'D', + 'INIT_D_DIR', + 'prefix', + 'systemd_system_unitdir', + 'sysconfdir', + ], + recipe, + ) + + def fileExists(self, filename): + self.assertExists(filename.format(**self.bb_vars)) + + def fileNotExists(self, filename): + self.assertNotExists(filename.format(**self.bb_vars)) + + def test_systemd_in_distro(self): + """ + Summary: Verify that no sysvinit files are installed when the + systemd distro feature is enabled, but sysvinit is not. + Expected: Systemd service file exists, but /etc does not. + Product: OE-Core + Author: Peter Kjellerstedt <peter.kjellerstedt@axis.com> + """ + + self.write_config(""" +DISTRO_FEATURES:append = " systemd usrmerge" +DISTRO_FEATURES:remove = "sysvinit" +VIRTUAL-RUNTIME_init_manager = "systemd" +""") + bitbake("systemd-only systemd-and-sysvinit -c install") + + self.getVars("systemd-only") + self.fileExists("{D}{systemd_system_unitdir}/{BPN}.service") + + self.getVars("systemd-and-sysvinit") + self.fileExists("{D}{systemd_system_unitdir}/{BPN}.service") + self.fileNotExists("{D}{sysconfdir}") + + def test_systemd_and_sysvinit_in_distro(self): + """ + Summary: Verify that both systemd and sysvinit files are installed + when both the systemd and sysvinit distro features are + enabled. + Expected: Systemd service file and sysvinit initscript exist. + Product: OE-Core + Author: Peter Kjellerstedt <peter.kjellerstedt@axis.com> + """ + + self.write_config(""" +DISTRO_FEATURES:append = " systemd sysvinit usrmerge" +VIRTUAL-RUNTIME_init_manager = "systemd" +""") + bitbake("systemd-only systemd-and-sysvinit -c install") + + self.getVars("systemd-only") + self.fileExists("{D}{systemd_system_unitdir}/{BPN}.service") + + self.getVars("systemd-and-sysvinit") + self.fileExists("{D}{systemd_system_unitdir}/{BPN}.service") + self.fileExists("{D}{INIT_D_DIR}/{BPN}") + + def test_sysvinit_in_distro(self): + """ + Summary: Verify that no systemd service files are installed when the + sysvinit distro feature is enabled, but systemd is not. + Expected: The systemd service file does not exist, nor does /usr. + The sysvinit initscript exists. + Product: OE-Core + Author: Peter Kjellerstedt <peter.kjellerstedt@axis.com> + """ + + self.write_config(""" +DISTRO_FEATURES:remove = "systemd" +DISTRO_FEATURES:append = " sysvinit usrmerge" +VIRTUAL-RUNTIME_init_manager = "sysvinit" +""") + bitbake("systemd-only systemd-and-sysvinit -c install") + + self.getVars("systemd-only") + self.fileNotExists("{D}{systemd_system_unitdir}/{BPN}.service") + self.fileNotExists("{D}{prefix}") + self.fileNotExists("{D}{sysconfdir}") + self.fileExists("{D}") + + self.getVars("systemd-and-sysvinit") + self.fileNotExists("{D}{systemd_system_unitdir}/{BPN}.service") + self.fileNotExists("{D}{prefix}") + self.fileExists("{D}{INIT_D_DIR}/{BPN}") diff --git a/meta/lib/oeqa/selftest/cases/bblayers.py b/meta/lib/oeqa/selftest/cases/bblayers.py index f131d9856c..695d17377d 100644 --- a/meta/lib/oeqa/selftest/cases/bblayers.py +++ b/meta/lib/oeqa/selftest/cases/bblayers.py @@ -1,4 +1,6 @@ # +# Copyright OpenEmbedded Contributors +# # SPDX-License-Identifier: MIT # @@ -6,12 +8,23 @@ import os import re import oeqa.utils.ftools as ftools -from oeqa.utils.commands import runCmd, get_bb_var, get_bb_vars +from oeqa.utils.commands import runCmd, get_bb_var, get_bb_vars, bitbake from oeqa.selftest.case import OESelftestTestCase class BitbakeLayers(OESelftestTestCase): + @classmethod + def setUpClass(cls): + super(BitbakeLayers, cls).setUpClass() + bitbake("python3-jsonschema-native") + bitbake("-c addto_recipe_sysroot python3-jsonschema-native") + + def test_bitbakelayers_layerindexshowdepends(self): + result = runCmd('bitbake-layers layerindex-show-depends meta-poky') + find_in_contents = re.search("openembedded-core", result.output) + self.assertTrue(find_in_contents, msg = "openembedded-core should have been listed at this step. bitbake-layers layerindex-show-depends meta-poky output: %s" % result.output) + def test_bitbakelayers_showcrossdepends(self): result = runCmd('bitbake-layers show-cross-depends') self.assertIn('aspell', result.output) @@ -41,7 +54,7 @@ class BitbakeLayers(OESelftestTestCase): bb_file = os.path.join(testoutdir, recipe_path, recipe_file) self.assertTrue(os.path.isfile(bb_file), msg = "Cannot find xcursor-transparent-theme_0.1.1.bb in the test_bitbakelayers_flatten local dir.") contents = ftools.read_file(bb_file) - find_in_contents = re.search("##### bbappended from meta-selftest #####\n(.*\n)*include test_recipe.inc", contents) + find_in_contents = re.search(r"##### bbappended from meta-selftest #####\n(.*\n)*include test_recipe.inc", contents) self.assertTrue(find_in_contents, msg = "Flattening layers did not work. bitbake-layers flatten output: %s" % result.output) def test_bitbakelayers_add_remove(self): @@ -72,8 +85,9 @@ class BitbakeLayers(OESelftestTestCase): result = runCmd('bitbake-layers show-recipes -i image') self.assertIn('core-image-minimal', result.output) self.assertNotIn('mtd-utils:', result.output) - result = runCmd('bitbake-layers show-recipes -i cmake,pkgconfig') + result = runCmd('bitbake-layers show-recipes -i meson,pkgconfig') self.assertIn('libproxy:', result.output) + result = runCmd('bitbake-layers show-recipes -i cmake,pkgconfig') self.assertNotIn('mtd-utils:', result.output) # doesn't inherit either self.assertNotIn('wget:', result.output) # doesn't inherit cmake self.assertNotIn('waffle:', result.output) # doesn't inherit pkgconfig @@ -106,6 +120,11 @@ class BitbakeLayers(OESelftestTestCase): self.assertEqual(bb_vars['BBFILE_PRIORITY_%s' % layername], str(priority), 'BBFILE_PRIORITY_%s != %d' % (layername, priority)) + result = runCmd('bitbake-layers save-build-conf {} {}'.format(layerpath, "buildconf-1")) + for f in ('local.conf.sample', 'bblayers.conf.sample', 'conf-summary.txt', 'conf-notes.txt'): + fullpath = os.path.join(layerpath, "conf", "templates", "buildconf-1", f) + self.assertTrue(os.path.exists(fullpath), "Template configuration file {} not found".format(fullpath)) + def get_recipe_basename(self, recipe): recipe_file = "" result = runCmd("bitbake-layers show-recipes -f %s" % recipe) @@ -116,3 +135,108 @@ class BitbakeLayers(OESelftestTestCase): self.assertTrue(os.path.isfile(recipe_file), msg = "Can't find recipe file for %s" % recipe) return os.path.basename(recipe_file) + + def validate_layersjson(self, json): + python = os.path.join(get_bb_var('STAGING_BINDIR', 'python3-jsonschema-native'), 'nativepython3') + jsonvalidator = os.path.join(get_bb_var('STAGING_BINDIR', 'python3-jsonschema-native'), 'jsonschema') + jsonschema = os.path.join(get_bb_var('COREBASE'), 'meta/files/layers.schema.json') + result = runCmd("{} {} -i {} {}".format(python, jsonvalidator, json, jsonschema)) + + def test_validate_examplelayersjson(self): + json = os.path.join(get_bb_var('COREBASE'), "meta/files/layers.example.json") + self.validate_layersjson(json) + + def test_bitbakelayers_setup(self): + result = runCmd('bitbake-layers create-layers-setup {}'.format(self.testlayer_path)) + jsonfile = os.path.join(self.testlayer_path, "setup-layers.json") + self.validate_layersjson(jsonfile) + + # The revision-under-test may not necessarily be available on the remote server, + # so replace it with a revision that has a yocto-4.1 tag. + import json + with open(jsonfile) as f: + data = json.load(f) + for s in data['sources']: + data['sources'][s]['git-remote']['rev'] = '5200799866b92259e855051112520006e1aaaac0' + with open(jsonfile, 'w') as f: + json.dump(data, f) + + testcheckoutdir = os.path.join(self.builddir, 'test-layer-checkout') + result = runCmd('{}/setup-layers --destdir {}'.format(self.testlayer_path, testcheckoutdir)) + layers_json = os.path.join(testcheckoutdir, ".oe-layers.json") + self.assertTrue(os.path.exists(layers_json), "File {} not found in test layer checkout".format(layers_json)) + + # As setup-layers checkout out an old revision of poky, there is no setup-build symlink, + # and we need to run oe-setup-build directly from the current poky tree under test + oe_setup_build = os.path.join(get_bb_var('COREBASE'), 'scripts/oe-setup-build') + oe_setup_build_l = os.path.join(testcheckoutdir, 'setup-build') + os.symlink(oe_setup_build,oe_setup_build_l) + + cmd = '{} --layerlist {} list -v'.format(oe_setup_build_l, layers_json) + result = runCmd(cmd) + cond = "conf/templates/default" in result.output + self.assertTrue(cond, "Incorrect output from {}: {}".format(cmd, result.output)) + + # rather than hardcode the build setup cmdline here, let's actually run what the tool suggests to the user + conf = None + if 'poky-default' in result.output: + conf = 'poky-default' + elif 'meta-default' in result.output: + conf = 'meta-default' + self.assertIsNotNone(conf, "Could not find the configuration to set up a build in the output: {}".format(result.output)) + + cmd = '{} --layerlist {} setup -c {} --no-shell'.format(oe_setup_build_l, layers_json, conf) + result = runCmd(cmd) + + def test_bitbakelayers_updatelayer(self): + result = runCmd('bitbake-layers create-layers-setup {}'.format(self.testlayer_path)) + jsonfile = os.path.join(self.testlayer_path, "setup-layers.json") + self.validate_layersjson(jsonfile) + + import json + with open(jsonfile) as f: + data = json.load(f) + repos = [] + for s in data['sources']: + repos.append(s) + + self.assertTrue(len(repos) > 1, "Not enough repositories available") + self.validate_layersjson(jsonfile) + + test_ref_1 = 'ref_1' + test_ref_2 = 'ref_2' + + # Create a new layers setup using custom references + result = runCmd('bitbake-layers create-layers-setup --use-custom-reference {first_repo}:{test_ref} --use-custom-reference {second_repo}:{test_ref} {path}' + .format(first_repo=repos[0], second_repo=repos[1], test_ref=test_ref_1, path=self.testlayer_path)) + self.validate_layersjson(jsonfile) + + with open(jsonfile) as f: + data = json.load(f) + first_rev_1 = data['sources'][repos[0]]['git-remote']['rev'] + first_desc_1 = data['sources'][repos[0]]['git-remote']['describe'] + second_rev_1 = data['sources'][repos[1]]['git-remote']['rev'] + second_desc_1 = data['sources'][repos[1]]['git-remote']['describe'] + + self.assertEqual(first_rev_1, test_ref_1, "Revision not set correctly: '{}'".format(first_rev_1)) + self.assertEqual(first_desc_1, '', "Describe not cleared: '{}'".format(first_desc_1)) + self.assertEqual(second_rev_1, test_ref_1, "Revision not set correctly: '{}'".format(second_rev_1)) + self.assertEqual(second_desc_1, '', "Describe not cleared: '{}'".format(second_desc_1)) + + # Update one of the repositories in the layers setup using a different custom reference + # This should only update the selected repository, everything else should remain as is + result = runCmd('bitbake-layers create-layers-setup --update --use-custom-reference {first_repo}:{test_ref} {path}' + .format(first_repo=repos[0], test_ref=test_ref_2, path=self.testlayer_path)) + self.validate_layersjson(jsonfile) + + with open(jsonfile) as f: + data = json.load(f) + first_rev_2 = data['sources'][repos[0]]['git-remote']['rev'] + first_desc_2 = data['sources'][repos[0]]['git-remote']['describe'] + second_rev_2 = data['sources'][repos[1]]['git-remote']['rev'] + second_desc_2 = data['sources'][repos[1]]['git-remote']['describe'] + + self.assertEqual(first_rev_2, test_ref_2, "Revision not set correctly: '{}'".format(first_rev_2)) + self.assertEqual(first_desc_2, '', "Describe not cleared: '{}'".format(first_desc_2)) + self.assertEqual(second_rev_2, second_rev_1, "Revision should not be updated: '{}'".format(second_rev_2)) + self.assertEqual(second_desc_2, second_desc_1, "Describe should not be updated: '{}'".format(second_desc_2)) diff --git a/meta/lib/oeqa/selftest/cases/bblock.py b/meta/lib/oeqa/selftest/cases/bblock.py new file mode 100644 index 0000000000..2b62d2a0aa --- /dev/null +++ b/meta/lib/oeqa/selftest/cases/bblock.py @@ -0,0 +1,203 @@ +# +# Copyright (c) 2023 BayLibre, SAS +# Author: Julien Stepahn <jstephan@baylibre.com> +# +# SPDX-License-Identifier: GPL-2.0-only +# + +import os +import re +import bb.tinfoil + +import oeqa.utils.ftools as ftools +from oeqa.utils.commands import runCmd, get_bb_var, get_bb_vars, bitbake + +from oeqa.selftest.case import OESelftestTestCase + + +class BBLock(OESelftestTestCase): + @classmethod + def setUpClass(cls): + super(BBLock, cls).setUpClass() + cls.lockfile = cls.builddir + "/conf/bblock.conf" + + def unlock_recipes(self, recipes=None, tasks=None): + cmd = "bblock -r " + if recipes: + cmd += " ".join(recipes) + if tasks: + cmd += " -t " + ",".join(tasks) + result = runCmd(cmd) + + if recipes: + # ensure all signatures are removed from lockfile + contents = ftools.read_file(self.lockfile) + for recipe in recipes: + for task in tasks: + find_in_contents = re.search( + 'SIGGEN_LOCKEDSIGS_.+\s\+=\s"%s:%s:.*"' % (recipe, task), + contents, + ) + self.assertFalse( + find_in_contents, + msg="%s:%s should not be present into bblock.conf anymore" + % (recipe, task), + ) + self.assertExists(self.lockfile) + else: + self.assertNotExists(self.lockfile) + + def lock_recipes(self, recipes, tasks=None): + cmd = "bblock " + " ".join(recipes) + if tasks: + cmd += " -t " + ",".join(tasks) + + result = runCmd(cmd) + + self.assertExists(self.lockfile) + + # ensure all signatures are added to lockfile + contents = ftools.read_file(self.lockfile) + for recipe in recipes: + if tasks: + for task in tasks: + find_in_contents = re.search( + 'SIGGEN_LOCKEDSIGS_.+\s\+=\s"%s:%s:.*"' % (recipe, task), + contents, + ) + self.assertTrue( + find_in_contents, + msg="%s:%s was not added into bblock.conf. bblock output: %s" + % (recipe, task, result.output), + ) + + def modify_tasks(self, recipes, tasks): + task_append = "" + for recipe in recipes: + bb_vars = get_bb_vars(["PV"], recipe) + recipe_pv = bb_vars["PV"] + recipe_append_file = recipe + "_" + recipe_pv + ".bbappend" + + os.mkdir(os.path.join(self.testlayer_path, "recipes-test", recipe)) + recipe_append_path = os.path.join( + self.testlayer_path, "recipes-test", recipe, recipe_append_file + ) + + for task in tasks: + task_append += "%s:append() {\n#modify task hash \n}\n" % task + ftools.write_file(recipe_append_path, task_append) + self.add_command_to_tearDown( + "rm -rf %s" % os.path.join(self.testlayer_path, "recipes-test", recipe) + ) + + def test_lock_single_recipe_single_task(self): + recipes = ["quilt"] + tasks = ["do_compile"] + self._run_test(recipes, tasks) + + def test_lock_single_recipe_multiple_tasks(self): + recipes = ["quilt"] + tasks = ["do_compile", "do_install"] + self._run_test(recipes, tasks) + + def test_lock_single_recipe_all_tasks(self): + recipes = ["quilt"] + self._run_test(recipes, None) + + def test_lock_multiple_recipe_single_task(self): + recipes = ["quilt", "bc"] + tasks = ["do_compile"] + self._run_test(recipes, tasks) + + def test_lock_architecture_specific(self): + # unlock all recipes and ensure no bblock.conf file exist + self.unlock_recipes() + + recipes = ["quilt"] + tasks = ["do_compile"] + + # lock quilt's do_compile task for another machine + if self.td["MACHINE"] == "qemux86-64": + machine = "qemuarm" + else: + machine = "qemux86-64" + + self.write_config('MACHINE = "%s"\n' % machine) + + self.lock_recipes(recipes, tasks) + + self.write_config('MACHINE = "%s"\n' % self.td["MACHINE"]) + # modify quilt's do_compile task + self.modify_tasks(recipes, tasks) + + # build quilt using the default machine + # No Note/Warning should be emitted since sig is locked for another machine + # (quilt package is architecture dependant) + info_message = "NOTE: The following recipes have locked tasks: " + recipes[0] + warn_message = "The %s:%s sig is computed to be" % (recipes[0], tasks[0]) + result = bitbake(recipes[0] + " -n") + self.assertNotIn(info_message, result.output) + self.assertNotIn(warn_message, result.output) + + # unlock all recipes + self.unlock_recipes() + + def _run_test(self, recipes, tasks=None): + # unlock all recipes and ensure no bblock.conf file exist + self.unlock_recipes() + + self.write_config('BB_SIGNATURE_HANDLER = "OEBasicHash"') + + # lock tasks for recipes + result = self.lock_recipes(recipes, tasks) + + if not tasks: + tasks = [] + result = bitbake("-c listtasks " + recipes[0]) + with bb.tinfoil.Tinfoil() as tinfoil: + tinfoil.prepare(config_only=False, quiet=2) + d = tinfoil.parse_recipe(recipes[0]) + + for line in result.output.splitlines(): + if line.startswith("do_"): + task = line.split()[0] + if "setscene" in task: + continue + if d.getVarFlag(task, "nostamp"): + continue + tasks.append(task) + + # build recipes. At this stage we should have a Note about recipes + # having locked task's sig, but no warning since sig still match + info_message = "NOTE: The following recipes have locked tasks: " + " ".join( + recipes + ) + for recipe in recipes: + result = bitbake(recipe + " -n") + self.assertIn(info_message, result.output) + for task in tasks: + warn_message = "The %s:%s sig is computed to be" % (recipe, task) + self.assertNotIn(warn_message, result.output) + + # modify all tasks that are locked to trigger a sig change then build the recipes + # at this stage we should have a Note as before, but also a Warning for all + # locked tasks indicating the sig mismatch + self.modify_tasks(recipes, tasks) + for recipe in recipes: + result = bitbake(recipe + " -n") + self.assertIn(info_message, result.output) + for task in tasks: + warn_message = "The %s:%s sig is computed to be" % (recipe, task) + self.assertIn(warn_message, result.output) + + # unlock all tasks and rebuild, no more Note/Warning should remain + self.unlock_recipes(recipes, tasks) + for recipe in recipes: + result = bitbake(recipe + " -n") + self.assertNotIn(info_message, result.output) + for task in tasks: + warn_message = "The %s:%s sig is computed to be" % (recipe, task) + self.assertNotIn(warn_message, result.output) + + # unlock all recipes + self.unlock_recipes() diff --git a/meta/lib/oeqa/selftest/cases/bblogging.py b/meta/lib/oeqa/selftest/cases/bblogging.py new file mode 100644 index 0000000000..040c6db089 --- /dev/null +++ b/meta/lib/oeqa/selftest/cases/bblogging.py @@ -0,0 +1,182 @@ +# +# Copyright OpenEmbedded Contributors +# +# SPDX-License-Identifier: MIT +# + + +from oeqa.selftest.case import OESelftestTestCase +from oeqa.utils.commands import bitbake + +class BitBakeLogging(OESelftestTestCase): + + def assertCount(self, item, entry, count): + self.assertEqual(item.count(entry), count, msg="Output:\n'''\n%s\n'''\ndoesn't contain %d copies of:\n'''\n%s\n'''\n" % (item, count, entry)) + + def test_shell_loggingA(self): + # no logs, no verbose + self.write_config('BBINCLUDELOGS = ""') + result = bitbake("logging-test -c shelltest -f", ignore_status = True) + self.assertIn("ERROR: Logfile of failure stored in:", result.output) + self.assertNotIn("This is shell stdout", result.output) + self.assertNotIn("This is shell stderr", result.output) + + def test_shell_loggingB(self): + # logs, no verbose + self.write_config('BBINCLUDELOGS = "yes"') + result = bitbake("logging-test -c shelltest -f", ignore_status = True) + self.assertIn("ERROR: Logfile of failure stored in:", result.output) + self.assertCount(result.output, "This is shell stdout", 1) + self.assertCount(result.output, "This is shell stderr", 1) + + def test_shell_loggingC(self): + # no logs, verbose + self.write_config('BBINCLUDELOGS = ""') + result = bitbake("logging-test -c shelltest -f -v", ignore_status = True) + self.assertIn("ERROR: Logfile of failure stored in:", result.output) + # two copies due to set +x + self.assertCount(result.output, "This is shell stdout", 2) + self.assertCount(result.output, "This is shell stderr", 2) + + def test_shell_loggingD(self): + # logs, verbose + self.write_config('BBINCLUDELOGS = "yes"') + result = bitbake("logging-test -c shelltest -f -v", ignore_status = True) + self.assertIn("ERROR: Logfile of failure stored in:", result.output) + # two copies due to set +x + self.assertCount(result.output, "This is shell stdout", 2) + self.assertCount(result.output, "This is shell stderr", 2) + + def test_python_exec_func_shell_loggingA(self): + # no logs, no verbose + self.write_config('BBINCLUDELOGS = ""') + result = bitbake("logging-test -c pythontest_exec_func_shell -f", + ignore_status = True) + self.assertIn("ERROR: Logfile of failure stored in:", result.output) + self.assertNotIn("This is shell stdout", result.output) + self.assertNotIn("This is shell stderr", result.output) + + def test_python_exec_func_shell_loggingB(self): + # logs, no verbose + self.write_config('BBINCLUDELOGS = "yes"') + result = bitbake("logging-test -c pythontest_exec_func_shell -f", + ignore_status = True) + self.assertIn("ERROR: Logfile of failure stored in:", result.output) + self.assertCount(result.output, "This is shell stdout", 1) + self.assertCount(result.output, "This is shell stderr", 1) + + def test_python_exec_func_shell_loggingC(self): + # no logs, verbose + self.write_config('BBINCLUDELOGS = ""') + result = bitbake("logging-test -c pythontest_exec_func_shell -f -v", + ignore_status = True) + self.assertIn("ERROR: Logfile of failure stored in:", result.output) + # two copies due to set +x + self.assertCount(result.output, "This is shell stdout", 2) + self.assertCount(result.output, "This is shell stderr", 2) + + def test_python_exec_func_shell_loggingD(self): + # logs, verbose + self.write_config('BBINCLUDELOGS = "yes"') + result = bitbake("logging-test -c pythontest_exec_func_shell -f -v", + ignore_status = True) + self.assertIn("ERROR: Logfile of failure stored in:", result.output) + # two copies due to set +x + self.assertCount(result.output, "This is shell stdout", 2) + self.assertCount(result.output, "This is shell stderr", 2) + + def test_python_exit_loggingA(self): + # no logs, no verbose + self.write_config('BBINCLUDELOGS = ""') + result = bitbake("logging-test -c pythontest_exit -f", ignore_status = True) + self.assertIn("ERROR: Logfile of failure stored in:", result.output) + self.assertNotIn("This is python stdout", result.output) + + def test_python_exit_loggingB(self): + # logs, no verbose + self.write_config('BBINCLUDELOGS = "yes"') + result = bitbake("logging-test -c pythontest_exit -f", ignore_status = True) + self.assertIn("ERROR: Logfile of failure stored in:", result.output) + # A sys.exit() should include the output + self.assertCount(result.output, "This is python stdout", 1) + + def test_python_exit_loggingC(self): + # no logs, verbose + self.write_config('BBINCLUDELOGS = ""') + result = bitbake("logging-test -c pythontest_exit -f -v", ignore_status = True) + self.assertIn("ERROR: Logfile of failure stored in:", result.output) + self.assertCount(result.output, "This is python stdout", 1) + + def test_python_exit_loggingD(self): + # logs, verbose + self.write_config('BBINCLUDELOGS = "yes"') + result = bitbake("logging-test -c pythontest_exit -f -v", ignore_status = True) + self.assertIn("ERROR: Logfile of failure stored in:", result.output) + self.assertCount(result.output, "This is python stdout", 1) + + def test_python_exec_func_python_loggingA(self): + # no logs, no verbose + self.write_config('BBINCLUDELOGS = ""') + result = bitbake("logging-test -c pythontest_exec_func_python -f", + ignore_status = True) + self.assertIn("ERROR: Logfile of failure stored in:", result.output) + self.assertNotIn("This is python stdout", result.output) + + def test_python_exec_func_python_loggingB(self): + # logs, no verbose + self.write_config('BBINCLUDELOGS = "yes"') + result = bitbake("logging-test -c pythontest_exec_func_python -f", + ignore_status = True) + self.assertIn("ERROR: Logfile of failure stored in:", result.output) + # A sys.exit() should include the output + self.assertCount(result.output, "This is python stdout", 1) + + def test_python_exec_func_python_loggingC(self): + # no logs, verbose + self.write_config('BBINCLUDELOGS = ""') + result = bitbake("logging-test -c pythontest_exec_func_python -f -v", + ignore_status = True) + self.assertIn("ERROR: Logfile of failure stored in:", result.output) + self.assertCount(result.output, "This is python stdout", 1) + + def test_python_exec_func_python_loggingD(self): + # logs, verbose + self.write_config('BBINCLUDELOGS = "yes"') + result = bitbake("logging-test -c pythontest_exec_func_python -f -v", + ignore_status = True) + self.assertIn("ERROR: Logfile of failure stored in:", result.output) + self.assertCount(result.output, "This is python stdout", 1) + + def test_python_fatal_loggingA(self): + # no logs, no verbose + self.write_config('BBINCLUDELOGS = ""') + result = bitbake("logging-test -c pythontest_fatal -f", ignore_status = True) + self.assertIn("ERROR: Logfile of failure stored in:", result.output) + self.assertNotIn("This is python fatal test stdout", result.output) + self.assertCount(result.output, "This is a fatal error", 1) + + def test_python_fatal_loggingB(self): + # logs, no verbose + self.write_config('BBINCLUDELOGS = "yes"') + result = bitbake("logging-test -c pythontest_fatal -f", ignore_status = True) + self.assertIn("ERROR: Logfile of failure stored in:", result.output) + # A bb.fatal() should not include the output + self.assertNotIn("This is python fatal test stdout", result.output) + self.assertCount(result.output, "This is a fatal error", 1) + + def test_python_fatal_loggingC(self): + # no logs, verbose + self.write_config('BBINCLUDELOGS = ""') + result = bitbake("logging-test -c pythontest_fatal -f -v", ignore_status = True) + self.assertIn("ERROR: Logfile of failure stored in:", result.output) + self.assertCount(result.output, "This is python fatal test stdout", 1) + self.assertCount(result.output, "This is a fatal error", 1) + + def test_python_fatal_loggingD(self): + # logs, verbose + self.write_config('BBINCLUDELOGS = "yes"') + result = bitbake("logging-test -c pythontest_fatal -f -v", ignore_status = True) + self.assertIn("ERROR: Logfile of failure stored in:", result.output) + self.assertCount(result.output, "This is python fatal test stdout", 1) + self.assertCount(result.output, "This is a fatal error", 1) + diff --git a/meta/lib/oeqa/selftest/cases/bbtests.py b/meta/lib/oeqa/selftest/cases/bbtests.py index dc423ec439..98e9f81661 100644 --- a/meta/lib/oeqa/selftest/cases/bbtests.py +++ b/meta/lib/oeqa/selftest/cases/bbtests.py @@ -1,4 +1,6 @@ # +# Copyright OpenEmbedded Contributors +# # SPDX-License-Identifier: MIT # @@ -39,7 +41,7 @@ class BitbakeTests(OESelftestTestCase): def test_event_handler(self): self.write_config("INHERIT += \"test_events\"") - result = bitbake('m4-native') + result = bitbake('selftest-hello-native') find_build_started = re.search(r"NOTE: Test for bb\.event\.BuildStarted(\n.*)*NOTE: Executing.*Tasks", result.output) find_build_completed = re.search(r"Tasks Summary:.*(\n.*)*NOTE: Test for bb\.event\.BuildCompleted", result.output) self.assertTrue(find_build_started, msg = "Match failed in:\n%s" % result.output) @@ -47,11 +49,11 @@ class BitbakeTests(OESelftestTestCase): self.assertNotIn('Test for bb.event.InvalidEvent', result.output) def test_local_sstate(self): - bitbake('m4-native') - bitbake('m4-native -cclean') - result = bitbake('m4-native') - find_setscene = re.search("m4-native.*do_.*_setscene", result.output) - self.assertTrue(find_setscene, msg = "No \"m4-native.*do_.*_setscene\" message found during bitbake m4-native. bitbake output: %s" % result.output ) + bitbake('selftest-hello-native') + bitbake('selftest-hello-native -cclean') + result = bitbake('selftest-hello-native') + find_setscene = re.search("selftest-hello-native.*do_.*_setscene", result.output) + self.assertTrue(find_setscene, msg = "No \"selftest-hello-native.*do_.*_setscene\" message found during bitbake selftest-hello-native. bitbake output: %s" % result.output ) def test_bitbake_invalid_recipe(self): result = bitbake('-b asdf', ignore_status=True) @@ -63,15 +65,15 @@ class BitbakeTests(OESelftestTestCase): def test_warnings_errors(self): result = bitbake('-b asdf', ignore_status=True) - find_warnings = re.search("Summary: There w.{2,3}? [1-9][0-9]* WARNING messages* shown", result.output) - find_errors = re.search("Summary: There w.{2,3}? [1-9][0-9]* ERROR messages* shown", result.output) + find_warnings = re.search("Summary: There w.{2,3}? [1-9][0-9]* WARNING messages*", result.output) + find_errors = re.search("Summary: There w.{2,3}? [1-9][0-9]* ERROR messages*", result.output) self.assertTrue(find_warnings, msg="Did not find the mumber of warnings at the end of the build:\n" + result.output) self.assertTrue(find_errors, msg="Did not find the mumber of errors at the end of the build:\n" + result.output) def test_invalid_patch(self): # This patch should fail to apply. - self.write_recipeinc('man-db', 'FILESEXTRAPATHS_prepend := "${THISDIR}/files:"\nSRC_URI += "file://0001-Test-patch-here.patch"') - self.write_config("INHERIT_remove = \"report-error\"") + self.write_recipeinc('man-db', 'FILESEXTRAPATHS:prepend := "${THISDIR}/files:"\nSRC_URI += "file://0001-Test-patch-here.patch"') + self.write_config("INHERIT:remove = \"report-error\"") result = bitbake('man-db -c patch', ignore_status=True) self.delete_recipeinc('man-db') bitbake('-cclean man-db') @@ -83,12 +85,15 @@ class BitbakeTests(OESelftestTestCase): def test_force_task_1(self): # test 1 from bug 5875 + import uuid test_recipe = 'zlib' - test_data = "Microsoft Made No Profit From Anyone's Zunes Yo" + # Need to use uuid otherwise hash equivlance would change the workflow + test_data = "Microsoft Made No Profit From Anyone's Zunes Yo %s" % uuid.uuid1() bb_vars = get_bb_vars(['D', 'PKGDEST', 'mandir'], test_recipe) image_dir = bb_vars['D'] pkgsplit_dir = bb_vars['PKGDEST'] man_dir = bb_vars['mandir'] + self.write_config("PACKAGE_CLASSES = \"package_rpm\"") bitbake('-c clean %s' % test_recipe) bitbake('-c package -f %s' % test_recipe) @@ -138,19 +143,14 @@ class BitbakeTests(OESelftestTestCase): self.write_recipeinc('man-db', data) self.write_config("""DL_DIR = \"${TOPDIR}/download-selftest\" SSTATE_DIR = \"${TOPDIR}/download-selftest\" -INHERIT_remove = \"report-error\" +INHERIT:remove = \"report-error\" """) self.track_for_cleanup(os.path.join(self.builddir, "download-selftest")) - bitbake('-ccleanall man-db') result = bitbake('-c fetch man-db', ignore_status=True) - bitbake('-ccleanall man-db') self.delete_recipeinc('man-db') self.assertEqual(result.status, 1, msg="Command succeded when it should have failed. bitbake output: %s" % result.output) - self.assertIn('Fetcher failure: Unable to find file file://invalid anywhere. The paths that were searched were:', result.output) - line = self.getline(result, 'Fetcher failure for URL: \'file://invalid\'. Unable to fetch URL from any source.') - self.assertTrue(line and line.startswith("ERROR:"), msg = "\"invalid\" file \ -doesn't exist, yet fetcher didn't report any error. bitbake output: %s" % result.output) + self.assertIn('Unable to get checksum for man-db SRC_URI entry invalid: file could not be found', result.output) def test_rename_downloaded_file(self): # TODO unique dldir instead of using cleanall @@ -160,7 +160,7 @@ SSTATE_DIR = \"${TOPDIR}/download-selftest\" """) self.track_for_cleanup(os.path.join(self.builddir, "download-selftest")) - data = 'SRC_URI = "${GNU_MIRROR}/aspell/aspell-${PV}.tar.gz;downloadfilename=test-aspell.tar.gz"' + data = 'SRC_URI = "https://downloads.yoctoproject.org/mirror/sources/aspell-${PV}.tar.gz;downloadfilename=test-aspell.tar.gz"' self.write_recipeinc('aspell', data) result = bitbake('-f -c fetch aspell', ignore_status=True) self.delete_recipeinc('aspell') @@ -175,7 +175,7 @@ SSTATE_DIR = \"${TOPDIR}/download-selftest\" self.assertIn('localconf', result.output) def test_dry_run(self): - result = runCmd('bitbake -n m4-native') + result = runCmd('bitbake -n selftest-hello-native') self.assertEqual(0, result.status, "bitbake dry run didn't run as expected. %s" % result.output) def test_just_parse(self): @@ -188,6 +188,10 @@ SSTATE_DIR = \"${TOPDIR}/download-selftest\" self.assertTrue(find, "No version returned for searched recipe. bitbake output: %s" % result.output) def test_prefile(self): + # Test when the prefile does not exist + result = runCmd('bitbake -r conf/prefile.conf', ignore_status=True) + self.assertEqual(1, result.status, "bitbake didn't error and should have when a specified prefile didn't exist: %s" % result.output) + # Test when the prefile exists preconf = os.path.join(self.builddir, 'conf/prefile.conf') self.track_for_cleanup(preconf) ftools.write_file(preconf ,"TEST_PREFILE=\"prefile\"") @@ -198,6 +202,10 @@ SSTATE_DIR = \"${TOPDIR}/download-selftest\" self.assertIn('localconf', result.output) def test_postfile(self): + # Test when the postfile does not exist + result = runCmd('bitbake -R conf/postfile.conf', ignore_status=True) + self.assertEqual(1, result.status, "bitbake didn't error and should have when a specified postfile didn't exist: %s" % result.output) + # Test when the postfile exists postconf = os.path.join(self.builddir, 'conf/postfile.conf') self.track_for_cleanup(postconf) ftools.write_file(postconf , "TEST_POSTFILE=\"postfile\"") @@ -212,7 +220,7 @@ SSTATE_DIR = \"${TOPDIR}/download-selftest\" def test_continue(self): self.write_config("""DL_DIR = \"${TOPDIR}/download-selftest\" SSTATE_DIR = \"${TOPDIR}/download-selftest\" -INHERIT_remove = \"report-error\" +INHERIT:remove = \"report-error\" """) self.track_for_cleanup(os.path.join(self.builddir, "download-selftest")) self.write_recipeinc('man-db',"\ndo_fail_task () {\nexit 1 \n}\n\naddtask do_fail_task before do_fetch\n" ) @@ -224,16 +232,21 @@ INHERIT_remove = \"report-error\" self.assertLess(errorpos,continuepos, msg = "bitbake didn't pass do_fail_task. bitbake output: %s" % result.output) def test_non_gplv3(self): - self.write_config('INCOMPATIBLE_LICENSE = "GPLv3"') + self.write_config('''INCOMPATIBLE_LICENSE = "GPL-3.0-or-later" +require conf/distro/include/no-gplv3.inc +''') result = bitbake('selftest-ed', ignore_status=True) self.assertEqual(result.status, 0, "Bitbake failed, exit code %s, output %s" % (result.status, result.output)) lic_dir = get_bb_var('LICENSE_DIRECTORY') - self.assertFalse(os.path.isfile(os.path.join(lic_dir, 'selftest-ed/generic_GPLv3'))) - self.assertTrue(os.path.isfile(os.path.join(lic_dir, 'selftest-ed/generic_GPLv2'))) + arch = get_bb_var('SSTATE_PKGARCH') + filename = os.path.join(lic_dir, arch, 'selftest-ed', 'generic_GPL-3.0-or-later') + self.assertFalse(os.path.isfile(filename), msg="License file %s exists and shouldn't" % filename) + filename = os.path.join(lic_dir, arch, 'selftest-ed', 'generic_GPL-2.0-or-later') + self.assertTrue(os.path.isfile(filename), msg="License file %s doesn't exist" % filename) def test_setscene_only(self): """ Bitbake option to restore from sstate only within a build (i.e. execute no real tasks, only setscene)""" - test_recipe = 'ed' + test_recipe = 'selftest-hello-native' bitbake(test_recipe) bitbake('-c clean %s' % test_recipe) @@ -246,7 +259,7 @@ INHERIT_remove = \"report-error\" 'Executed tasks were: %s' % (task, str(tasks))) def test_skip_setscene(self): - test_recipe = 'ed' + test_recipe = 'selftest-hello-native' bitbake(test_recipe) bitbake('-c clean %s' % test_recipe) @@ -297,3 +310,68 @@ INHERIT_remove = \"report-error\" test_recipe_summary_after = get_bb_var('SUMMARY', test_recipe) self.assertEqual(expected_recipe_summary, test_recipe_summary_after) + + def test_git_patchtool(self): + """ PATCHTOOL=git should work with non-git sources like tarballs + test recipe for the test must NOT containt git:// repository in SRC_URI + """ + test_recipe = "man-db" + self.write_recipeinc(test_recipe, 'PATCHTOOL=\"git\"') + src = get_bb_var("SRC_URI",test_recipe) + gitscm = re.search("git://", src) + self.assertFalse(gitscm, "test_git_patchtool pre-condition failed: {} test recipe contains git repo!".format(test_recipe)) + result = bitbake('{} -c patch'.format(test_recipe), ignore_status=False) + fatal = re.search("fatal: not a git repository (or any of the parent directories)", result.output) + self.assertFalse(fatal, "Failed to patch using PATCHTOOL=\"git\"") + self.delete_recipeinc(test_recipe) + bitbake('-cclean {}'.format(test_recipe)) + + def test_git_patchtool2(self): + """ Test if PATCHTOOL=git works with git repo and doesn't reinitialize it + """ + test_recipe = "gitrepotest" + src = get_bb_var("SRC_URI",test_recipe) + gitscm = re.search("git://", src) + self.assertTrue(gitscm, "test_git_patchtool pre-condition failed: {} test recipe doesn't contains git repo!".format(test_recipe)) + result = bitbake('{} -c patch'.format(test_recipe), ignore_status=False) + srcdir = get_bb_var('S', test_recipe) + result = runCmd("git log", cwd = srcdir) + self.assertFalse("bitbake_patching_started" in result.output, msg = "Repository has been reinitialized. {}".format(srcdir)) + self.delete_recipeinc(test_recipe) + bitbake('-cclean {}'.format(test_recipe)) + + + def test_git_unpack_nonetwork(self): + """ + Test that a recipe with a floating tag that needs to be resolved upstream doesn't + access the network in a patch task run in a separate builld invocation + """ + + # Enable the recipe to float using a distro override + self.write_config("DISTROOVERRIDES .= \":gitunpack-enable-recipe\"") + + bitbake('gitunpackoffline -c fetch') + bitbake('gitunpackoffline -c patch') + + def test_git_unpack_nonetwork_fail(self): + """ + Test that a recipe with a floating tag which doesn't call get_srcrev() in the fetcher + raises an error when the fetcher is called. + """ + + # Enable the recipe to float using a distro override + self.write_config("DISTROOVERRIDES .= \":gitunpack-enable-recipe\"") + + result = bitbake('gitunpackoffline-fail -c fetch', ignore_status=True) + self.assertTrue(re.search("Recipe uses a floating tag/branch .* for repo .* without a fixed SRCREV yet doesn't call bb.fetch2.get_srcrev()", result.output), msg = "Recipe without PV set to SRCPV should have failed: %s" % result.output) + + def test_unexpanded_variable_in_path(self): + """ + Test that bitbake fails if directory contains unexpanded bitbake variable in the name + """ + recipe_name = "gitunpackoffline" + self.write_config('PV:pn-gitunpackoffline:append = "+${UNDEFVAL}"') + result = bitbake('{}'.format(recipe_name), ignore_status=True) + self.assertGreater(result.status, 0, "Build should have failed if ${ is in the path") + self.assertTrue(re.search("ERROR: Directory name /.* contains unexpanded bitbake variable. This may cause build failures and WORKDIR polution", + result.output), msg = "mkdirhier with unexpanded variable should have failed: %s" % result.output) diff --git a/meta/lib/oeqa/selftest/cases/binutils.py b/meta/lib/oeqa/selftest/cases/binutils.py index 821f52f5a8..5ff263d342 100644 --- a/meta/lib/oeqa/selftest/cases/binutils.py +++ b/meta/lib/oeqa/selftest/cases/binutils.py @@ -1,12 +1,14 @@ +# +# Copyright OpenEmbedded Contributors +# # SPDX-License-Identifier: MIT +# import os -import sys -import re -import logging +import time from oeqa.core.decorator import OETestTag from oeqa.core.case import OEPTestResultTestCase from oeqa.selftest.case import OESelftestTestCase -from oeqa.utils.commands import bitbake, get_bb_var, get_bb_vars +from oeqa.utils.commands import bitbake, get_bb_vars def parse_values(content): for i in content: @@ -31,19 +33,23 @@ class BinutilsCrossSelfTest(OESelftestTestCase, OEPTestResultTestCase): features.append('CHECK_TARGETS = "{0}"'.format(suite)) self.write_config("\n".join(features)) - recipe = "binutils-cross-testsuite" + recipe = "binutils-testsuite" bb_vars = get_bb_vars(["B", "TARGET_SYS", "T"], recipe) builddir, target_sys, tdir = bb_vars["B"], bb_vars["TARGET_SYS"], bb_vars["T"] + start_time = time.time() + bitbake("{0} -c check".format(recipe)) + end_time = time.time() + sumspath = os.path.join(builddir, suite, "{0}.sum".format(suite)) if not os.path.exists(sumspath): sumspath = os.path.join(builddir, suite, "testsuite", "{0}.sum".format(suite)) logpath = os.path.splitext(sumspath)[0] + ".log" ptestsuite = "binutils-{}".format(suite) if suite != "binutils" else suite - self.ptest_section(ptestsuite, logfile = logpath) + self.ptest_section(ptestsuite, duration = int(end_time - start_time), logfile = logpath) with open(sumspath, "r") as f: for test, result in parse_values(f): self.ptest_result(ptestsuite, test, result) diff --git a/meta/lib/oeqa/selftest/cases/buildhistory.py b/meta/lib/oeqa/selftest/cases/buildhistory.py index d865da6252..2d55994916 100644 --- a/meta/lib/oeqa/selftest/cases/buildhistory.py +++ b/meta/lib/oeqa/selftest/cases/buildhistory.py @@ -1,4 +1,6 @@ # +# Copyright OpenEmbedded Contributors +# # SPDX-License-Identifier: MIT # diff --git a/meta/lib/oeqa/selftest/cases/buildoptions.py b/meta/lib/oeqa/selftest/cases/buildoptions.py index e91f0bd18f..423c31e189 100644 --- a/meta/lib/oeqa/selftest/cases/buildoptions.py +++ b/meta/lib/oeqa/selftest/cases/buildoptions.py @@ -1,4 +1,6 @@ # +# Copyright OpenEmbedded Contributors +# # SPDX-License-Identifier: MIT # @@ -9,8 +11,10 @@ import shutil import tempfile from oeqa.selftest.case import OESelftestTestCase from oeqa.selftest.cases.buildhistory import BuildhistoryBase -from oeqa.utils.commands import runCmd, bitbake, get_bb_var, get_bb_vars +from oeqa.core.decorator.data import skipIfMachine +from oeqa.utils.commands import bitbake, get_bb_var, get_bb_vars import oeqa.utils.ftools as ftools +from oeqa.core.decorator import OETestTag class ImageOptionsTests(OESelftestTestCase): @@ -38,34 +42,35 @@ class ImageOptionsTests(OESelftestTestCase): p = bb_vars['SYSROOT_DESTDIR'] + bb_vars['bindir'] + "/" + "ccache" self.assertTrue(os.path.isfile(p), msg = "No ccache found (%s)" % p) self.write_config('INHERIT += "ccache"') - self.add_command_to_tearDown('bitbake -c clean m4-native') - bitbake("m4-native -c clean") - bitbake("m4-native -f -c compile") - log_compile = os.path.join(get_bb_var("WORKDIR","m4-native"), "temp/log.do_compile") + recipe = "libgcc-initial" + self.add_command_to_tearDown('bitbake -c clean %s' % recipe) + bitbake("%s -c clean" % recipe) + bitbake("%s -f -c compile" % recipe) + log_compile = os.path.join(get_bb_var("WORKDIR", recipe), "temp/log.do_compile") with open(log_compile, "r") as f: loglines = "".join(f.readlines()) - self.assertIn("ccache", loglines, msg="No match for ccache in m4-native log.do_compile. For further details: %s" % log_compile) + self.assertIn("ccache", loglines, msg="No match for ccache in %s log.do_compile. For further details: %s" % (recipe , log_compile)) def test_read_only_image(self): distro_features = get_bb_var('DISTRO_FEATURES') if not ('x11' in distro_features and 'opengl' in distro_features): - self.skipTest('core-image-sato requires x11 and opengl in distro features') + self.skipTest('core-image-sato/weston requires x11 and opengl in distro features') self.write_config('IMAGE_FEATURES += "read-only-rootfs"') - bitbake("core-image-sato") + bitbake("core-image-sato core-image-weston") # do_image will fail if there are any pending postinsts class DiskMonTest(OESelftestTestCase): def test_stoptask_behavior(self): - self.write_config('BB_DISKMON_DIRS = "STOPTASKS,${TMPDIR},100000G,100K"') + self.write_config('BB_DISKMON_DIRS = "STOPTASKS,${TMPDIR},100000G,100K"\nBB_HEARTBEAT_EVENT = "1"') res = bitbake("delay -c delay", ignore_status = True) self.assertTrue('ERROR: No new tasks can be executed since the disk space monitor action is "STOPTASKS"!' in res.output, msg = "Tasks should have stopped. Disk monitor is set to STOPTASK: %s" % res.output) self.assertEqual(res.status, 1, msg = "bitbake reported exit code %s. It should have been 1. Bitbake output: %s" % (str(res.status), res.output)) - self.write_config('BB_DISKMON_DIRS = "ABORT,${TMPDIR},100000G,100K"') + self.write_config('BB_DISKMON_DIRS = "HALT,${TMPDIR},100000G,100K"\nBB_HEARTBEAT_EVENT = "1"') res = bitbake("delay -c delay", ignore_status = True) - self.assertTrue('ERROR: Immediately abort since the disk space monitor action is "ABORT"!' in res.output, "Tasks should have been aborted immediatelly. Disk monitor is set to ABORT: %s" % res.output) + self.assertTrue('ERROR: Immediately halt since the disk space monitor action is "HALT"!' in res.output, "Tasks should have been halted immediately. Disk monitor is set to HALT: %s" % res.output) self.assertEqual(res.status, 1, msg = "bitbake reported exit code %s. It should have been 1. Bitbake output: %s" % (str(res.status), res.output)) - self.write_config('BB_DISKMON_DIRS = "WARN,${TMPDIR},100000G,100K"') + self.write_config('BB_DISKMON_DIRS = "WARN,${TMPDIR},100000G,100K"\nBB_HEARTBEAT_EVENT = "1"') res = bitbake("delay -c delay") self.assertTrue('WARNING: The free space' in res.output, msg = "A warning should have been displayed for disk monitor is set to WARN: %s" %res.output) @@ -77,9 +82,9 @@ class SanityOptionsTest(OESelftestTestCase): def test_options_warnqa_errorqa_switch(self): - self.write_config("INHERIT_remove = \"report-error\"") + self.write_config("INHERIT:remove = \"report-error\"") if "packages-list" not in get_bb_var("ERROR_QA"): - self.append_config("ERROR_QA_append = \" packages-list\"") + self.append_config("ERROR_QA:append:pn-xcursor-transparent-theme = \" packages-list\"") self.write_recipeinc('xcursor-transparent-theme', 'PACKAGES += \"${PN}-dbg\"') self.add_command_to_tearDown('bitbake -c clean xcursor-transparent-theme') @@ -89,8 +94,8 @@ class SanityOptionsTest(OESelftestTestCase): self.assertTrue(line and line.startswith("ERROR:"), msg=res.output) self.assertEqual(res.status, 1, msg = "bitbake reported exit code %s. It should have been 1. Bitbake output: %s" % (str(res.status), res.output)) self.write_recipeinc('xcursor-transparent-theme', 'PACKAGES += \"${PN}-dbg\"') - self.append_config('ERROR_QA_remove = "packages-list"') - self.append_config('WARN_QA_append = " packages-list"') + self.append_config('ERROR_QA:remove:pn-xcursor-transparent-theme = "packages-list"') + self.append_config('WARN_QA:append:pn-xcursor-transparent-theme = " packages-list"') res = bitbake("xcursor-transparent-theme -f -c package") self.delete_recipeinc('xcursor-transparent-theme') line = self.getline(res, "QA Issue: xcursor-transparent-theme-dbg is listed in PACKAGES multiple times, this leads to packaging errors.") @@ -147,19 +152,48 @@ class BuildhistoryTests(BuildhistoryBase): self.run_buildhistory_operation(target, target_config="PR = \"r1\"", change_bh_location=True) self.run_buildhistory_operation(target, target_config="PR = \"r0\"", change_bh_location=False, expect_error=True, error_regex=error) + def test_fileinfo(self): + self.config_buildhistory() + bitbake('hicolor-icon-theme') + history_dir = get_bb_var('BUILDHISTORY_DIR_PACKAGE', 'hicolor-icon-theme') + self.assertTrue(os.path.isdir(history_dir), 'buildhistory dir was not created.') + + def load_bh(f): + d = {} + for line in open(f): + split = [s.strip() for s in line.split('=', 1)] + if len(split) > 1: + d[split[0]] = split[1] + return d + + data = load_bh(os.path.join(history_dir, 'hicolor-icon-theme', 'latest')) + self.assertIn('FILELIST', data) + self.assertEqual(data['FILELIST'], '/usr/share/icons/hicolor/index.theme') + self.assertGreater(int(data['PKGSIZE']), 0) + + data = load_bh(os.path.join(history_dir, 'hicolor-icon-theme-dev', 'latest')) + if 'FILELIST' in data: + self.assertEqual(data['FILELIST'], '/usr/share/pkgconfig/default-icon-theme.pc') + self.assertGreater(int(data['PKGSIZE']), 0) + class ArchiverTest(OESelftestTestCase): def test_arch_work_dir_and_export_source(self): """ Test for archiving the work directory and exporting the source files. """ - self.write_config("INHERIT += \"archiver\"\nARCHIVER_MODE[src] = \"original\"\nARCHIVER_MODE[srpm] = \"1\"") + self.write_config(""" +INHERIT += "archiver" +PACKAGE_CLASSES = "package_rpm" +ARCHIVER_MODE[src] = "original" +ARCHIVER_MODE[srpm] = "1" +""") res = bitbake("xcursor-transparent-theme", ignore_status=True) self.assertEqual(res.status, 0, "\nCouldn't build xcursortransparenttheme.\nbitbake output %s" % res.output) deploy_dir_src = get_bb_var('DEPLOY_DIR_SRC') pkgs_path = g.glob(str(deploy_dir_src) + "/allarch*/xcurs*") src_file_glob = str(pkgs_path[0]) + "/xcursor*.src.rpm" - tar_file_glob = str(pkgs_path[0]) + "/xcursor*.tar.gz" - self.assertTrue((g.glob(src_file_glob) and g.glob(tar_file_glob)), "Couldn't find .src.rpm and .tar.gz files under %s/allarch*/xcursor*" % deploy_dir_src) + tar_file_glob = str(pkgs_path[0]) + "/xcursor*.tar.xz" + self.assertTrue((g.glob(src_file_glob) and g.glob(tar_file_glob)), "Couldn't find .src.rpm and .tar.xz files under %s/allarch*/xcursor*" % deploy_dir_src) class ToolchainOptions(OESelftestTestCase): def test_toolchain_fortran(self): @@ -167,10 +201,11 @@ class ToolchainOptions(OESelftestTestCase): Test that Fortran works by building a Hello, World binary. """ - features = 'FORTRAN_forcevariable = ",fortran"\n' + features = 'FORTRAN:forcevariable = ",fortran"\n' self.write_config(features) bitbake('fortran-helloworld') +@OETestTag("yocto-mirrors") class SourceMirroring(OESelftestTestCase): # Can we download everything from the Yocto Sources Mirror over http only def test_yocto_source_mirror(self): @@ -196,3 +231,9 @@ PREMIRRORS = "\\ bitbake("world --runall fetch") + +class Poisoning(OESelftestTestCase): + def test_poisoning(self): + res = bitbake("poison", ignore_status=True) + self.assertNotEqual(res.status, 0) + self.assertTrue("is unsafe for cross-compilation" in res.output) diff --git a/meta/lib/oeqa/selftest/cases/c_cpp.py b/meta/lib/oeqa/selftest/cases/c_cpp.py new file mode 100644 index 0000000000..9a70ce29f5 --- /dev/null +++ b/meta/lib/oeqa/selftest/cases/c_cpp.py @@ -0,0 +1,60 @@ +# +# Copyright OpenEmbedded Contributors +# +# SPDX-License-Identifier: MIT +# + +from oeqa.selftest.case import OESelftestTestCase +from oeqa.core.decorator.data import skipIfNotQemuUsermode +from oeqa.utils.commands import bitbake + + +class CCppTests(OESelftestTestCase): + + @skipIfNotQemuUsermode() + def _qemu_usermode(self, recipe_name): + self.add_command_to_tearDown("bitbake -c clean %s" % recipe_name) + bitbake("%s -c run_tests" % recipe_name) + + @skipIfNotQemuUsermode() + def _qemu_usermode_failing(self, recipe_name): + config = 'PACKAGECONFIG:pn-%s = "failing_test"' % recipe_name + self.write_config(config) + self.add_command_to_tearDown("bitbake -c clean %s" % recipe_name) + result = bitbake("%s -c run_tests" % recipe_name, ignore_status=True) + self.assertNotEqual(0, result.status, "command: %s is expected to fail but passed, status: %s, output: %s, error: %s" % ( + result.command, result.status, result.output, result.error)) + + +class CMakeTests(CCppTests): + def test_cmake_qemu(self): + """Test for cmake-qemu.bbclass good case + + compile the cmake-example and verify the CTests pass in qemu-user. + qemu-user is configured by CMAKE_CROSSCOMPILING_EMULATOR. + """ + self._qemu_usermode("cmake-example") + + def test_cmake_qemu_failing(self): + """Test for cmake-qemu.bbclass bad case + + Break the comparison in the test code and verify the CTests do not pass. + """ + self._qemu_usermode_failing("cmake-example") + + +class MesonTests(CCppTests): + def test_meson_qemu(self): + """Test the qemu-user feature of the meson.bbclass good case + + compile the meson-example and verify the Unit Test pass in qemu-user. + qemu-user is configured by meson's exe_wrapper option. + """ + self._qemu_usermode("meson-example") + + def test_meson_qemu_failing(self): + """Test the qemu-user feature of the meson.bbclass bad case + + Break the comparison in the test code and verify the Unit Test does not pass in qemu-user. + """ + self._qemu_usermode_failing("meson-example") diff --git a/meta/lib/oeqa/selftest/cases/containerimage.py b/meta/lib/oeqa/selftest/cases/containerimage.py index c0998e319e..23c0a1408a 100644 --- a/meta/lib/oeqa/selftest/cases/containerimage.py +++ b/meta/lib/oeqa/selftest/cases/containerimage.py @@ -1,4 +1,6 @@ # +# Copyright OpenEmbedded Contributors +# # SPDX-License-Identifier: MIT # @@ -13,7 +15,7 @@ from oeqa.utils.commands import bitbake, get_bb_vars, runCmd # The only package added to the image is container_image_testpkg, which # contains one file. However, due to some other things not cleaning up during # rootfs creation, there is some cruft. Ideally bugs will be filed and the -# cruft removed, but for now we whitelist some known set. +# cruft removed, but for now we ignore some known set. # # Also for performance reasons we're only checking the cruft when using ipk. # When using deb, and rpm it is a bit different and we could test all @@ -22,7 +24,7 @@ from oeqa.utils.commands import bitbake, get_bb_vars, runCmd # class ContainerImageTests(OESelftestTestCase): - # Verify that when specifying a IMAGE_TYPEDEP_ of the form "foo.bar" that + # Verify that when specifying a IMAGE_TYPEDEP: of the form "foo.bar" that # the conversion type bar gets added as a dep as well def test_expected_files(self): @@ -42,6 +44,9 @@ IMAGE_FSTYPES = "container" PACKAGE_CLASSES = "package_ipk" IMAGE_FEATURES = "" IMAGE_BUILDINFO_FILE = "" +INIT_MANAGER = "sysvinit" +IMAGE_INSTALL:remove = "ssh-pregen-hostkeys" + """) bbvars = get_bb_vars(['bindir', 'sysconfdir', 'localstatedir', @@ -57,11 +62,7 @@ IMAGE_BUILDINFO_FILE = "" '.{sysconfdir}/version', './run/', '.{localstatedir}/cache/', - '.{localstatedir}/cache/ldconfig/', - '.{localstatedir}/cache/ldconfig/aux-cache', - '.{localstatedir}/cache/opkg/', - '.{localstatedir}/lib/', - '.{localstatedir}/lib/opkg/' + '.{localstatedir}/lib/' ] expected_files = [ x.format(bindir=bbvars['bindir'], diff --git a/meta/lib/oeqa/selftest/cases/cve_check.py b/meta/lib/oeqa/selftest/cases/cve_check.py new file mode 100644 index 0000000000..3dd3e89d3e --- /dev/null +++ b/meta/lib/oeqa/selftest/cases/cve_check.py @@ -0,0 +1,291 @@ +# +# Copyright OpenEmbedded Contributors +# +# SPDX-License-Identifier: MIT +# + +import json +import os +from oeqa.selftest.case import OESelftestTestCase +from oeqa.utils.commands import bitbake, get_bb_vars + +class CVECheck(OESelftestTestCase): + + def test_version_compare(self): + from oe.cve_check import Version + + result = Version("100") > Version("99") + self.assertTrue( result, msg="Failed to compare version '100' > '99'") + result = Version("2.3.1") > Version("2.2.3") + self.assertTrue( result, msg="Failed to compare version '2.3.1' > '2.2.3'") + result = Version("2021-01-21") > Version("2020-12-25") + self.assertTrue( result, msg="Failed to compare version '2021-01-21' > '2020-12-25'") + result = Version("1.2-20200910") < Version("1.2-20200920") + self.assertTrue( result, msg="Failed to compare version '1.2-20200910' < '1.2-20200920'") + + result = Version("1.0") >= Version("1.0beta") + self.assertTrue( result, msg="Failed to compare version '1.0' >= '1.0beta'") + result = Version("1.0-rc2") > Version("1.0-rc1") + self.assertTrue( result, msg="Failed to compare version '1.0-rc2' > '1.0-rc1'") + result = Version("1.0.alpha1") < Version("1.0") + self.assertTrue( result, msg="Failed to compare version '1.0.alpha1' < '1.0'") + result = Version("1.0_dev") <= Version("1.0") + self.assertTrue( result, msg="Failed to compare version '1.0_dev' <= '1.0'") + + # ignore "p1" and "p2", so these should be equal + result = Version("1.0p2") == Version("1.0p1") + self.assertTrue( result ,msg="Failed to compare version '1.0p2' to '1.0p1'") + # ignore the "b" and "r" + result = Version("1.0b") == Version("1.0r") + self.assertTrue( result ,msg="Failed to compare version '1.0b' to '1.0r'") + + # consider the trailing alphabet as patched level when comparing + result = Version("1.0b","alphabetical") < Version("1.0r","alphabetical") + self.assertTrue( result ,msg="Failed to compare version with suffix '1.0b' < '1.0r'") + result = Version("1.0b","alphabetical") > Version("1.0","alphabetical") + self.assertTrue( result ,msg="Failed to compare version with suffix '1.0b' > '1.0'") + + # consider the trailing "p" and "patch" as patched released when comparing + result = Version("1.0","patch") < Version("1.0p1","patch") + self.assertTrue( result ,msg="Failed to compare version with suffix '1.0' < '1.0p1'") + result = Version("1.0p2","patch") > Version("1.0p1","patch") + self.assertTrue( result ,msg="Failed to compare version with suffix '1.0p2' > '1.0p1'") + result = Version("1.0_patch2","patch") < Version("1.0_patch3","patch") + self.assertTrue( result ,msg="Failed to compare version with suffix '1.0_patch2' < '1.0_patch3'") + + + def test_convert_cve_version(self): + from oe.cve_check import convert_cve_version + + # Default format + self.assertEqual(convert_cve_version("8.3"), "8.3") + self.assertEqual(convert_cve_version(""), "") + + # OpenSSL format version + self.assertEqual(convert_cve_version("1.1.1t"), "1.1.1t") + + # OpenSSH format + self.assertEqual(convert_cve_version("8.3_p1"), "8.3p1") + self.assertEqual(convert_cve_version("8.3_p22"), "8.3p22") + + # Linux kernel format + self.assertEqual(convert_cve_version("6.2_rc8"), "6.2-rc8") + self.assertEqual(convert_cve_version("6.2_rc31"), "6.2-rc31") + + def test_product_match(self): + from oe.cve_check import has_cve_product_match + + status = {} + status["detail"] = "ignored" + status["vendor"] = "*" + status["product"] = "*" + status["description"] = "" + status["mapping"] = "" + + self.assertEqual(has_cve_product_match(status, "some_vendor:some_product"), True) + self.assertEqual(has_cve_product_match(status, "*:*"), True) + self.assertEqual(has_cve_product_match(status, "some_product"), True) + self.assertEqual(has_cve_product_match(status, "glibc"), True) + self.assertEqual(has_cve_product_match(status, "glibca"), True) + self.assertEqual(has_cve_product_match(status, "aglibc"), True) + self.assertEqual(has_cve_product_match(status, "*"), True) + self.assertEqual(has_cve_product_match(status, "aglibc glibc test:test"), True) + + status["product"] = "glibc" + self.assertEqual(has_cve_product_match(status, "some_vendor:some_product"), False) + # The CPE in the recipe must be defined, no * accepted + self.assertEqual(has_cve_product_match(status, "*:*"), False) + self.assertEqual(has_cve_product_match(status, "*"), False) + self.assertEqual(has_cve_product_match(status, "some_product"), False) + self.assertEqual(has_cve_product_match(status, "glibc"), True) + self.assertEqual(has_cve_product_match(status, "glibca"), False) + self.assertEqual(has_cve_product_match(status, "aglibc"), False) + self.assertEqual(has_cve_product_match(status, "some_vendor:glibc"), True) + self.assertEqual(has_cve_product_match(status, "some_vendor:glibc test"), True) + self.assertEqual(has_cve_product_match(status, "test some_vendor:glibc"), True) + + status["vendor"] = "glibca" + status["product"] = "glibc" + self.assertEqual(has_cve_product_match(status, "some_vendor:some_product"), False) + # The CPE in the recipe must be defined, no * accepted + self.assertEqual(has_cve_product_match(status, "*:*"), False) + self.assertEqual(has_cve_product_match(status, "*"), False) + self.assertEqual(has_cve_product_match(status, "some_product"), False) + self.assertEqual(has_cve_product_match(status, "glibc"), False) + self.assertEqual(has_cve_product_match(status, "glibca"), False) + self.assertEqual(has_cve_product_match(status, "aglibc"), False) + self.assertEqual(has_cve_product_match(status, "some_vendor:glibc"), False) + self.assertEqual(has_cve_product_match(status, "glibca:glibc"), True) + self.assertEqual(has_cve_product_match(status, "test:test glibca:glibc"), True) + self.assertEqual(has_cve_product_match(status, "test glibca:glibc"), True) + self.assertEqual(has_cve_product_match(status, "glibca:glibc test"), True) + + + def test_recipe_report_json(self): + config = """ +INHERIT += "cve-check" +CVE_CHECK_FORMAT_JSON = "1" +""" + self.write_config(config) + + vars = get_bb_vars(["CVE_CHECK_SUMMARY_DIR", "CVE_CHECK_SUMMARY_FILE_NAME_JSON"]) + summary_json = os.path.join(vars["CVE_CHECK_SUMMARY_DIR"], vars["CVE_CHECK_SUMMARY_FILE_NAME_JSON"]) + recipe_json = os.path.join(vars["CVE_CHECK_SUMMARY_DIR"], "m4-native_cve.json") + + try: + os.remove(summary_json) + os.remove(recipe_json) + except FileNotFoundError: + pass + + bitbake("m4-native -c cve_check") + + def check_m4_json(filename): + with open(filename) as f: + report = json.load(f) + self.assertEqual(report["version"], "1") + self.assertEqual(len(report["package"]), 1) + package = report["package"][0] + self.assertEqual(package["name"], "m4-native") + found_cves = { issue["id"]: issue["status"] for issue in package["issue"]} + self.assertIn("CVE-2008-1687", found_cves) + self.assertEqual(found_cves["CVE-2008-1687"], "Patched") + + self.assertExists(summary_json) + check_m4_json(summary_json) + self.assertExists(recipe_json) + check_m4_json(recipe_json) + + + def test_image_json(self): + config = """ +INHERIT += "cve-check" +CVE_CHECK_FORMAT_JSON = "1" +""" + self.write_config(config) + + vars = get_bb_vars(["CVE_CHECK_DIR", "CVE_CHECK_SUMMARY_DIR", "CVE_CHECK_SUMMARY_FILE_NAME_JSON"]) + report_json = os.path.join(vars["CVE_CHECK_SUMMARY_DIR"], vars["CVE_CHECK_SUMMARY_FILE_NAME_JSON"]) + print(report_json) + try: + os.remove(report_json) + except FileNotFoundError: + pass + + bitbake("core-image-minimal-initramfs") + self.assertExists(report_json) + + # Check that the summary report lists at least one package + with open(report_json) as f: + report = json.load(f) + self.assertEqual(report["version"], "1") + self.assertGreater(len(report["package"]), 1) + + # Check that a random recipe wrote a recipe report to deploy/cve/ + recipename = report["package"][0]["name"] + recipe_report = os.path.join(vars["CVE_CHECK_DIR"], recipename + "_cve.json") + self.assertExists(recipe_report) + with open(recipe_report) as f: + report = json.load(f) + self.assertEqual(report["version"], "1") + self.assertEqual(len(report["package"]), 1) + self.assertEqual(report["package"][0]["name"], recipename) + + + def test_recipe_report_json_unpatched(self): + config = """ +INHERIT += "cve-check" +CVE_CHECK_FORMAT_JSON = "1" +CVE_CHECK_REPORT_PATCHED = "0" +""" + self.write_config(config) + + vars = get_bb_vars(["CVE_CHECK_SUMMARY_DIR", "CVE_CHECK_SUMMARY_FILE_NAME_JSON"]) + summary_json = os.path.join(vars["CVE_CHECK_SUMMARY_DIR"], vars["CVE_CHECK_SUMMARY_FILE_NAME_JSON"]) + recipe_json = os.path.join(vars["CVE_CHECK_SUMMARY_DIR"], "m4-native_cve.json") + + try: + os.remove(summary_json) + os.remove(recipe_json) + except FileNotFoundError: + pass + + bitbake("m4-native -c cve_check") + + def check_m4_json(filename): + with open(filename) as f: + report = json.load(f) + self.assertEqual(report["version"], "1") + self.assertEqual(len(report["package"]), 1) + package = report["package"][0] + self.assertEqual(package["name"], "m4-native") + #m4 had only Patched CVEs, so the issues array will be empty + self.assertEqual(package["issue"], []) + + self.assertExists(summary_json) + check_m4_json(summary_json) + self.assertExists(recipe_json) + check_m4_json(recipe_json) + + + def test_recipe_report_json_ignored(self): + config = """ +INHERIT += "cve-check" +CVE_CHECK_FORMAT_JSON = "1" +CVE_CHECK_REPORT_PATCHED = "1" +""" + self.write_config(config) + + vars = get_bb_vars(["CVE_CHECK_SUMMARY_DIR", "CVE_CHECK_SUMMARY_FILE_NAME_JSON"]) + summary_json = os.path.join(vars["CVE_CHECK_SUMMARY_DIR"], vars["CVE_CHECK_SUMMARY_FILE_NAME_JSON"]) + recipe_json = os.path.join(vars["CVE_CHECK_SUMMARY_DIR"], "logrotate_cve.json") + + try: + os.remove(summary_json) + os.remove(recipe_json) + except FileNotFoundError: + pass + + bitbake("logrotate -c cve_check") + + def check_m4_json(filename): + with open(filename) as f: + report = json.load(f) + self.assertEqual(report["version"], "1") + self.assertEqual(len(report["package"]), 1) + package = report["package"][0] + self.assertEqual(package["name"], "logrotate") + found_cves = {} + for issue in package["issue"]: + found_cves[issue["id"]] = { + "status" : issue["status"], + "detail" : issue["detail"] if "detail" in issue else "", + "description" : issue["description"] if "description" in issue else "" + } + # m4 CVE should not be in logrotate + self.assertNotIn("CVE-2008-1687", found_cves) + # logrotate has both Patched and Ignored CVEs + detail = "version-not-in-range" + self.assertIn("CVE-2011-1098", found_cves) + self.assertEqual(found_cves["CVE-2011-1098"]["status"], "Patched") + self.assertEqual(found_cves["CVE-2011-1098"]["detail"], detail) + self.assertEqual(len(found_cves["CVE-2011-1098"]["description"]), 0) + detail = "not-applicable-platform" + description = "CVE is debian, gentoo or SUSE specific on the way logrotate was installed/used" + self.assertIn("CVE-2011-1548", found_cves) + self.assertEqual(found_cves["CVE-2011-1548"]["status"], "Ignored") + self.assertEqual(found_cves["CVE-2011-1548"]["detail"], detail) + self.assertEqual(found_cves["CVE-2011-1548"]["description"], description) + self.assertIn("CVE-2011-1549", found_cves) + self.assertEqual(found_cves["CVE-2011-1549"]["status"], "Ignored") + self.assertEqual(found_cves["CVE-2011-1549"]["detail"], detail) + self.assertEqual(found_cves["CVE-2011-1549"]["description"], description) + self.assertIn("CVE-2011-1550", found_cves) + self.assertEqual(found_cves["CVE-2011-1550"]["status"], "Ignored") + self.assertEqual(found_cves["CVE-2011-1550"]["detail"], detail) + self.assertEqual(found_cves["CVE-2011-1550"]["description"], description) + + self.assertExists(summary_json) + check_m4_json(summary_json) + self.assertExists(recipe_json) + check_m4_json(recipe_json) diff --git a/meta/lib/oeqa/selftest/cases/debuginfod.py b/meta/lib/oeqa/selftest/cases/debuginfod.py new file mode 100644 index 0000000000..46c0cd87bb --- /dev/null +++ b/meta/lib/oeqa/selftest/cases/debuginfod.py @@ -0,0 +1,160 @@ +# +# Copyright OpenEmbedded Contributors +# +# SPDX-License-Identifier: MIT +# +import os +import socketserver +import subprocess +import time +import urllib +import pathlib + +from oeqa.core.decorator import OETestTag +from oeqa.selftest.case import OESelftestTestCase +from oeqa.utils.commands import bitbake, get_bb_var, runqemu + + +class Debuginfod(OESelftestTestCase): + + def wait_for_debuginfod(self, port): + """ + debuginfod takes time to scan the packages and requesting too early may + result in a test failure if the right packages haven't been scanned yet. + + Request the metrics endpoint periodically and wait for there to be no + busy scanning threads. + + Returns if debuginfod is ready, raises an exception if not within the + timeout. + """ + + # Wait two minutes + countdown = 24 + delay = 5 + latest = None + + while countdown: + self.logger.info("waiting...") + time.sleep(delay) + + self.logger.info("polling server") + if self.debuginfod.poll(): + self.logger.info("server dead") + self.debuginfod.communicate() + self.fail("debuginfod terminated unexpectedly") + self.logger.info("server alive") + + try: + with urllib.request.urlopen("http://localhost:%d/metrics" % port, timeout=10) as f: + for line in f.read().decode("ascii").splitlines(): + key, value = line.rsplit(" ", 1) + if key == "thread_busy{role=\"scan\"}": + latest = int(value) + self.logger.info("Waiting for %d scan jobs to finish" % latest) + if latest == 0: + return + except urllib.error.URLError as e: + # TODO: how to catch just timeouts? + self.logger.error(e) + + countdown -= 1 + + raise TimeoutError("Cannot connect debuginfod, still %d scan jobs running" % latest) + + def start_debuginfod(self, feed_dir): + # We assume that the caller has already bitbake'd elfutils-native:do_addto_recipe_sysroot + + # Save some useful paths for later + native_sysroot = pathlib.Path(get_bb_var("RECIPE_SYSROOT_NATIVE", "elfutils-native")) + native_bindir = native_sysroot / "usr" / "bin" + self.debuginfod = native_bindir / "debuginfod" + self.debuginfod_find = native_bindir / "debuginfod-find" + + cmd = [ + self.debuginfod, + "--verbose", + # In-memory database, this is a one-shot test + "--database=:memory:", + # Don't use all the host cores + "--concurrency=8", + "--connection-pool=8", + # Disable rescanning, this is a one-shot test + "--rescan-time=0", + "--groom-time=0", + feed_dir, + ] + + format = get_bb_var("PACKAGE_CLASSES").split()[0] + if format == "package_deb": + cmd.append("--scan-deb-dir") + elif format == "package_ipk": + cmd.append("--scan-deb-dir") + elif format == "package_rpm": + cmd.append("--scan-rpm-dir") + else: + self.fail("Unknown package class %s" % format) + + # Find a free port. Racey but the window is small. + with socketserver.TCPServer(("localhost", 0), None) as s: + self.port = s.server_address[1] + cmd.append("--port=%d" % self.port) + + self.logger.info(f"Starting server {cmd}") + self.debuginfod = subprocess.Popen(cmd, env={}) + self.wait_for_debuginfod(self.port) + + + def test_debuginfod_native(self): + """ + Test debuginfod outside of qemu, by building a package and looking up a + binary's debuginfo using elfutils-native. + """ + + self.write_config(""" +TMPDIR = "${TOPDIR}/tmp-debuginfod" +DISTRO_FEATURES:append = " debuginfod" +INHERIT += "localpkgfeed" +""") + bitbake("elfutils-native:do_addto_recipe_sysroot xz xz:do_package xz:do_localpkgfeed") + + try: + self.start_debuginfod(get_bb_var("LOCALPKGFEED_DIR", "xz")) + + env = os.environ.copy() + env["DEBUGINFOD_URLS"] = "http://localhost:%d/" % self.port + + pkgs = pathlib.Path(get_bb_var("PKGDEST", "xz")) + cmd = (self.debuginfod_find, "debuginfo", pkgs / "xz" / "usr" / "bin" / "xz.xz") + self.logger.info(f"Starting client {cmd}") + output = subprocess.check_output(cmd, env=env, text=True) + # This should be more comprehensive + self.assertIn("/.cache/debuginfod_client/", output) + finally: + self.debuginfod.kill() + + @OETestTag("runqemu") + def test_debuginfod_qemu(self): + """ + Test debuginfod-find inside a qemu, talking to a debuginfod on the host. + """ + + self.write_config(""" +TMPDIR = "${TOPDIR}/tmp-debuginfod" +DISTRO_FEATURES:append = " debuginfod" +INHERIT += "localpkgfeed" +CORE_IMAGE_EXTRA_INSTALL += "elfutils xz" + """) + bitbake("core-image-minimal elfutils-native:do_addto_recipe_sysroot xz:do_localpkgfeed") + + try: + self.start_debuginfod(get_bb_var("LOCALPKGFEED_DIR", "xz")) + + with runqemu("core-image-minimal", runqemuparams="nographic") as qemu: + cmd = "DEBUGINFOD_URLS=http://%s:%d/ debuginfod-find debuginfo /usr/bin/xz" % (qemu.server_ip, self.port) + self.logger.info(f"Starting client {cmd}") + status, output = qemu.run_serial(cmd) + # This should be more comprehensive + self.assertIn("/.cache/debuginfod_client/", output) + finally: + self.debuginfod.kill() diff --git a/meta/lib/oeqa/selftest/cases/devtool.py b/meta/lib/oeqa/selftest/cases/devtool.py index 57e6662e4a..7d61773980 100644 --- a/meta/lib/oeqa/selftest/cases/devtool.py +++ b/meta/lib/oeqa/selftest/cases/devtool.py @@ -1,18 +1,23 @@ # +# Copyright OpenEmbedded Contributors +# # SPDX-License-Identifier: MIT # +import errno import os import re import shutil import tempfile import glob import fnmatch +import unittest +import json -import oeqa.utils.ftools as ftools from oeqa.selftest.case import OESelftestTestCase from oeqa.utils.commands import runCmd, bitbake, get_bb_var, create_temp_layer from oeqa.utils.commands import get_bb_vars, runqemu, get_test_layer +from oeqa.core.decorator import OETestTag oldmetapath = None @@ -24,6 +29,9 @@ def setUpModule(): corecopydir = os.path.join(templayerdir, 'core-copy') bblayers_conf = os.path.join(os.environ['BUILDDIR'], 'conf', 'bblayers.conf') edited_layers = [] + # make sure user doesn't have a local workspace + result = runCmd('bitbake-layers show-layers') + assert "workspacelayer" not in result.output, "Devtool test suite cannot be run with a local workspace directory" # We need to take a copy of the meta layer so we can modify it and not # have any races against other tests that might be running in parallel @@ -38,10 +46,17 @@ def setUpModule(): canonical_layerpath = os.path.realpath(canonical_layerpath) + '/' edited_layers.append(layerpath) oldmetapath = os.path.realpath(layerpath) + + # when downloading poky from tar.gz some tests will be skipped (BUG 12389) + try: + runCmd('git rev-parse --is-inside-work-tree', cwd=canonical_layerpath) + except: + raise unittest.SkipTest("devtool tests require folder to be a git repo") + result = runCmd('git rev-parse --show-toplevel', cwd=canonical_layerpath) oldreporoot = result.output.rstrip() newmetapath = os.path.join(corecopydir, os.path.relpath(oldmetapath, oldreporoot)) - runCmd('git clone %s %s' % (oldreporoot, corecopydir), cwd=templayerdir) + runCmd('git clone file://%s %s' % (oldreporoot, corecopydir), cwd=templayerdir) # Now we need to copy any modified files # You might ask "why not just copy the entire tree instead of # cloning and doing this?" - well, the problem with that is @@ -56,7 +71,8 @@ def setUpModule(): if pth.startswith(canonical_layerpath): if relpth.endswith('/'): destdir = os.path.join(corecopydir, relpth) - shutil.copytree(pth, destdir) + # avoid race condition by not copying .pyc files YPBZ#13421,13803 + shutil.copytree(pth, destdir, ignore=shutil.ignore_patterns('*.pyc', '__pycache__')) else: destdir = os.path.join(corecopydir, os.path.dirname(relpth)) bb.utils.mkdirhier(destdir) @@ -79,32 +95,15 @@ def tearDownModule(): bb.utils.edit_bblayers_conf(bblayers_conf, None, None, bblayers_edit_cb) shutil.rmtree(templayerdir) -class DevtoolBase(OESelftestTestCase): - - @classmethod - def setUpClass(cls): - super(DevtoolBase, cls).setUpClass() - bb_vars = get_bb_vars(['TOPDIR', 'SSTATE_DIR']) - cls.original_sstate = bb_vars['SSTATE_DIR'] - cls.devtool_sstate = os.path.join(bb_vars['TOPDIR'], 'sstate_devtool') - cls.sstate_conf = 'SSTATE_DIR = "%s"\n' % cls.devtool_sstate - cls.sstate_conf += ('SSTATE_MIRRORS += "file://.* file:///%s/PATH"\n' - % cls.original_sstate) - - @classmethod - def tearDownClass(cls): - cls.logger.debug('Deleting devtool sstate cache on %s' % cls.devtool_sstate) - runCmd('rm -rf %s' % cls.devtool_sstate) - super(DevtoolBase, cls).tearDownClass() +class DevtoolTestCase(OESelftestTestCase): def setUp(self): """Test case setup function""" - super(DevtoolBase, self).setUp() + super(DevtoolTestCase, self).setUp() self.workspacedir = os.path.join(self.builddir, 'workspace') self.assertTrue(not os.path.exists(self.workspacedir), 'This test cannot be run with a workspace directory ' 'under the build directory') - self.append_config(self.sstate_conf) def _check_src_repo(self, repo_dir): """Check srctree git repository""" @@ -234,6 +233,103 @@ class DevtoolBase(OESelftestTestCase): filelist.append(' '.join(splitline)) return filelist + def _check_diff(self, diffoutput, addlines, removelines): + """Check output from 'git diff' matches expectation""" + remaining_addlines = addlines[:] + remaining_removelines = removelines[:] + for line in diffoutput.splitlines(): + if line.startswith('+++') or line.startswith('---'): + continue + elif line.startswith('+'): + matched = False + for item in addlines: + if re.match(item, line[1:].strip()): + matched = True + remaining_addlines.remove(item) + break + self.assertTrue(matched, 'Unexpected diff add line: %s' % line) + elif line.startswith('-'): + matched = False + for item in removelines: + if re.match(item, line[1:].strip()): + matched = True + remaining_removelines.remove(item) + break + self.assertTrue(matched, 'Unexpected diff remove line: %s' % line) + if remaining_addlines: + self.fail('Expected added lines not found: %s' % remaining_addlines) + if remaining_removelines: + self.fail('Expected removed lines not found: %s' % remaining_removelines) + + def _check_runqemu_prerequisites(self): + """Check runqemu is available + + Whilst some tests would seemingly be better placed as a runtime test, + unfortunately the runtime tests run under bitbake and you can't run + devtool within bitbake (since devtool needs to run bitbake itself). + Additionally we are testing build-time functionality as well, so + really this has to be done as an oe-selftest test. + """ + machine = get_bb_var('MACHINE') + if not machine.startswith('qemu'): + self.skipTest('This test only works with qemu machines') + if not os.path.exists('/etc/runqemu-nosudo'): + self.skipTest('You must set up tap devices with scripts/runqemu-gen-tapdevs before running this test') + result = runCmd('PATH="$PATH:/sbin:/usr/sbin" ip tuntap show', ignore_status=True) + if result.status != 0: + result = runCmd('PATH="$PATH:/sbin:/usr/sbin" ifconfig -a', ignore_status=True) + if result.status != 0: + self.skipTest('Failed to determine if tap devices exist with ifconfig or ip: %s' % result.output) + for line in result.output.splitlines(): + if line.startswith('tap'): + break + else: + self.skipTest('No tap devices found - you must set up tap devices with scripts/runqemu-gen-tapdevs before running this test') + + def _test_devtool_add_git_url(self, git_url, version, pn, resulting_src_uri, srcrev=None): + self.track_for_cleanup(self.workspacedir) + self.add_command_to_tearDown('bitbake-layers remove-layer */workspace') + command = 'devtool add --version %s %s %s' % (version, pn, git_url) + if srcrev : + command += ' --srcrev %s' %srcrev + result = runCmd(command) + self.assertExists(os.path.join(self.workspacedir, 'conf', 'layer.conf'), 'Workspace directory not created') + # Check the recipe name is correct + recipefile = get_bb_var('FILE', pn) + self.assertIn('%s_git.bb' % pn, recipefile, 'Recipe file incorrectly named') + self.assertIn(recipefile, result.output) + # Test devtool status + result = runCmd('devtool status') + self.assertIn(pn, result.output) + self.assertIn(recipefile, result.output) + checkvars = {} + checkvars['SRC_URI'] = resulting_src_uri + self._test_recipe_contents(recipefile, checkvars, []) + +class DevtoolBase(DevtoolTestCase): + + @classmethod + def setUpClass(cls): + super(DevtoolBase, cls).setUpClass() + bb_vars = get_bb_vars(['TOPDIR', 'SSTATE_DIR']) + cls.original_sstate = bb_vars['SSTATE_DIR'] + cls.devtool_sstate = os.path.join(bb_vars['TOPDIR'], 'sstate_devtool') + cls.sstate_conf = 'SSTATE_DIR = "%s"\n' % cls.devtool_sstate + cls.sstate_conf += ('SSTATE_MIRRORS += "file://.* file:///%s/PATH"\n' + % cls.original_sstate) + cls.sstate_conf += ('BB_HASHSERVE_UPSTREAM = "hashserv.yocto.io:8687"\n') + + @classmethod + def tearDownClass(cls): + cls.logger.debug('Deleting devtool sstate cache on %s' % cls.devtool_sstate) + runCmd('rm -rf %s' % cls.devtool_sstate) + super(DevtoolBase, cls).tearDownClass() + + def setUp(self): + """Test case setup function""" + super(DevtoolBase, self).setUp() + self.append_config(self.sstate_conf) + class DevtoolTests(DevtoolBase): @@ -268,7 +364,7 @@ class DevtoolAddTests(DevtoolBase): self.track_for_cleanup(tempdir) pn = 'pv' pv = '1.5.3' - url = 'http://www.ivarch.com/programs/sources/pv-1.5.3.tar.bz2' + url = 'http://downloads.yoctoproject.org/mirror/sources/pv-1.5.3.tar.bz2' result = runCmd('wget %s' % url, cwd=tempdir) result = runCmd('tar xfv %s' % os.path.basename(url), cwd=tempdir) srcdir = os.path.join(tempdir, '%s-%s' % (pn, pv)) @@ -303,6 +399,38 @@ class DevtoolAddTests(DevtoolBase): bindir = bindir[1:] self.assertTrue(os.path.isfile(os.path.join(installdir, bindir, 'pv')), 'pv binary not found in D') + def test_devtool_add_binary(self): + # Create a binary package containing a known test file + tempdir = tempfile.mkdtemp(prefix='devtoolqa') + self.track_for_cleanup(tempdir) + pn = 'tst-bin' + pv = '1.0' + test_file_dir = "var/lib/%s/" % pn + test_file_name = "test_file" + test_file_content = "TEST CONTENT" + test_file_package_root = os.path.join(tempdir, pn) + test_file_dir_full = os.path.join(test_file_package_root, test_file_dir) + bb.utils.mkdirhier(test_file_dir_full) + with open(os.path.join(test_file_dir_full, test_file_name), "w") as f: + f.write(test_file_content) + bin_package_path = os.path.join(tempdir, "%s.tar.gz" % pn) + runCmd("tar czf %s -C %s ." % (bin_package_path, test_file_package_root)) + + # Test devtool add -b on the binary package + self.track_for_cleanup(self.workspacedir) + self.add_command_to_tearDown('bitbake -c cleansstate %s' % pn) + self.add_command_to_tearDown('bitbake-layers remove-layer */workspace') + result = runCmd('devtool add -b %s %s' % (pn, bin_package_path)) + self.assertExists(os.path.join(self.workspacedir, 'conf', 'layer.conf'), 'Workspace directory not created') + + # Build the resulting recipe + result = runCmd('devtool build %s' % pn) + installdir = get_bb_var('D', pn) + self.assertTrue(installdir, 'Could not query installdir variable') + + # Check that a known file from the binary package has indeed been installed + self.assertTrue(os.path.isfile(os.path.join(installdir, test_file_dir, test_file_name)), '%s not found in D' % test_file_name) + def test_devtool_add_git_local(self): # We need dbus built so that DEPENDS recognition works bitbake('dbus') @@ -335,15 +463,32 @@ class DevtoolAddTests(DevtoolBase): self.assertIn(srcdir, result.output) self.assertIn(recipefile, result.output) checkvars = {} - checkvars['LICENSE'] = 'GPLv2' + checkvars['LICENSE'] = 'GPL-2.0-only' checkvars['LIC_FILES_CHKSUM'] = 'file://COPYING;md5=b234ee4d69f5fce4486a80fdaf4a4263' checkvars['S'] = '${WORKDIR}/git' - checkvars['PV'] = '0.1+git${SRCPV}' - checkvars['SRC_URI'] = 'git://git.yoctoproject.org/git/dbus-wait;protocol=https' + checkvars['PV'] = '0.1+git' + checkvars['SRC_URI'] = 'git://git.yoctoproject.org/git/dbus-wait;protocol=https;branch=master' checkvars['SRCREV'] = srcrev checkvars['DEPENDS'] = set(['dbus']) self._test_recipe_contents(recipefile, checkvars, []) + def test_devtool_add_git_style1(self): + version = 'v3.1.0' + pn = 'mbedtls' + # this will trigger reformat_git_uri with branch parameter in url + git_url = "'git://git@github.com/ARMmbed/mbedtls.git;branch=mbedtls-2.28;protocol=https'" + resulting_src_uri = "git://git@github.com/ARMmbed/mbedtls.git;branch=mbedtls-2.28;protocol=https" + self._test_devtool_add_git_url(git_url, version, pn, resulting_src_uri) + + def test_devtool_add_git_style2(self): + version = 'v3.1.0' + srcrev = 'v3.1.0' + pn = 'mbedtls' + # this will trigger reformat_git_uri with branch parameter in url + git_url = "'git://git@github.com/ARMmbed/mbedtls.git;protocol=https'" + resulting_src_uri = "git://git@github.com/ARMmbed/mbedtls.git;protocol=https;branch=master" + self._test_devtool_add_git_url(git_url, version, pn, resulting_src_uri, srcrev) + def test_devtool_add_library(self): # Fetch source tempdir = tempfile.mkdtemp(prefix='devtoolqa') @@ -372,7 +517,7 @@ class DevtoolAddTests(DevtoolBase): recipefile = '%s/recipes/libftdi/libftdi_%s.bb' % (self.workspacedir, version) result = runCmd('recipetool setvar %s EXTRA_OECMAKE -- \'-DPYTHON_BINDINGS=OFF -DLIBFTDI_CMAKE_CONFIG_DIR=${datadir}/cmake/Modules\'' % recipefile) with open(recipefile, 'a') as f: - f.write('\nFILES_${PN}-dev += "${datadir}/cmake/Modules"\n') + f.write('\nFILES:${PN}-dev += "${datadir}/cmake/Modules"\n') # We don't have the ability to pick up this dependency automatically yet... f.write('\nDEPENDS += "libusb1"\n') f.write('\nTESTLIBOUTPUT = "${COMPONENTS_DIR}/${TUNE_PKGARCH}/${PN}/${libdir}"\n') @@ -404,7 +549,7 @@ class DevtoolAddTests(DevtoolBase): self.track_for_cleanup(self.workspacedir) self.add_command_to_tearDown('bitbake -c cleansstate %s' % testrecipe) self.add_command_to_tearDown('bitbake-layers remove-layer */workspace') - result = runCmd('devtool add %s %s -f %s' % (testrecipe, srcdir, url)) + result = runCmd('devtool add --no-pypi %s %s -f %s' % (testrecipe, srcdir, url)) self.assertExists(os.path.join(self.workspacedir, 'conf', 'layer.conf'), 'Workspace directory not created. %s' % result.output) self.assertTrue(os.path.isfile(os.path.join(srcdir, 'setup.py')), 'Unable to find setup.py in source directory') self.assertTrue(os.path.isdir(os.path.join(srcdir, '.git')), 'git repository for external source tree was not created') @@ -423,7 +568,7 @@ class DevtoolAddTests(DevtoolBase): result = runCmd('devtool reset -n %s' % testrecipe) shutil.rmtree(srcdir) fakever = '1.9' - result = runCmd('devtool add %s %s -f %s -V %s' % (testrecipe, srcdir, url, fakever)) + result = runCmd('devtool add --no-pypi %s %s -f %s -V %s' % (testrecipe, srcdir, url, fakever)) self.assertTrue(os.path.isfile(os.path.join(srcdir, 'setup.py')), 'Unable to find setup.py in source directory') # Test devtool status result = runCmd('devtool status') @@ -441,6 +586,7 @@ class DevtoolAddTests(DevtoolBase): tempdir = tempfile.mkdtemp(prefix='devtoolqa') self.track_for_cleanup(tempdir) url = 'gitsm://git.yoctoproject.org/mraa' + url_branch = '%s;branch=master' % url checkrev = 'ae127b19a50aa54255e4330ccfdd9a5d058e581d' testrecipe = 'mraa' srcdir = os.path.join(tempdir, testrecipe) @@ -460,8 +606,8 @@ class DevtoolAddTests(DevtoolBase): self.assertIn('_git.bb', recipefile, 'Recipe file incorrectly named') checkvars = {} checkvars['S'] = '${WORKDIR}/git' - checkvars['PV'] = '1.0+git${SRCPV}' - checkvars['SRC_URI'] = url + checkvars['PV'] = '1.0+git' + checkvars['SRC_URI'] = url_branch checkvars['SRCREV'] = '${AUTOREV}' self._test_recipe_contents(recipefile, checkvars, []) # Try with revision and version specified @@ -479,8 +625,8 @@ class DevtoolAddTests(DevtoolBase): self.assertIn('_git.bb', recipefile, 'Recipe file incorrectly named') checkvars = {} checkvars['S'] = '${WORKDIR}/git' - checkvars['PV'] = '1.5+git${SRCPV}' - checkvars['SRC_URI'] = url + checkvars['PV'] = '1.5+git' + checkvars['SRC_URI'] = url_branch checkvars['SRCREV'] = checkrev self._test_recipe_contents(recipefile, checkvars, []) @@ -503,7 +649,7 @@ class DevtoolAddTests(DevtoolBase): result = runCmd('devtool status') self.assertIn(testrecipe, result.output) self.assertIn(srcdir, result.output) - # Check recipe + # Check recipedevtool add recipefile = get_bb_var('FILE', testrecipe) self.assertIn('%s_%s.bb' % (testrecipe, testver), recipefile, 'Recipe file incorrectly named') checkvars = {} @@ -511,6 +657,43 @@ class DevtoolAddTests(DevtoolBase): checkvars['SRC_URI'] = url.replace(testver, '${PV}') self._test_recipe_contents(recipefile, checkvars, []) + def test_devtool_add_npm(self): + collections = get_bb_var('BBFILE_COLLECTIONS').split() + if "openembedded-layer" not in collections: + self.skipTest("Test needs meta-oe for nodejs") + + pn = 'savoirfairelinux-node-server-example' + pv = '1.0.0' + url = 'npm://registry.npmjs.org;package=@savoirfairelinux/node-server-example;version=' + pv + # Test devtool add + self.track_for_cleanup(self.workspacedir) + self.add_command_to_tearDown('bitbake -c cleansstate %s' % pn) + self.add_command_to_tearDown('bitbake-layers remove-layer */workspace') + result = runCmd('devtool add \'%s\'' % url) + self.assertExists(os.path.join(self.workspacedir, 'conf', 'layer.conf'), 'Workspace directory not created') + self.assertExists(os.path.join(self.workspacedir, 'recipes', pn, '%s_%s.bb' % (pn, pv)), 'Recipe not created') + self.assertExists(os.path.join(self.workspacedir, 'recipes', pn, pn, 'npm-shrinkwrap.json'), 'Shrinkwrap not created') + # Test devtool status + result = runCmd('devtool status') + self.assertIn(pn, result.output) + # Clean up anything in the workdir/sysroot/sstate cache (have to do this *after* devtool add since the recipe only exists then) + bitbake('%s -c cleansstate' % pn) + # Test devtool build + result = runCmd('devtool build %s' % pn) + + def test_devtool_add_python_egg_requires(self): + # Fetch source + tempdir = tempfile.mkdtemp(prefix='devtoolqa') + self.track_for_cleanup(tempdir) + testver = '0.14.0' + url = 'https://files.pythonhosted.org/packages/e9/9e/25d59f5043cf763833b2581c8027fa92342c4cf8ee523b498ecdf460c16d/uvicorn-%s.tar.gz' % testver + testrecipe = 'python3-uvicorn' + srcdir = os.path.join(tempdir, testrecipe) + # Test devtool add + self.track_for_cleanup(self.workspacedir) + self.add_command_to_tearDown('bitbake-layers remove-layer */workspace') + result = runCmd('devtool add %s %s -f %s' % (testrecipe, srcdir, url)) + class DevtoolModifyTests(DevtoolBase): def test_devtool_modify(self): @@ -570,6 +753,25 @@ class DevtoolModifyTests(DevtoolBase): result = runCmd('devtool status') self.assertNotIn('mdadm', result.output) + def test_devtool_modify_go(self): + import oe.path + from tempfile import TemporaryDirectory + with TemporaryDirectory(prefix='devtoolqa') as tempdir: + self.track_for_cleanup(self.workspacedir) + self.add_command_to_tearDown('bitbake -c clean go-helloworld') + self.add_command_to_tearDown('bitbake-layers remove-layer */workspace') + result = runCmd('devtool modify go-helloworld -x %s' % tempdir) + self.assertExists( + oe.path.join(tempdir, 'src', 'golang.org', 'x', 'example', 'go.mod'), + 'Extracted source could not be found' + ) + self.assertExists( + oe.path.join(self.workspacedir, 'conf', 'layer.conf'), + 'Workspace directory not created' + ) + matches = glob.glob(oe.path.join(self.workspacedir, 'appends', 'go-helloworld_*.bbappend')) + self.assertTrue(matches, 'bbappend not created %s' % result.output) + def test_devtool_buildclean(self): def assertFile(path, *paths): f = os.path.join(path, *paths) @@ -624,7 +826,7 @@ class DevtoolModifyTests(DevtoolBase): self.track_for_cleanup(self.workspacedir) self.add_command_to_tearDown('bitbake-layers remove-layer */workspace') - testrecipes = 'perf kernel-devsrc package-index core-image-minimal meta-toolchain packagegroup-core-sdk meta-ide-support'.split() + testrecipes = 'perf kernel-devsrc package-index core-image-minimal meta-toolchain packagegroup-core-sdk'.split() # Find actual name of gcc-source since it now includes the version - crude, but good enough for this purpose result = runCmd('bitbake-layers show-recipes gcc-source*') for line in result.output.splitlines(): @@ -656,7 +858,7 @@ class DevtoolModifyTests(DevtoolBase): bbclassextended = False inheritnative = False - testrecipes = 'mtools-native apt-native desktop-file-utils-native'.split() + testrecipes = 'cdrtools-native mtools-native apt-native desktop-file-utils-native'.split() for testrecipe in testrecipes: checkextend = 'native' in (get_bb_var('BBCLASSEXTEND', testrecipe) or '').split() if not bbclassextended: @@ -673,6 +875,39 @@ class DevtoolModifyTests(DevtoolBase): self.assertTrue(bbclassextended, 'None of these recipes are BBCLASSEXTENDed to native - need to adjust testrecipes list: %s' % ', '.join(testrecipes)) self.assertTrue(inheritnative, 'None of these recipes do "inherit native" - need to adjust testrecipes list: %s' % ', '.join(testrecipes)) + def test_devtool_modify_localfiles_only(self): + # Check preconditions + testrecipe = 'base-files' + src_uri = (get_bb_var('SRC_URI', testrecipe) or '').split() + foundlocalonly = False + correct_symlink = False + for item in src_uri: + if item.startswith('file://'): + if '.patch' not in item: + foundlocalonly = True + else: + foundlocalonly = False + break + self.assertTrue(foundlocalonly, 'This test expects the %s recipe to fetch local files only and it seems that it no longer does' % testrecipe) + # Clean up anything in the workdir/sysroot/sstate cache + bitbake('%s -c cleansstate' % testrecipe) + # Try modifying a recipe + tempdir = tempfile.mkdtemp(prefix='devtoolqa') + self.track_for_cleanup(tempdir) + self.track_for_cleanup(self.workspacedir) + self.add_command_to_tearDown('bitbake -c clean %s' % testrecipe) + self.add_command_to_tearDown('bitbake-layers remove-layer */workspace') + result = runCmd('devtool modify %s -x %s' % (testrecipe, tempdir)) + srcfile = os.path.join(tempdir, 'share/dot.bashrc') + self.assertExists(srcfile, 'Extracted source could not be found') + matches = glob.glob(os.path.join(self.workspacedir, 'appends', '%s_*.bbappend' % testrecipe)) + self.assertTrue(matches, 'bbappend not created') + # Test devtool status + result = runCmd('devtool status') + self.assertIn(testrecipe, result.output) + self.assertIn(tempdir, result.output) + # Try building + bitbake(testrecipe) def test_devtool_modify_git(self): # Check preconditions @@ -701,6 +936,122 @@ class DevtoolModifyTests(DevtoolBase): # Try building bitbake(testrecipe) + def test_devtool_modify_git_no_extract(self): + # Check preconditions + testrecipe = 'psplash' + src_uri = get_bb_var('SRC_URI', testrecipe) + self.assertIn('git://', src_uri, 'This test expects the %s recipe to be a git recipe' % testrecipe) + # Clean up anything in the workdir/sysroot/sstate cache + bitbake('%s -c cleansstate' % testrecipe) + # Try modifying a recipe + tempdir = tempfile.mkdtemp(prefix='devtoolqa') + self.track_for_cleanup(tempdir) + self.track_for_cleanup(self.workspacedir) + self.add_command_to_tearDown('bitbake -c clean %s' % testrecipe) + self.add_command_to_tearDown('bitbake-layers remove-layer */workspace') + result = runCmd('git clone https://git.yoctoproject.org/psplash %s && devtool modify -n %s %s' % (tempdir, testrecipe, tempdir)) + self.assertExists(os.path.join(self.workspacedir, 'conf', 'layer.conf'), 'Workspace directory not created. devtool output: %s' % result.output) + matches = glob.glob(os.path.join(self.workspacedir, 'appends', 'psplash_*.bbappend')) + self.assertTrue(matches, 'bbappend not created') + # Test devtool status + result = runCmd('devtool status') + self.assertIn(testrecipe, result.output) + self.assertIn(tempdir, result.output) + + def test_devtool_modify_git_crates_subpath(self): + # This tests two things in devtool context: + # - that we support local git dependencies for cargo based recipe + # - that we support patches in SRC_URI when git url contains subpath parameter + + # Check preconditions: + # recipe inherits cargo + # git:// uri with a subpath as the main package + # some crate:// in SRC_URI + # others git:// in SRC_URI + # cointains a patch + testrecipe = 'hello-rs' + bb_vars = get_bb_vars(['SRC_URI', 'FILE', 'UNPACKDIR', 'CARGO_HOME'], testrecipe) + recipefile = bb_vars['FILE'] + unpackdir = bb_vars['UNPACKDIR'] + cargo_home = bb_vars['CARGO_HOME'] + src_uri = bb_vars['SRC_URI'].split() + self.assertTrue(src_uri[0].startswith('git://'), + 'This test expects the %s recipe to have a git repo has its main uri' % testrecipe) + self.assertIn(';subpath=', src_uri[0], + 'This test expects the %s recipe to have a git uri with subpath' % testrecipe) + self.assertTrue(any([uri.startswith('crate://') for uri in src_uri]), + 'This test expects the %s recipe to have some crates in its src uris' % testrecipe) + self.assertGreaterEqual(sum(map(lambda x:x.startswith('git://'), src_uri)), 2, + 'This test expects the %s recipe to have several git:// uris' % testrecipe) + self.assertTrue(any([uri.startswith('file://') and '.patch' in uri for uri in src_uri]), + 'This test expects the %s recipe to have a patch in its src uris' % testrecipe) + + self._test_recipe_contents(recipefile, {}, ['ptest-cargo']) + + # Clean up anything in the workdir/sysroot/sstate cache + bitbake('%s -c cleansstate' % testrecipe) + # Try modifying a recipe + tempdir = tempfile.mkdtemp(prefix='devtoolqa') + self.track_for_cleanup(tempdir) + self.track_for_cleanup(self.workspacedir) + self.add_command_to_tearDown('bitbake -c clean %s' % testrecipe) + self.add_command_to_tearDown('bitbake-layers remove-layer */workspace') + result = runCmd('devtool modify %s -x %s' % (testrecipe, tempdir)) + self.assertExists(os.path.join(tempdir, 'Cargo.toml'), 'Extracted source could not be found') + self.assertExists(os.path.join(self.workspacedir, 'conf', 'layer.conf'), 'Workspace directory not created. devtool output: %s' % result.output) + matches = glob.glob(os.path.join(self.workspacedir, 'appends', '%s_*.bbappend' % testrecipe)) + self.assertTrue(matches, 'bbappend not created') + # Test devtool status + result = runCmd('devtool status') + self.assertIn(testrecipe, result.output) + self.assertIn(tempdir, result.output) + # Check git repo + self._check_src_repo(tempdir) + # Check that the patch is correctly applied. + # The last commit message in the tree must contain the following note: + # Notes (devtool): + # original patch: <patchname> + # .. + patchname = None + for uri in src_uri: + if uri.startswith('file://') and '.patch' in uri: + patchname = uri.replace("file://", "").partition('.patch')[0] + '.patch' + self.assertIsNotNone(patchname) + result = runCmd('git -C %s log -1' % tempdir) + self.assertIn("Notes (devtool):\n original patch: %s" % patchname, result.output) + + # Configure the recipe to check that the git dependencies are correctly patched in cargo config + bitbake('-c configure %s' % testrecipe) + + cargo_config_path = os.path.join(cargo_home, 'config') + with open(cargo_config_path, "r") as f: + cargo_config_contents = [line.strip('\n') for line in f.readlines()] + + # Get back git dependencies of the recipe (ignoring the main one) + # and check that they are all correctly patched to be fetched locally + git_deps = [uri for uri in src_uri if uri.startswith("git://")][1:] + for git_dep in git_deps: + raw_url, _, raw_parms = git_dep.partition(";") + parms = {} + for parm in raw_parms.split(";"): + name_parm, _, value_parm = parm.partition('=') + parms[name_parm]=value_parm + self.assertIn('protocol', parms, 'git dependencies uri should contain the "protocol" parameter') + self.assertIn('name', parms, 'git dependencies uri should contain the "name" parameter') + self.assertIn('destsuffix', parms, 'git dependencies uri should contain the "destsuffix" parameter') + self.assertIn('type', parms, 'git dependencies uri should contain the "type" parameter') + self.assertEqual(parms['type'], 'git-dependency', 'git dependencies uri should have "type=git-dependency"') + raw_url = raw_url.replace("git://", '%s://' % parms['protocol']) + patch_line = '[patch."%s"]' % raw_url + path_patched = os.path.join(unpackdir, parms['destsuffix']) + path_override_line = '%s = { path = "%s" }' % (parms['name'], path_patched) + # Would have been better to use tomllib to read this file :/ + self.assertIn(patch_line, cargo_config_contents) + self.assertIn(path_override_line, cargo_config_contents) + + # Try to package the recipe + bitbake('-c package_qa %s' % testrecipe) + def test_devtool_modify_localfiles(self): # Check preconditions testrecipe = 'lighttpd' @@ -752,6 +1103,57 @@ class DevtoolModifyTests(DevtoolBase): self._check_src_repo(tempdir) # This is probably sufficient + def test_devtool_modify_overrides(self): + # Try modifying a recipe with patches in overrides + tempdir = tempfile.mkdtemp(prefix='devtoolqa') + self.track_for_cleanup(tempdir) + self.track_for_cleanup(self.workspacedir) + self.add_command_to_tearDown('bitbake-layers remove-layer */workspace') + result = runCmd('devtool modify devtool-patch-overrides -x %s' % (tempdir)) + + self._check_src_repo(tempdir) + source = os.path.join(tempdir, "source") + def check(branch, expected): + runCmd('git -C %s checkout %s' % (tempdir, branch)) + with open(source, "rt") as f: + content = f.read() + self.assertEqual(content, expected) + if self.td["MACHINE"] == "qemux86": + check('devtool', 'This is a test for qemux86\n') + elif self.td["MACHINE"] == "qemuarm": + check('devtool', 'This is a test for qemuarm\n') + else: + check('devtool', 'This is a test for something\n') + check('devtool-no-overrides', 'This is a test for something\n') + check('devtool-override-qemuarm', 'This is a test for qemuarm\n') + check('devtool-override-qemux86', 'This is a test for qemux86\n') + + def test_devtool_modify_multiple_sources(self): + # This test check that recipes fetching several sources can be used with devtool modify/build + # Check preconditions + testrecipe = 'bzip2' + src_uri = get_bb_var('SRC_URI', testrecipe) + src1 = 'https://' in src_uri + src2 = 'git://' in src_uri + 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) + # Clean up anything in the workdir/sysroot/sstate cache + bitbake('%s -c cleansstate' % testrecipe) + # Try modifying a recipe + tempdir = tempfile.mkdtemp(prefix='devtoolqa') + self.track_for_cleanup(tempdir) + self.track_for_cleanup(self.workspacedir) + self.add_command_to_tearDown('bitbake -c clean %s' % testrecipe) + self.add_command_to_tearDown('bitbake-layers remove-layer */workspace') + result = runCmd('devtool modify %s -x %s' % (testrecipe, tempdir)) + self.assertEqual(result.status, 0, "Could not modify recipe %s. Output: %s" % (testrecipe, result.output)) + # Test devtool status + result = runCmd('devtool status') + self.assertIn(testrecipe, result.output) + self.assertIn(tempdir, result.output) + # Try building + result = bitbake(testrecipe) + self.assertEqual(result.status, 0, "Bitbake failed, exit code %s, output %s" % (result.status, result.output)) + class DevtoolUpdateTests(DevtoolBase): def test_devtool_update_recipe(self): @@ -781,14 +1183,15 @@ class DevtoolUpdateTests(DevtoolBase): result = runCmd('git commit -m "Add a new file"', cwd=tempdir) self.add_command_to_tearDown('cd %s; rm %s/*.patch; git checkout %s %s' % (os.path.dirname(recipefile), testrecipe, testrecipe, os.path.basename(recipefile))) result = runCmd('devtool update-recipe %s' % testrecipe) + result = runCmd('git add minicom', cwd=os.path.dirname(recipefile)) expected_status = [(' M', '.*/%s$' % os.path.basename(recipefile)), - ('??', '.*/0001-Change-the-README.patch$'), - ('??', '.*/0002-Add-a-new-file.patch$')] + ('A ', '.*/0001-Change-the-README.patch$'), + ('A ', '.*/0002-Add-a-new-file.patch$')] self._check_repo_status(os.path.dirname(recipefile), expected_status) def test_devtool_update_recipe_git(self): # Check preconditions - testrecipe = 'mtd-utils' + testrecipe = 'mtd-utils-selftest' bb_vars = get_bb_vars(['FILE', 'SRC_URI'], testrecipe) recipefile = bb_vars['FILE'] src_uri = bb_vars['SRC_URI'] @@ -822,28 +1225,12 @@ class DevtoolUpdateTests(DevtoolBase): self._check_repo_status(os.path.dirname(recipefile), expected_status) result = runCmd('git diff %s' % os.path.basename(recipefile), cwd=os.path.dirname(recipefile)) - addlines = ['SRCREV = ".*"', 'SRC_URI = "git://git.infradead.org/mtd-utils.git"'] + addlines = ['SRCREV = ".*"', 'SRC_URI = "git://git.infradead.org/mtd-utils.git;branch=master"'] srcurilines = src_uri.split() srcurilines[0] = 'SRC_URI = "' + srcurilines[0] srcurilines.append('"') removelines = ['SRCREV = ".*"'] + srcurilines - for line in result.output.splitlines(): - if line.startswith('+++') or line.startswith('---'): - continue - elif line.startswith('+'): - matched = False - for item in addlines: - if re.match(item, line[1:].strip()): - matched = True - break - self.assertTrue(matched, 'Unexpected diff add line: %s' % line) - elif line.startswith('-'): - matched = False - for item in removelines: - if re.match(item, line[1:].strip()): - matched = True - break - self.assertTrue(matched, 'Unexpected diff remove line: %s' % line) + self._check_diff(result.output, addlines, removelines) # Now try with auto mode runCmd('cd %s; git checkout %s %s' % (os.path.dirname(recipefile), testrecipe, os.path.basename(recipefile))) result = runCmd('devtool update-recipe %s' % testrecipe) @@ -893,7 +1280,7 @@ class DevtoolUpdateTests(DevtoolBase): self.assertExists(patchfile, 'Patch file not created') # Check bbappend contents - expectedlines = ['FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:"\n', + expectedlines = ['FILESEXTRAPATHS:prepend := "${THISDIR}/${PN}:"\n', '\n', 'SRC_URI += "file://0001-Add-our-custom-version.patch"\n', '\n'] @@ -905,15 +1292,16 @@ class DevtoolUpdateTests(DevtoolBase): with open(bbappendfile, 'r') as f: self.assertEqual(expectedlines, f.readlines()) # Drop new commit and check patch gets deleted - result = runCmd('git reset HEAD^', cwd=tempsrcdir) + result = runCmd('git reset HEAD^ --hard', cwd=tempsrcdir) result = runCmd('devtool update-recipe %s -a %s' % (testrecipe, templayerdir)) self.assertNotExists(patchfile, 'Patch file not deleted') - expectedlines2 = ['FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:"\n', + expectedlines2 = ['FILESEXTRAPATHS:prepend := "${THISDIR}/${PN}:"\n', '\n'] with open(bbappendfile, 'r') as f: self.assertEqual(expectedlines2, f.readlines()) # Put commit back and check we can run it if layer isn't in bblayers.conf os.remove(bbappendfile) + result = runCmd("sed 's!\\(#define VERSION\\W*\"[^\"]*\\)\"!\\1-custom\"!' -i ReadMe.c", cwd=tempsrcdir) result = runCmd('git commit -a -m "Add our custom version"', cwd=tempsrcdir) result = runCmd('bitbake-layers remove-layer %s' % templayerdir, cwd=self.builddir) result = runCmd('devtool update-recipe %s -a %s' % (testrecipe, templayerdir)) @@ -925,10 +1313,11 @@ class DevtoolUpdateTests(DevtoolBase): def test_devtool_update_recipe_append_git(self): # Check preconditions - testrecipe = 'mtd-utils' - bb_vars = get_bb_vars(['FILE', 'SRC_URI'], testrecipe) + testrecipe = 'mtd-utils-selftest' + bb_vars = get_bb_vars(['FILE', 'SRC_URI', 'LAYERSERIES_CORENAMES'], testrecipe) recipefile = bb_vars['FILE'] src_uri = bb_vars['SRC_URI'] + corenames = bb_vars['LAYERSERIES_CORENAMES'] self.assertIn('git://', src_uri, 'This test expects the %s recipe to be a git recipe' % testrecipe) for entry in src_uri.split(): if entry.startswith('git://'): @@ -959,7 +1348,7 @@ class DevtoolUpdateTests(DevtoolBase): f.write('BBFILE_PATTERN_oeselftesttemplayer = "^${LAYERDIR}/"\n') f.write('BBFILE_PRIORITY_oeselftesttemplayer = "999"\n') f.write('BBFILE_PATTERN_IGNORE_EMPTY_oeselftesttemplayer = "1"\n') - f.write('LAYERSERIES_COMPAT_oeselftesttemplayer = "${LAYERSERIES_COMPAT_core}"\n') + f.write('LAYERSERIES_COMPAT_oeselftesttemplayer = "%s"\n' % corenames) self.add_command_to_tearDown('bitbake-layers remove-layer %s || true' % templayerdir) result = runCmd('bitbake-layers add-layer %s' % templayerdir, cwd=self.builddir) # Create the bbappend @@ -987,7 +1376,7 @@ class DevtoolUpdateTests(DevtoolBase): with open(bbappendfile, 'r') as f: self.assertEqual(expectedlines, set(f.readlines())) # Drop new commit and check SRCREV changes - result = runCmd('git reset HEAD^', cwd=tempsrcdir) + result = runCmd('git reset HEAD^ --hard', cwd=tempsrcdir) result = runCmd('devtool update-recipe -m srcrev %s -a %s' % (testrecipe, templayerdir)) self.assertNotExists(os.path.join(appenddir, testrecipe), 'Patch directory should not be created') result = runCmd('git rev-parse HEAD', cwd=tempsrcdir) @@ -999,6 +1388,7 @@ class DevtoolUpdateTests(DevtoolBase): self.assertEqual(expectedlines, set(f.readlines())) # Put commit back and check we can run it if layer isn't in bblayers.conf os.remove(bbappendfile) + result = runCmd('echo "# Additional line" >> Makefile.am', cwd=tempsrcdir) result = runCmd('git commit -a -m "Change the Makefile"', cwd=tempsrcdir) result = runCmd('bitbake-layers remove-layer %s' % templayerdir, cwd=self.builddir) result = runCmd('devtool update-recipe -m srcrev %s -a %s' % (testrecipe, templayerdir)) @@ -1030,22 +1420,39 @@ class DevtoolUpdateTests(DevtoolBase): # Try building just to ensure we haven't broken that bitbake("%s" % testrecipe) # Edit / commit local source - runCmd('echo "/* Foobar */" >> oe-local-files/makedevs.c', cwd=tempdir) - runCmd('echo "Foo" > oe-local-files/new-local', cwd=tempdir) + runCmd('echo "/* Foobar */" >> makedevs.c', cwd=tempdir) + runCmd('echo "Foo" > new-local', cwd=tempdir) runCmd('echo "Bar" > new-file', cwd=tempdir) runCmd('git add new-file', cwd=tempdir) runCmd('git commit -m "Add new file"', cwd=tempdir) - self.add_command_to_tearDown('cd %s; git clean -fd .; git checkout .' % - os.path.dirname(recipefile)) + runCmd('git add new-local', cwd=tempdir) runCmd('devtool update-recipe %s' % testrecipe) expected_status = [(' M', '.*/%s$' % os.path.basename(recipefile)), (' M', '.*/makedevs/makedevs.c$'), ('??', '.*/makedevs/new-local$'), ('??', '.*/makedevs/0001-Add-new-file.patch$')] self._check_repo_status(os.path.dirname(recipefile), expected_status) - - def test_devtool_update_recipe_local_files_2(self): - """Check local source files support when oe-local-files is in Git""" + # Now try to update recipe in another layer, so first, clean it + runCmd('cd %s; git clean -fd .; git checkout .' % os.path.dirname(recipefile)) + # Create a temporary layer and add it to bblayers.conf + self._create_temp_layer(templayerdir, True, 'templayer') + # Update recipe in templayer + result = runCmd('devtool update-recipe %s -a %s' % (testrecipe, templayerdir)) + self.assertNotIn('WARNING:', result.output) + # Check recipe is still clean + self._check_repo_status(os.path.dirname(recipefile), []) + splitpath = os.path.dirname(recipefile).split(os.sep) + appenddir = os.path.join(templayerdir, splitpath[-2], splitpath[-1]) + bbappendfile = self._check_bbappend(testrecipe, recipefile, appenddir) + patchfile = os.path.join(appenddir, testrecipe, '0001-Add-new-file.patch') + new_local_file = os.path.join(appenddir, testrecipe, 'new_local') + local_file = os.path.join(appenddir, testrecipe, 'makedevs.c') + self.assertExists(patchfile, 'Patch file 0001-Add-new-file.patch not created') + self.assertExists(local_file, 'File makedevs.c not created') + self.assertExists(patchfile, 'File new_local not created') + + def _test_devtool_update_recipe_local_files_2(self): + """Check local source files support when editing local files in Git""" testrecipe = 'devtool-test-local' recipefile = get_bb_var('FILE', testrecipe) recipedir = os.path.dirname(recipefile) @@ -1060,17 +1467,13 @@ class DevtoolUpdateTests(DevtoolBase): result = runCmd('devtool modify %s -x %s' % (testrecipe, tempdir)) # Check git repo self._check_src_repo(tempdir) - # Add oe-local-files to Git - runCmd('rm oe-local-files/.gitignore', cwd=tempdir) - runCmd('git add oe-local-files', cwd=tempdir) - runCmd('git commit -m "Add local sources"', cwd=tempdir) # Edit / commit local sources - runCmd('echo "# Foobar" >> oe-local-files/file1', cwd=tempdir) + runCmd('echo "# Foobar" >> file1', cwd=tempdir) runCmd('git commit -am "Edit existing file"', cwd=tempdir) - runCmd('git rm oe-local-files/file2', cwd=tempdir) + runCmd('git rm file2', cwd=tempdir) runCmd('git commit -m"Remove file"', cwd=tempdir) - runCmd('echo "Foo" > oe-local-files/new-local', cwd=tempdir) - runCmd('git add oe-local-files/new-local', cwd=tempdir) + runCmd('echo "Foo" > new-local', cwd=tempdir) + runCmd('git add new-local', cwd=tempdir) runCmd('git commit -m "Add new local file"', cwd=tempdir) runCmd('echo "Gar" > new-file', cwd=tempdir) runCmd('git add new-file', cwd=tempdir) @@ -1079,7 +1482,7 @@ class DevtoolUpdateTests(DevtoolBase): os.path.dirname(recipefile)) # Checkout unmodified file to working copy -> devtool should still pick # the modified version from HEAD - runCmd('git checkout HEAD^ -- oe-local-files/file1', cwd=tempdir) + runCmd('git checkout HEAD^ -- file1', cwd=tempdir) runCmd('devtool update-recipe %s' % testrecipe) expected_status = [(' M', '.*/%s$' % os.path.basename(recipefile)), (' M', '.*/file1$'), @@ -1088,6 +1491,59 @@ class DevtoolUpdateTests(DevtoolBase): ('??', '.*/0001-Add-new-file.patch$')] self._check_repo_status(os.path.dirname(recipefile), expected_status) + def test_devtool_update_recipe_with_gitignore(self): + # First, modify the recipe + testrecipe = 'devtool-test-ignored' + bb_vars = get_bb_vars(['FILE'], testrecipe) + recipefile = bb_vars['FILE'] + patchfile = os.path.join(os.path.dirname(recipefile), testrecipe, testrecipe + '.patch') + newpatchfile = os.path.join(os.path.dirname(recipefile), testrecipe, testrecipe + '.patch.expected') + tempdir = tempfile.mkdtemp(prefix='devtoolqa') + self.track_for_cleanup(tempdir) + self.track_for_cleanup(self.workspacedir) + self.add_command_to_tearDown('bitbake-layers remove-layer */workspace') + # (don't bother with cleaning the recipe on teardown, we won't be building it) + result = runCmd('devtool modify %s' % testrecipe) + self.add_command_to_tearDown('cd %s; rm %s/*; git checkout %s %s' % (os.path.dirname(recipefile), testrecipe, testrecipe, os.path.basename(recipefile))) + result = runCmd('devtool finish --force-patch-refresh %s meta-selftest' % testrecipe) + # Check recipe got changed as expected + with open(newpatchfile, 'r') as f: + desiredlines = f.readlines() + with open(patchfile, 'r') as f: + newlines = f.readlines() + # Ignore the initial lines, because oe-selftest creates own meta-selftest repo + # which changes the metadata subject which is added into the patch, but keep + # .patch.expected as it is in case someone runs devtool finish --force-patch-refresh + # devtool-test-ignored manually, then it should generate exactly the same .patch file + self.assertEqual(desiredlines[5:], newlines[5:]) + + def test_devtool_update_recipe_long_filename(self): + # First, modify the recipe + testrecipe = 'devtool-test-long-filename' + bb_vars = get_bb_vars(['FILE'], testrecipe) + recipefile = bb_vars['FILE'] + patchfilename = '0001-I-ll-patch-you-only-if-devtool-lets-me-to-do-it-corr.patch' + patchfile = os.path.join(os.path.dirname(recipefile), testrecipe, patchfilename) + newpatchfile = os.path.join(os.path.dirname(recipefile), testrecipe, patchfilename + '.expected') + tempdir = tempfile.mkdtemp(prefix='devtoolqa') + self.track_for_cleanup(tempdir) + self.track_for_cleanup(self.workspacedir) + self.add_command_to_tearDown('bitbake-layers remove-layer */workspace') + # (don't bother with cleaning the recipe on teardown, we won't be building it) + result = runCmd('devtool modify %s' % testrecipe) + self.add_command_to_tearDown('cd %s; rm %s/*; git checkout %s %s' % (os.path.dirname(recipefile), testrecipe, testrecipe, os.path.basename(recipefile))) + result = runCmd('devtool finish --force-patch-refresh %s meta-selftest' % testrecipe) + # Check recipe got changed as expected + with open(newpatchfile, 'r') as f: + desiredlines = f.readlines() + with open(patchfile, 'r') as f: + newlines = f.readlines() + # Ignore the initial lines, because oe-selftest creates own meta-selftest repo + # which changes the metadata subject which is added into the patch, but keep + # .patch.expected as it is in case someone runs devtool finish --force-patch-refresh + # devtool-test-ignored manually, then it should generate exactly the same .patch file + self.assertEqual(desiredlines[5:], newlines[5:]) + def test_devtool_update_recipe_local_files_3(self): # First, modify the recipe testrecipe = 'devtool-test-localonly' @@ -1101,7 +1557,7 @@ class DevtoolUpdateTests(DevtoolBase): # (don't bother with cleaning the recipe on teardown, we won't be building it) result = runCmd('devtool modify %s' % testrecipe) # Modify one file - runCmd('echo "Another line" >> file2', cwd=os.path.join(self.workspacedir, 'sources', testrecipe, 'oe-local-files')) + runCmd('echo "Another line" >> file2', cwd=os.path.join(self.workspacedir, 'sources', testrecipe)) self.add_command_to_tearDown('cd %s; rm %s/*; git checkout %s %s' % (os.path.dirname(recipefile), testrecipe, testrecipe, os.path.basename(recipefile))) result = runCmd('devtool update-recipe %s' % testrecipe) expected_status = [(' M', '.*/%s/file2$' % testrecipe)] @@ -1124,7 +1580,7 @@ class DevtoolUpdateTests(DevtoolBase): # Modify one file srctree = os.path.join(self.workspacedir, 'sources', testrecipe) runCmd('echo "Another line" >> README', cwd=srctree) - runCmd('git commit -a --amend --no-edit', cwd=srctree) + runCmd('git commit -a --amend --no-edit --no-verify', cwd=srctree) self.add_command_to_tearDown('cd %s; rm %s/*; git checkout %s %s' % (os.path.dirname(recipefile), testrecipe, testrecipe, os.path.basename(recipefile))) result = runCmd('devtool update-recipe %s' % testrecipe) expected_status = [(' M', '.*/%s/readme.patch.gz$' % testrecipe)] @@ -1160,6 +1616,121 @@ class DevtoolUpdateTests(DevtoolBase): expected_status = [] self._check_repo_status(os.path.dirname(recipefile), expected_status) + def test_devtool_finish_modify_git_subdir(self): + # Check preconditions + testrecipe = 'dos2unix' + self.append_config('ERROR_QA:remove:pn-dos2unix = "patch-status"\n') + bb_vars = get_bb_vars(['SRC_URI', 'S', 'WORKDIR', 'FILE'], testrecipe) + self.assertIn('git://', bb_vars['SRC_URI'], 'This test expects the %s recipe to be a git recipe' % testrecipe) + workdir_git = '%s/git/' % bb_vars['WORKDIR'] + if not bb_vars['S'].startswith(workdir_git): + self.fail('This test expects the %s recipe to be building from a subdirectory of the git repo' % testrecipe) + subdir = bb_vars['S'].split(workdir_git, 1)[1] + # Clean up anything in the workdir/sysroot/sstate cache + bitbake('%s -c cleansstate' % testrecipe) + # Try modifying a recipe + tempdir = tempfile.mkdtemp(prefix='devtoolqa') + self.track_for_cleanup(tempdir) + self.track_for_cleanup(self.workspacedir) + self.add_command_to_tearDown('bitbake -c clean %s' % testrecipe) + self.add_command_to_tearDown('bitbake-layers remove-layer */workspace') + result = runCmd('devtool modify %s -x %s' % (testrecipe, tempdir)) + testsrcfile = os.path.join(tempdir, subdir, 'dos2unix.c') + self.assertExists(testsrcfile, 'Extracted source could not be found') + self.assertExists(os.path.join(self.workspacedir, 'conf', 'layer.conf'), 'Workspace directory not created. devtool output: %s' % result.output) + self.assertNotExists(os.path.join(tempdir, subdir, '.git'), 'Subdirectory has been initialised as a git repo') + # Check git repo + self._check_src_repo(tempdir) + # Modify file + runCmd("sed -i '1s:^:/* Add a comment */\\n:' %s" % testsrcfile) + result = runCmd('git commit -a -m "Add a comment"', cwd=tempdir) + # Now try updating original recipe + recipefile = bb_vars['FILE'] + recipedir = os.path.dirname(recipefile) + self.add_command_to_tearDown('cd %s; rm -f %s/*.patch; git checkout .' % (recipedir, testrecipe)) + result = runCmd('devtool update-recipe %s' % testrecipe) + expected_status = [(' M', '.*/%s$' % os.path.basename(recipefile)), + ('??', '.*/%s/%s/$' % (testrecipe, testrecipe))] + self._check_repo_status(os.path.dirname(recipefile), expected_status) + result = runCmd('git diff %s' % os.path.basename(recipefile), cwd=os.path.dirname(recipefile)) + removelines = ['SRC_URI = "git://.*"'] + addlines = [ + 'SRC_URI = "git://.* \\\\', + 'file://0001-Add-a-comment.patch;patchdir=.. \\\\', + '"' + ] + self._check_diff(result.output, addlines, removelines) + # Put things back so we can run devtool finish on a different layer + runCmd('cd %s; rm -f %s/*.patch; git checkout .' % (recipedir, testrecipe)) + # Run devtool finish + res = re.search('recipes-.*', recipedir) + self.assertTrue(res, 'Unable to find recipe subdirectory') + recipesubdir = res[0] + self.add_command_to_tearDown('rm -rf %s' % os.path.join(self.testlayer_path, recipesubdir)) + result = runCmd('devtool finish %s meta-selftest' % testrecipe) + # Check bbappend file contents + appendfn = os.path.join(self.testlayer_path, recipesubdir, '%s_%%.bbappend' % testrecipe) + with open(appendfn, 'r') as f: + appendlines = f.readlines() + expected_appendlines = [ + 'FILESEXTRAPATHS:prepend := "${THISDIR}/${PN}:"\n', + '\n', + 'SRC_URI += "file://0001-Add-a-comment.patch;patchdir=.."\n', + '\n' + ] + self.assertEqual(appendlines, expected_appendlines) + self.assertExists(os.path.join(os.path.dirname(appendfn), testrecipe, '0001-Add-a-comment.patch')) + # Try building + bitbake('%s -c patch' % testrecipe) + + def test_devtool_git_submodules(self): + # This tests if we can add a patch in a git submodule and extract it properly using devtool finish + # Check preconditions + self.assertTrue(not os.path.exists(self.workspacedir), 'This test cannot be run with a workspace directory under the build directory') + self.track_for_cleanup(self.workspacedir) + recipe = 'vulkan-samples' + src_uri = get_bb_var('SRC_URI', recipe) + self.assertIn('gitsm://', src_uri, 'This test expects the %s recipe to be a git recipe with submodules' % recipe) + oldrecipefile = get_bb_var('FILE', recipe) + recipedir = os.path.dirname(oldrecipefile) + result = runCmd('git status --porcelain .', cwd=recipedir) + if result.output.strip(): + self.fail('Recipe directory for %s contains uncommitted changes' % recipe) + self.assertIn('/meta/', recipedir) + tempdir = tempfile.mkdtemp(prefix='devtoolqa') + self.track_for_cleanup(tempdir) + self.add_command_to_tearDown('bitbake-layers remove-layer */workspace') + result = runCmd('devtool modify %s %s' % (recipe, tempdir)) + self.assertExists(os.path.join(tempdir, 'CMakeLists.txt'), 'Extracted source could not be found') + # Test devtool status + result = runCmd('devtool status') + self.assertIn(recipe, result.output) + self.assertIn(tempdir, result.output) + # Modify a source file in a submodule, (grab the first one) + result = runCmd('git submodule --quiet foreach \'echo $sm_path\'', cwd=tempdir) + submodule = result.output.splitlines()[0] + submodule_path = os.path.join(tempdir, submodule) + runCmd('echo "#This is a first comment" >> testfile', cwd=submodule_path) + result = runCmd('git status --porcelain . ', cwd=submodule_path) + self.assertIn("testfile", result.output) + runCmd('git add testfile; git commit -m "Adding a new file"', cwd=submodule_path) + + # Try finish to the original layer + self.add_command_to_tearDown('rm -rf %s ; cd %s ; git checkout %s' % (recipedir, os.path.dirname(recipedir), recipedir)) + runCmd('devtool finish -f %s meta' % recipe) + result = runCmd('devtool status') + self.assertNotIn(recipe, result.output, 'Recipe should have been reset by finish but wasn\'t') + self.assertNotExists(os.path.join(self.workspacedir, 'recipes', recipe), 'Recipe directory should not exist after finish') + expected_status = [(' M', '.*/%s$' % os.path.basename(oldrecipefile)), + ('??', '.*/.*-Adding-a-new-file.patch$')] + self._check_repo_status(recipedir, expected_status) + # Make sure the patch is added to the recipe with the correct "patchdir" option + result = runCmd('git diff .', cwd=recipedir) + addlines = [ + 'file://0001-Adding-a-new-file.patch;patchdir=%s \\\\' % submodule + ] + self._check_diff(result.output, addlines, []) + class DevtoolExtractTests(DevtoolBase): def test_devtool_extract(self): @@ -1208,33 +1779,15 @@ class DevtoolExtractTests(DevtoolBase): matches2 = glob.glob(stampprefix2 + '*') self.assertFalse(matches2, 'Stamp files exist for recipe %s that should have been cleaned' % testrecipe2) + @OETestTag("runqemu") def test_devtool_deploy_target(self): - # NOTE: Whilst this test would seemingly be better placed as a runtime test, - # unfortunately the runtime tests run under bitbake and you can't run - # devtool within bitbake (since devtool needs to run bitbake itself). - # Additionally we are testing build-time functionality as well, so - # really this has to be done as an oe-selftest test. - # - # Check preconditions - machine = get_bb_var('MACHINE') - if not machine.startswith('qemu'): - self.skipTest('This test only works with qemu machines') - if not os.path.exists('/etc/runqemu-nosudo'): - self.skipTest('You must set up tap devices with scripts/runqemu-gen-tapdevs before running this test') - result = runCmd('PATH="$PATH:/sbin:/usr/sbin" ip tuntap show', ignore_status=True) - if result.status != 0: - result = runCmd('PATH="$PATH:/sbin:/usr/sbin" ifconfig -a', ignore_status=True) - if result.status != 0: - self.skipTest('Failed to determine if tap devices exist with ifconfig or ip: %s' % result.output) - for line in result.output.splitlines(): - if line.startswith('tap'): - break - else: - self.skipTest('No tap devices found - you must set up tap devices with scripts/runqemu-gen-tapdevs before running this test') + self._check_runqemu_prerequisites() self.assertTrue(not os.path.exists(self.workspacedir), 'This test cannot be run with a workspace directory under the build directory') # Definitions testrecipe = 'mdadm' testfile = '/sbin/mdadm' + if "usrmerge" in get_bb_var('DISTRO_FEATURES'): + testfile = '/usr/sbin/mdadm' testimage = 'oe-selftest-image' testcommand = '/sbin/mdadm --help' # Build an image to run @@ -1328,6 +1881,14 @@ class DevtoolExtractTests(DevtoolBase): class DevtoolUpgradeTests(DevtoolBase): + def setUp(self): + super().setUp() + try: + runCmd("git config --global user.name") + runCmd("git config --global user.email") + except: + self.skip("Git user.name and user.email must be set") + def test_devtool_upgrade(self): # Check preconditions self.assertTrue(not os.path.exists(self.workspacedir), 'This test cannot be run with a workspace directory under the build directory') @@ -1408,6 +1969,100 @@ class DevtoolUpgradeTests(DevtoolBase): self.assertNotIn(recipe, result.output) self.assertNotExists(os.path.join(self.workspacedir, 'recipes', recipe), 'Recipe directory should not exist after resetting') + def test_devtool_upgrade_drop_md5sum(self): + # Check preconditions + self.assertTrue(not os.path.exists(self.workspacedir), 'This test cannot be run with a workspace directory under the build directory') + self.track_for_cleanup(self.workspacedir) + self.add_command_to_tearDown('bitbake-layers remove-layer */workspace') + # For the moment, we are using a real recipe. + recipe = 'devtool-upgrade-test3' + version = '1.6.0' + oldrecipefile = get_bb_var('FILE', recipe) + tempdir = tempfile.mkdtemp(prefix='devtoolqa') + self.track_for_cleanup(tempdir) + # Check upgrade. Code does not check if new PV is older or newer that current PV, so, it may be that + # we are downgrading instead of upgrading. + result = runCmd('devtool upgrade %s %s -V %s' % (recipe, tempdir, version)) + # Check new recipe file is present + newrecipefile = os.path.join(self.workspacedir, 'recipes', recipe, '%s_%s.bb' % (recipe, version)) + self.assertExists(newrecipefile, 'Recipe file should exist after upgrade') + # Check recipe got changed as expected + with open(oldrecipefile + '.upgraded', 'r') as f: + desiredlines = f.readlines() + with open(newrecipefile, 'r') as f: + newlines = f.readlines() + self.assertEqual(desiredlines, newlines) + + def test_devtool_upgrade_all_checksums(self): + # Check preconditions + self.assertTrue(not os.path.exists(self.workspacedir), 'This test cannot be run with a workspace directory under the build directory') + self.track_for_cleanup(self.workspacedir) + self.add_command_to_tearDown('bitbake-layers remove-layer */workspace') + # For the moment, we are using a real recipe. + recipe = 'devtool-upgrade-test4' + version = '1.6.0' + oldrecipefile = get_bb_var('FILE', recipe) + tempdir = tempfile.mkdtemp(prefix='devtoolqa') + self.track_for_cleanup(tempdir) + # Check upgrade. Code does not check if new PV is older or newer that current PV, so, it may be that + # we are downgrading instead of upgrading. + result = runCmd('devtool upgrade %s %s -V %s' % (recipe, tempdir, version)) + # Check new recipe file is present + newrecipefile = os.path.join(self.workspacedir, 'recipes', recipe, '%s_%s.bb' % (recipe, version)) + self.assertExists(newrecipefile, 'Recipe file should exist after upgrade') + # Check recipe got changed as expected + with open(oldrecipefile + '.upgraded', 'r') as f: + desiredlines = f.readlines() + with open(newrecipefile, 'r') as f: + newlines = f.readlines() + self.assertEqual(desiredlines, newlines) + + def test_devtool_upgrade_recipe_upgrade_extra_tasks(self): + # Check preconditions + self.assertTrue(not os.path.exists(self.workspacedir), 'This test cannot be run with a workspace directory under the build directory') + self.track_for_cleanup(self.workspacedir) + self.add_command_to_tearDown('bitbake-layers remove-layer */workspace') + recipe = 'python3-guessing-game' + version = '0.2.0' + commit = '40cf004c2772ffa20ea803fa3be1528a75be3e98' + oldrecipefile = get_bb_var('FILE', recipe) + oldcratesincfile = os.path.join(os.path.dirname(oldrecipefile), os.path.basename(oldrecipefile).strip('_git.bb') + '-crates.inc') + tempdir = tempfile.mkdtemp(prefix='devtoolqa') + self.track_for_cleanup(tempdir) + # Check that recipe is not already under devtool control + result = runCmd('devtool status') + self.assertNotIn(recipe, result.output) + # Check upgrade + result = runCmd('devtool upgrade %s %s --version %s --srcrev %s' % (recipe, tempdir, version, commit)) + # Check if srctree at least is populated + self.assertTrue(len(os.listdir(tempdir)) > 0, 'srctree (%s) should be populated with new (%s) source code' % (tempdir, commit)) + # Check new recipe file and new -crates.inc files are present + newrecipefile = os.path.join(self.workspacedir, 'recipes', recipe, os.path.basename(oldrecipefile)) + newcratesincfile = os.path.join(self.workspacedir, 'recipes', recipe, os.path.basename(oldcratesincfile)) + self.assertExists(newrecipefile, 'Recipe file should exist after upgrade') + self.assertExists(newcratesincfile, 'Recipe crates.inc file should exist after upgrade') + # Check devtool status and make sure recipe is present + result = runCmd('devtool status') + self.assertIn(recipe, result.output) + self.assertIn(tempdir, result.output) + # Check recipe got changed as expected + with open(oldrecipefile + '.upgraded', 'r') as f: + desiredlines = f.readlines() + with open(newrecipefile, 'r') as f: + newlines = f.readlines() + self.assertEqual(desiredlines, newlines) + # Check crates.inc got changed as expected + with open(oldcratesincfile + '.upgraded', 'r') as f: + desiredlines = f.readlines() + with open(newcratesincfile, 'r') as f: + newlines = f.readlines() + self.assertEqual(desiredlines, newlines) + # Check devtool reset recipe + result = runCmd('devtool reset %s -n' % recipe) + result = runCmd('devtool status') + self.assertNotIn(recipe, result.output) + self.assertNotExists(os.path.join(self.workspacedir, 'recipes', recipe), 'Recipe directory should not exist after resetting') + def test_devtool_layer_plugins(self): """Test that devtool can use plugins from other layers. @@ -1426,12 +2081,28 @@ class DevtoolUpgradeTests(DevtoolBase): for p in paths: dstdir = os.path.join(dstdir, p) if not os.path.exists(dstdir): - os.makedirs(dstdir) - self.track_for_cleanup(dstdir) + try: + os.makedirs(dstdir) + except PermissionError: + return False + except OSError as e: + if e.errno == errno.EROFS: + return False + else: + raise e + if p == "lib": + # Can race with other tests + self.add_command_to_tearDown('rmdir --ignore-fail-on-non-empty %s' % dstdir) + else: + self.track_for_cleanup(dstdir) dstfile = os.path.join(dstdir, os.path.basename(srcfile)) if srcfile != dstfile: - shutil.copy(srcfile, dstfile) + try: + shutil.copy(srcfile, dstfile) + except PermissionError: + return False self.track_for_cleanup(dstfile) + return True def test_devtool_load_plugin(self): """Test that devtool loads only the first found plugin in BBPATH.""" @@ -1449,15 +2120,17 @@ class DevtoolUpgradeTests(DevtoolBase): plugincontent = fh.readlines() try: self.assertIn('meta-selftest', srcfile, 'wrong bbpath plugin found') - for path in searchpath: - self._copy_file_with_cleanup(srcfile, path, 'lib', 'devtool') + searchpath = [ + path for path in searchpath + if self._copy_file_with_cleanup(srcfile, path, 'lib', 'devtool') + ] result = runCmd("devtool --quiet count") self.assertEqual(result.output, '1') result = runCmd("devtool --quiet multiloaded") self.assertEqual(result.output, "no") for path in searchpath: result = runCmd("devtool --quiet bbdir") - self.assertEqual(result.output, path) + self.assertEqual(os.path.realpath(result.output), os.path.realpath(path)) os.unlink(os.path.join(result.output, 'lib', 'devtool', 'bbpath.py')) finally: with open(srcfile, 'w') as fh: @@ -1638,6 +2311,52 @@ class DevtoolUpgradeTests(DevtoolBase): if files: self.fail('Unexpected file(s) copied next to bbappend: %s' % ', '.join(files)) + def test_devtool_finish_update_patch(self): + # This test uses a modified version of the sysdig recipe from meta-oe. + # - The patches have been renamed. + # - The dependencies are commented out since the recipe is not being + # built. + # + # The sysdig recipe is interesting in that it fetches two different Git + # repositories, and there are patches for both. This leads to that + # devtool will create ignore commits as it uses Git submodules to keep + # track of the second repository. + # + # This test will verify that the ignored commits actually are ignored + # when a commit in between is modified. It will also verify that the + # updated patch keeps its original name. + + # Check preconditions + self.assertTrue(not os.path.exists(self.workspacedir), 'This test cannot be run with a workspace directory under the build directory') + # Try modifying a recipe + self.track_for_cleanup(self.workspacedir) + recipe = 'sysdig-selftest' + recipefile = get_bb_var('FILE', recipe) + recipedir = os.path.dirname(recipefile) + result = runCmd('git status --porcelain .', cwd=recipedir) + if result.output.strip(): + self.fail('Recipe directory for %s contains uncommitted changes' % recipe) + tempdir = tempfile.mkdtemp(prefix='devtoolqa') + self.track_for_cleanup(tempdir) + self.add_command_to_tearDown('bitbake-layers remove-layer */workspace') + result = runCmd('devtool modify %s %s' % (recipe, tempdir)) + self.add_command_to_tearDown('cd %s; rm %s/*; git checkout %s %s' % (recipedir, recipe, recipe, os.path.basename(recipefile))) + self.assertExists(os.path.join(tempdir, 'CMakeLists.txt'), 'Extracted source could not be found') + # Make a change to one of the existing commits + result = runCmd('echo "# A comment " >> CMakeLists.txt', cwd=tempdir) + result = runCmd('git status --porcelain', cwd=tempdir) + self.assertIn('M CMakeLists.txt', result.output) + result = runCmd('git commit --fixup HEAD^ CMakeLists.txt', cwd=tempdir) + result = runCmd('git show -s --format=%s', cwd=tempdir) + self.assertIn('fixup! cmake: Pass PROBE_NAME via CFLAGS', result.output) + result = runCmd('GIT_SEQUENCE_EDITOR=true git rebase -i --autosquash devtool-base', cwd=tempdir) + result = runCmd('devtool finish %s meta-selftest' % recipe) + result = runCmd('devtool status') + self.assertNotIn(recipe, result.output, 'Recipe should have been reset by finish but wasn\'t') + self.assertNotExists(os.path.join(self.workspacedir, 'recipes', recipe), 'Recipe directory should not exist after finish') + expected_status = [(' M', '.*/0099-cmake-Pass-PROBE_NAME-via-CFLAGS.patch$')] + self._check_repo_status(recipedir, expected_status) + def test_devtool_rename(self): # Check preconditions self.assertTrue(not os.path.exists(self.workspacedir), 'This test cannot be run with a workspace directory under the build directory') @@ -1674,7 +2393,6 @@ class DevtoolUpgradeTests(DevtoolBase): self._test_recipe_contents(newrecipefile, checkvars, []) # Try again - change just name this time result = runCmd('devtool reset -n %s' % newrecipename) - shutil.rmtree(newsrctree) add_recipe() newrecipefile = os.path.join(self.workspacedir, 'recipes', newrecipename, '%s_%s.bb' % (newrecipename, recipever)) result = runCmd('devtool rename %s %s' % (recipename, newrecipename)) @@ -1687,7 +2405,6 @@ class DevtoolUpgradeTests(DevtoolBase): self._test_recipe_contents(newrecipefile, checkvars, []) # Try again - change just version this time result = runCmd('devtool reset -n %s' % newrecipename) - shutil.rmtree(newsrctree) add_recipe() newrecipefile = os.path.join(self.workspacedir, 'recipes', recipename, '%s_%s.bb' % (recipename, newrecipever)) result = runCmd('devtool rename %s -V %s' % (recipename, newrecipever)) @@ -1719,9 +2436,10 @@ class DevtoolUpgradeTests(DevtoolBase): Expected: devtool modify is able to checkout the source of the kernel and modification to the source and configurations are reflected when building the kernel. - """ - kernel_provider = get_bb_var('PREFERRED_PROVIDER_virtual/kernel') - # Clean up the enviroment + """ + kernel_provider = self.td['PREFERRED_PROVIDER_virtual/kernel'] + + # Clean up the environment bitbake('%s -c clean' % kernel_provider) tempdir = tempfile.mkdtemp(prefix='devtoolqa') tempdir_cfg = tempfile.mkdtemp(prefix='config_qa') @@ -1747,33 +2465,545 @@ class DevtoolUpgradeTests(DevtoolBase): self.assertExists(os.path.join(tempdir, 'Makefile'), 'Extracted source could not be found') #Step 4.2 configfile = os.path.join(tempdir,'.config') - diff = runCmd('diff %s %s' % (tmpconfig, configfile)) - self.assertEqual(0,diff.status,'Kernel .config file is not the same using bitbake and devtool') + runCmd('diff %s %s' % (tmpconfig, configfile)) + #Step 4.3 #NOTE: virtual/kernel is mapped to kernel_provider - result = runCmd('devtool build %s' % kernel_provider) - self.assertEqual(0,result.status,'Cannot build kernel using `devtool build`') + runCmd('devtool build %s' % kernel_provider) kernelfile = os.path.join(get_bb_var('KBUILD_OUTPUT', kernel_provider), 'vmlinux') self.assertExists(kernelfile, 'Kernel was not build correctly') #Modify the kernel source - modfile = os.path.join(tempdir,'arch/x86/boot/header.S') - modstring = "Use a boot loader. Devtool testing." - modapplied = runCmd("sed -i 's/Use a boot loader./%s/' %s" % (modstring, modfile)) - self.assertEqual(0,modapplied.status,'Modification to %s on kernel source failed' % modfile) + modfile = os.path.join(tempdir, 'init/version.c') + # Moved to uts.h in 6.1 onwards + modfile2 = os.path.join(tempdir, 'include/linux/uts.h') + runCmd("sed -i 's/Linux/LiNuX/g' %s %s" % (modfile, modfile2)) + #Modify the configuration - codeconfigfile = os.path.join(tempdir,'.config.new') + codeconfigfile = os.path.join(tempdir, '.config.new') modconfopt = "CONFIG_SG_POOL=n" - modconf = runCmd("sed -i 's/CONFIG_SG_POOL=y/%s/' %s" % (modconfopt, codeconfigfile)) - self.assertEqual(0,modconf.status,'Modification to %s failed' % codeconfigfile) + runCmd("sed -i 's/CONFIG_SG_POOL=y/%s/' %s" % (modconfopt, codeconfigfile)) + #Build again kernel with devtool - rebuild = runCmd('devtool build %s' % kernel_provider) - self.assertEqual(0,rebuild.status,'Fail to build kernel after modification of source and config') + runCmd('devtool build %s' % kernel_provider) + #Step 4.4 - bzimagename = 'bzImage-' + get_bb_var('KERNEL_VERSION_NAME', kernel_provider) - bzimagefile = os.path.join(get_bb_var('D', kernel_provider),'boot', bzimagename) - checkmodcode = runCmd("grep '%s' %s" % (modstring, bzimagefile)) - self.assertEqual(0,checkmodcode.status,'Modification on kernel source failed') + runCmd("grep '%s' %s" % ('LiNuX', kernelfile)) + #Step 4.5 - checkmodconfg = runCmd("grep %s %s" % (modconfopt, codeconfigfile)) - self.assertEqual(0,checkmodconfg.status,'Modification to configuration file failed') + runCmd("grep %s %s" % (modconfopt, codeconfigfile)) + + +class DevtoolIdeSdkTests(DevtoolBase): + def _write_bb_config(self, recipe_names): + """Helper to write the bitbake local.conf file""" + conf_lines = [ + 'IMAGE_CLASSES += "image-combined-dbg"', + 'IMAGE_GEN_DEBUGFS = "1"', + 'IMAGE_INSTALL:append = " gdbserver %s"' % ' '.join( + [r + '-ptest' for r in recipe_names]) + ] + self.write_config("\n".join(conf_lines)) + + def _check_workspace(self): + """Check if a workspace directory is available and setup the cleanup""" + self.assertTrue(not os.path.exists(self.workspacedir), + 'This test cannot be run with a workspace directory under the build directory') + self.track_for_cleanup(self.workspacedir) + self.add_command_to_tearDown('bitbake-layers remove-layer */workspace') + + def _workspace_scripts_dir(self, recipe_name): + return os.path.realpath(os.path.join(self.builddir, 'workspace', 'ide-sdk', recipe_name, 'scripts')) + + def _sources_scripts_dir(self, src_dir): + return os.path.realpath(os.path.join(src_dir, 'oe-scripts')) + + def _workspace_gdbinit_dir(self, recipe_name): + return os.path.realpath(os.path.join(self.builddir, 'workspace', 'ide-sdk', recipe_name, 'scripts', 'gdbinit')) + + def _sources_gdbinit_dir(self, src_dir): + return os.path.realpath(os.path.join(src_dir, 'oe-gdbinit')) + + def _devtool_ide_sdk_recipe(self, recipe_name, build_file, testimage): + """Setup a recipe for working with devtool ide-sdk + + Basically devtool modify -x followed by some tests + """ + tempdir = tempfile.mkdtemp(prefix='devtoolqa') + self.track_for_cleanup(tempdir) + self.add_command_to_tearDown('bitbake -c clean %s' % recipe_name) + + result = runCmd('devtool modify %s -x %s' % (recipe_name, tempdir)) + self.assertExists(os.path.join(tempdir, build_file), + 'Extracted source could not be found') + self.assertExists(os.path.join(self.workspacedir, 'conf', + 'layer.conf'), 'Workspace directory not created') + matches = glob.glob(os.path.join(self.workspacedir, + 'appends', recipe_name + '.bbappend')) + self.assertTrue(matches, 'bbappend not created %s' % result.output) + + # Test devtool status + result = runCmd('devtool status') + self.assertIn(recipe_name, result.output) + self.assertIn(tempdir, result.output) + self._check_src_repo(tempdir) + + # Usually devtool ide-sdk would initiate the build of the SDK. + # But there is a circular dependency with starting Qemu and passing the IP of runqemu to devtool ide-sdk. + if testimage: + bitbake("%s qemu-native qemu-helper-native" % testimage) + deploy_dir_image = get_bb_var('DEPLOY_DIR_IMAGE') + self.add_command_to_tearDown('bitbake -c clean %s' % testimage) + self.add_command_to_tearDown( + 'rm -f %s/%s*' % (deploy_dir_image, testimage)) + + return tempdir + + def _get_recipe_ids(self, recipe_name): + """IDs needed to write recipe specific config entries into IDE config files""" + package_arch = get_bb_var('PACKAGE_ARCH', recipe_name) + recipe_id = recipe_name + "-" + package_arch + recipe_id_pretty = recipe_name + ": " + package_arch + return (recipe_id, recipe_id_pretty) + + def _verify_install_script_code(self, tempdir, recipe_name): + """Verify the scripts referred by the tasks.json file are fine. + + This function does not depend on Qemu. Therefore it verifies the scripts + exists and the delete step works as expected. But it does not try to + deploy to Qemu. + """ + recipe_id, recipe_id_pretty = self._get_recipe_ids(recipe_name) + with open(os.path.join(tempdir, '.vscode', 'tasks.json')) as tasks_j: + tasks_d = json.load(tasks_j) + tasks = tasks_d["tasks"] + task_install = next( + (task for task in tasks if task["label"] == "install && deploy-target %s" % recipe_id_pretty), None) + self.assertIsNot(task_install, None) + # execute only the bb_run_do_install script since the deploy would require e.g. Qemu running. + i_and_d_script = "install_and_deploy_" + recipe_id + i_and_d_script_path = os.path.join( + self._workspace_scripts_dir(recipe_name), i_and_d_script) + self.assertExists(i_and_d_script_path) + del_script = "delete_package_dirs_" + recipe_id + del_script_path = os.path.join( + self._workspace_scripts_dir(recipe_name), del_script) + self.assertExists(del_script_path) + runCmd(del_script_path, cwd=tempdir) + + def _devtool_ide_sdk_qemu(self, tempdir, qemu, recipe_name, example_exe): + """Verify deployment and execution in Qemu system work for one recipe. + + This function checks the entire SDK workflow: changing the code, recompiling + it and deploying it back to Qemu, and checking that the changes have been + incorporated into the provided binaries. It also runs the tests of the recipe. + """ + recipe_id, _ = self._get_recipe_ids(recipe_name) + i_and_d_script = "install_and_deploy_" + recipe_id + install_deploy_cmd = os.path.join( + self._workspace_scripts_dir(recipe_name), i_and_d_script) + self.assertExists(install_deploy_cmd, + '%s script not found' % install_deploy_cmd) + runCmd(install_deploy_cmd) + + MAGIC_STRING_ORIG = "Magic: 123456789" + MAGIC_STRING_NEW = "Magic: 987654321" + ptest_cmd = "ptest-runner " + recipe_name + + # validate that SSH is working + status, _ = qemu.run("uname") + self.assertEqual( + status, 0, msg="Failed to connect to the SSH server on Qemu") + + # Verify the unmodified example prints the magic string + status, output = qemu.run(example_exe) + self.assertEqual(status, 0, msg="%s failed: %s" % + (example_exe, output)) + self.assertIn(MAGIC_STRING_ORIG, output) + + # Verify the unmodified ptests work + status, output = qemu.run(ptest_cmd) + self.assertEqual(status, 0, msg="%s failed: %s" % (ptest_cmd, output)) + self.assertIn("PASS: cpp-example-lib", output) + + # Verify remote debugging works + self._gdb_cross_debugging( + qemu, recipe_name, example_exe, MAGIC_STRING_ORIG) + + # Replace the Magic String in the code, compile and deploy to Qemu + cpp_example_lib_hpp = os.path.join(tempdir, 'cpp-example-lib.hpp') + with open(cpp_example_lib_hpp, 'r') as file: + cpp_code = file.read() + cpp_code = cpp_code.replace(MAGIC_STRING_ORIG, MAGIC_STRING_NEW) + with open(cpp_example_lib_hpp, 'w') as file: + file.write(cpp_code) + runCmd(install_deploy_cmd, cwd=tempdir) + + # Verify the modified example prints the modified magic string + status, output = qemu.run(example_exe) + self.assertEqual(status, 0, msg="%s failed: %s" % + (example_exe, output)) + self.assertNotIn(MAGIC_STRING_ORIG, output) + self.assertIn(MAGIC_STRING_NEW, output) + + # Verify the modified example ptests work + status, output = qemu.run(ptest_cmd) + self.assertEqual(status, 0, msg="%s failed: %s" % (ptest_cmd, output)) + self.assertIn("PASS: cpp-example-lib", output) + + # Verify remote debugging works wit the modified magic string + self._gdb_cross_debugging( + qemu, recipe_name, example_exe, MAGIC_STRING_NEW) + + def _gdb_cross(self): + """Verify gdb-cross is provided by devtool ide-sdk""" + target_arch = self.td["TARGET_ARCH"] + target_sys = self.td["TARGET_SYS"] + gdb_recipe = "gdb-cross-" + target_arch + gdb_binary = target_sys + "-gdb" + + native_sysroot = get_bb_var("RECIPE_SYSROOT_NATIVE", gdb_recipe) + r = runCmd("%s --version" % gdb_binary, + native_sysroot=native_sysroot, target_sys=target_sys) + self.assertEqual(r.status, 0) + self.assertIn("GNU gdb", r.output) + + def _gdb_cross_debugging(self, qemu, recipe_name, example_exe, magic_string): + """Verify gdb-cross is working + + Test remote debugging: + break main + run + continue + break CppExample::print_json() + continue + print CppExample::test_string.compare("cpp-example-lib Magic: 123456789") + $1 = 0 + print CppExample::test_string.compare("cpp-example-lib Magic: 123456789aaa") + $2 = -3 + list cpp-example-lib.hpp:13,13 + 13 inline static const std::string test_string = "cpp-example-lib Magic: 123456789"; + continue + """ + sshargs = '-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no' + gdbserver_script = os.path.join(self._workspace_scripts_dir( + recipe_name), 'gdbserver_1234_usr-bin-' + example_exe + '_m') + gdb_script = os.path.join(self._workspace_scripts_dir( + recipe_name), 'gdb_1234_usr-bin-' + example_exe) + + # Start a gdbserver + r = runCmd(gdbserver_script) + self.assertEqual(r.status, 0) + + # Check there is a gdbserver running + r = runCmd('ssh %s root@%s %s' % (sshargs, qemu.ip, 'ps')) + self.assertEqual(r.status, 0) + self.assertIn("gdbserver ", r.output) + + # Check the pid file is correct + test_cmd = "cat /proc/$(cat /tmp/gdbserver_1234_usr-bin-" + \ + example_exe + "/pid)/cmdline" + r = runCmd('ssh %s root@%s %s' % (sshargs, qemu.ip, test_cmd)) + self.assertEqual(r.status, 0) + self.assertIn("gdbserver", r.output) + + # Test remote debugging works + gdb_batch_cmd = " --batch -ex 'break main' -ex 'run'" + gdb_batch_cmd += " -ex 'break CppExample::print_json()' -ex 'continue'" + gdb_batch_cmd += " -ex 'print CppExample::test_string.compare(\"cpp-example-lib %s\")'" % magic_string + gdb_batch_cmd += " -ex 'print CppExample::test_string.compare(\"cpp-example-lib %saaa\")'" % magic_string + gdb_batch_cmd += " -ex 'list cpp-example-lib.hpp:13,13'" + gdb_batch_cmd += " -ex 'continue'" + r = runCmd(gdb_script + gdb_batch_cmd) + self.logger.debug("%s %s returned: %s", gdb_script, + gdb_batch_cmd, r.output) + self.assertEqual(r.status, 0) + self.assertIn("Breakpoint 1, main", r.output) + self.assertIn("$1 = 0", r.output) # test.string.compare equal + self.assertIn("$2 = -3", r.output) # test.string.compare longer + self.assertIn( + 'inline static const std::string test_string = "cpp-example-lib %s";' % magic_string, r.output) + self.assertIn("exited normally", r.output) + + # Stop the gdbserver + r = runCmd(gdbserver_script + ' stop') + self.assertEqual(r.status, 0) + + # Check there is no gdbserver running + r = runCmd('ssh %s root@%s %s' % (sshargs, qemu.ip, 'ps')) + self.assertEqual(r.status, 0) + self.assertNotIn("gdbserver ", r.output) + + def _verify_cmake_preset(self, tempdir): + """Verify the generated cmake preset works as expected + + Check if compiling works + Check if unit tests can be executed in qemu (not qemu-system) + """ + with open(os.path.join(tempdir, 'CMakeUserPresets.json')) as cmake_preset_j: + cmake_preset_d = json.load(cmake_preset_j) + config_presets = cmake_preset_d["configurePresets"] + self.assertEqual(len(config_presets), 1) + cmake_exe = config_presets[0]["cmakeExecutable"] + preset_name = config_presets[0]["name"] + + # Verify the wrapper for cmake native is available + self.assertExists(cmake_exe) + + # Verify the cmake preset generated by devtool ide-sdk is available + result = runCmd('%s --list-presets' % cmake_exe, cwd=tempdir) + self.assertIn(preset_name, result.output) + + # Verify cmake re-uses the o files compiled by bitbake + result = runCmd('%s --build --preset %s' % + (cmake_exe, preset_name), cwd=tempdir) + self.assertIn("ninja: no work to do.", result.output) + + # Verify the unit tests work (in Qemu user mode) + result = runCmd('%s --build --preset %s --target test' % + (cmake_exe, preset_name), cwd=tempdir) + self.assertIn("100% tests passed", result.output) + + # Verify re-building and testing works again + result = runCmd('%s --build --preset %s --target clean' % + (cmake_exe, preset_name), cwd=tempdir) + self.assertIn("Cleaning", result.output) + result = runCmd('%s --build --preset %s' % + (cmake_exe, preset_name), cwd=tempdir) + self.assertIn("Building", result.output) + self.assertIn("Linking", result.output) + result = runCmd('%s --build --preset %s --target test' % + (cmake_exe, preset_name), cwd=tempdir) + self.assertIn("Running tests...", result.output) + self.assertIn("100% tests passed", result.output) + + @OETestTag("runqemu") + def test_devtool_ide_sdk_none_qemu(self): + """Start qemu-system and run tests for multiple recipes. ide=none is used.""" + recipe_names = ["cmake-example", "meson-example"] + testimage = "oe-selftest-image" + + self._check_workspace() + self._write_bb_config(recipe_names) + self._check_runqemu_prerequisites() + + # Verify deployment to Qemu (system mode) works + bitbake(testimage) + with runqemu(testimage, runqemuparams="nographic") as qemu: + # cmake-example recipe + recipe_name = "cmake-example" + example_exe = "cmake-example" + build_file = "CMakeLists.txt" + tempdir = self._devtool_ide_sdk_recipe( + recipe_name, build_file, testimage) + bitbake_sdk_cmd = 'devtool ide-sdk %s %s -t root@%s -c --ide=none' % ( + recipe_name, testimage, qemu.ip) + runCmd(bitbake_sdk_cmd) + self._gdb_cross() + self._verify_cmake_preset(tempdir) + self._devtool_ide_sdk_qemu(tempdir, qemu, recipe_name, example_exe) + # Verify the oe-scripts sym-link is valid + self.assertEqual(self._workspace_scripts_dir( + recipe_name), self._sources_scripts_dir(tempdir)) + + # meson-example recipe + recipe_name = "meson-example" + example_exe = "mesonex" + build_file = "meson.build" + tempdir = self._devtool_ide_sdk_recipe( + recipe_name, build_file, testimage) + bitbake_sdk_cmd = 'devtool ide-sdk %s %s -t root@%s -c --ide=none' % ( + recipe_name, testimage, qemu.ip) + runCmd(bitbake_sdk_cmd) + self._gdb_cross() + self._devtool_ide_sdk_qemu(tempdir, qemu, recipe_name, example_exe) + # Verify the oe-scripts sym-link is valid + self.assertEqual(self._workspace_scripts_dir( + recipe_name), self._sources_scripts_dir(tempdir)) + + def test_devtool_ide_sdk_code_cmake(self): + """Verify a cmake recipe works with ide=code mode""" + recipe_name = "cmake-example" + build_file = "CMakeLists.txt" + testimage = "oe-selftest-image" + + self._check_workspace() + self._write_bb_config([recipe_name]) + tempdir = self._devtool_ide_sdk_recipe( + recipe_name, build_file, testimage) + bitbake_sdk_cmd = 'devtool ide-sdk %s %s -t root@192.168.17.17 -c --ide=code' % ( + recipe_name, testimage) + runCmd(bitbake_sdk_cmd) + self._verify_cmake_preset(tempdir) + self._verify_install_script_code(tempdir, recipe_name) + self._gdb_cross() + + def test_devtool_ide_sdk_code_meson(self): + """Verify a meson recipe works with ide=code mode""" + recipe_name = "meson-example" + build_file = "meson.build" + testimage = "oe-selftest-image" + + self._check_workspace() + self._write_bb_config([recipe_name]) + tempdir = self._devtool_ide_sdk_recipe( + recipe_name, build_file, testimage) + bitbake_sdk_cmd = 'devtool ide-sdk %s %s -t root@192.168.17.17 -c --ide=code' % ( + recipe_name, testimage) + runCmd(bitbake_sdk_cmd) + + with open(os.path.join(tempdir, '.vscode', 'settings.json')) as settings_j: + settings_d = json.load(settings_j) + meson_exe = settings_d["mesonbuild.mesonPath"] + meson_build_folder = settings_d["mesonbuild.buildFolder"] + + # Verify the wrapper for meson native is available + self.assertExists(meson_exe) + + # Verify meson re-uses the o files compiled by bitbake + result = runCmd('%s compile -C %s' % + (meson_exe, meson_build_folder), cwd=tempdir) + self.assertIn("ninja: no work to do.", result.output) + + # Verify the unit tests work (in Qemu) + runCmd('%s test -C %s' % (meson_exe, meson_build_folder), cwd=tempdir) + + # Verify re-building and testing works again + result = runCmd('%s compile -C %s --clean' % + (meson_exe, meson_build_folder), cwd=tempdir) + self.assertIn("Cleaning...", result.output) + result = runCmd('%s compile -C %s' % + (meson_exe, meson_build_folder), cwd=tempdir) + self.assertIn("Linking target", result.output) + runCmd('%s test -C %s' % (meson_exe, meson_build_folder), cwd=tempdir) + + self._verify_install_script_code(tempdir, recipe_name) + self._gdb_cross() + + def test_devtool_ide_sdk_shared_sysroots(self): + """Verify the shared sysroot SDK""" + + # Handle the workspace (which is not needed by this test case) + self._check_workspace() + + result_init = runCmd( + 'devtool ide-sdk -m shared oe-selftest-image cmake-example meson-example --ide=code') + bb_vars = get_bb_vars( + ['REAL_MULTIMACH_TARGET_SYS', 'DEPLOY_DIR_IMAGE', 'COREBASE'], "meta-ide-support") + environment_script = 'environment-setup-%s' % bb_vars['REAL_MULTIMACH_TARGET_SYS'] + deploydir = bb_vars['DEPLOY_DIR_IMAGE'] + environment_script_path = os.path.join(deploydir, environment_script) + cpp_example_src = os.path.join( + bb_vars['COREBASE'], 'meta-selftest', 'recipes-test', 'cpp', 'files') + + # Verify the cross environment script is available + self.assertExists(environment_script_path) + + def runCmdEnv(cmd, cwd): + cmd = '/bin/sh -c ". %s > /dev/null && %s"' % ( + environment_script_path, cmd) + return runCmd(cmd, cwd) + + # Verify building the C++ example works with CMake + tempdir_cmake = tempfile.mkdtemp(prefix='devtoolqa') + self.track_for_cleanup(tempdir_cmake) + + result_cmake = runCmdEnv("which cmake", cwd=tempdir_cmake) + cmake_native = os.path.normpath(result_cmake.output.strip()) + self.assertExists(cmake_native) + + runCmdEnv('cmake %s' % cpp_example_src, cwd=tempdir_cmake) + runCmdEnv('cmake --build %s' % tempdir_cmake, cwd=tempdir_cmake) + + # Verify the printed note really referres to a cmake executable + cmake_native_code = "" + for line in result_init.output.splitlines(): + m = re.search(r'"cmake.cmakePath": "(.*)"', line) + if m: + cmake_native_code = m.group(1) + break + self.assertExists(cmake_native_code) + self.assertEqual(cmake_native, cmake_native_code) + + # Verify building the C++ example works with Meson + tempdir_meson = tempfile.mkdtemp(prefix='devtoolqa') + self.track_for_cleanup(tempdir_meson) + + result_cmake = runCmdEnv("which meson", cwd=tempdir_meson) + meson_native = os.path.normpath(result_cmake.output.strip()) + self.assertExists(meson_native) + + runCmdEnv('meson setup %s' % tempdir_meson, cwd=cpp_example_src) + runCmdEnv('meson compile', cwd=tempdir_meson) + + def test_devtool_ide_sdk_plugins(self): + """Test that devtool ide-sdk can use plugins from other layers.""" + + # We need a workspace layer and a modified recipe (but no image) + modified_recipe_name = "meson-example" + modified_build_file = "meson.build" + testimage = "oe-selftest-image" + shared_recipe_name = "cmake-example" + + self._check_workspace() + self._write_bb_config([modified_recipe_name]) + tempdir = self._devtool_ide_sdk_recipe( + modified_recipe_name, modified_build_file, None) + + IDE_RE = re.compile(r'.*--ide \{(.*)\}.*') + + def get_ides_from_help(help_str): + m = IDE_RE.search(help_str) + return m.group(1).split(',') + + # verify the default plugins are available but the foo plugin is not + result = runCmd('devtool ide-sdk -h') + found_ides = get_ides_from_help(result.output) + self.assertIn('code', found_ides) + self.assertIn('none', found_ides) + self.assertNotIn('foo', found_ides) + + shared_config_file = os.path.join(tempdir, 'shared-config.txt') + shared_config_str = 'Dummy shared IDE config' + modified_config_file = os.path.join(tempdir, 'modified-config.txt') + modified_config_str = 'Dummy modified IDE config' + + # Generate a foo plugin in the workspace layer + plugin_dir = os.path.join( + self.workspacedir, 'lib', 'devtool', 'ide_plugins') + os.makedirs(plugin_dir) + plugin_code = 'from devtool.ide_plugins import IdeBase\n\n' + plugin_code += 'class IdeFoo(IdeBase):\n' + plugin_code += ' def setup_shared_sysroots(self, shared_env):\n' + plugin_code += ' with open("%s", "w") as config_file:\n' % shared_config_file + plugin_code += ' config_file.write("%s")\n\n' % shared_config_str + plugin_code += ' def setup_modified_recipe(self, args, image_recipe, modified_recipe):\n' + plugin_code += ' with open("%s", "w") as config_file:\n' % modified_config_file + plugin_code += ' config_file.write("%s")\n\n' % modified_config_str + plugin_code += 'def register_ide_plugin(ide_plugins):\n' + plugin_code += ' ide_plugins["foo"] = IdeFoo\n' + + plugin_py = os.path.join(plugin_dir, 'ide_foo.py') + with open(plugin_py, 'w') as plugin_file: + plugin_file.write(plugin_code) + + # Verify the foo plugin is available as well + result = runCmd('devtool ide-sdk -h') + found_ides = get_ides_from_help(result.output) + self.assertIn('code', found_ides) + self.assertIn('none', found_ides) + self.assertIn('foo', found_ides) + + # Verify the foo plugin generates a shared config + result = runCmd( + 'devtool ide-sdk -m shared --skip-bitbake --ide foo %s' % shared_recipe_name) + with open(shared_config_file) as shared_config: + shared_config_new = shared_config.read() + self.assertEqual(shared_config_str, shared_config_new) + + # Verify the foo plugin generates a modified config + result = runCmd('devtool ide-sdk --skip-bitbake --ide foo %s %s' % + (modified_recipe_name, testimage)) + with open(modified_config_file) as modified_config: + modified_config_new = modified_config.read() + self.assertEqual(modified_config_str, modified_config_new) diff --git a/meta/lib/oeqa/selftest/cases/diffoscope/A/file.txt b/meta/lib/oeqa/selftest/cases/diffoscope/A/file.txt new file mode 100644 index 0000000000..f70f10e4db --- /dev/null +++ b/meta/lib/oeqa/selftest/cases/diffoscope/A/file.txt @@ -0,0 +1 @@ +A diff --git a/meta/lib/oeqa/selftest/cases/diffoscope/B/file.txt b/meta/lib/oeqa/selftest/cases/diffoscope/B/file.txt new file mode 100644 index 0000000000..223b7836fb --- /dev/null +++ b/meta/lib/oeqa/selftest/cases/diffoscope/B/file.txt @@ -0,0 +1 @@ +B diff --git a/meta/lib/oeqa/selftest/cases/distrodata.py b/meta/lib/oeqa/selftest/cases/distrodata.py index 68ba556485..7771a42e2b 100644 --- a/meta/lib/oeqa/selftest/cases/distrodata.py +++ b/meta/lib/oeqa/selftest/cases/distrodata.py @@ -1,11 +1,10 @@ # +# Copyright OpenEmbedded Contributors +# # SPDX-License-Identifier: MIT # from oeqa.selftest.case import OESelftestTestCase -from oeqa.utils.commands import runCmd, bitbake, get_bb_var, get_bb_vars -from oeqa.utils.decorators import testcase -from oeqa.utils.ftools import write_file import oe.recipeutils @@ -18,13 +17,13 @@ class Distrodata(OESelftestTestCase): Product: oe-core Author: Alexander Kanavin <alex.kanavin@gmail.com> """ - feature = 'LICENSE_FLAGS_WHITELIST += " commercial"\n' + feature = 'LICENSE_FLAGS_ACCEPTED += " commercial"\n' self.write_config(feature) - pkgs = oe.recipeutils.get_recipe_upgrade_status() + pkggroups = oe.recipeutils.get_recipe_upgrade_status() - regressed_failures = [pkg[0] for pkg in pkgs if pkg[1] == 'UNKNOWN_BROKEN'] - regressed_successes = [pkg[0] for pkg in pkgs if pkg[1] == 'KNOWN_BROKEN'] + regressed_failures = [pkg['pn'] for pkgs in pkggroups for pkg in pkgs if pkg['status'] == 'UNKNOWN_BROKEN'] + regressed_successes = [pkg['pn'] for pkgs in pkggroups for pkg in pkgs if pkg['status'] == 'KNOWN_BROKEN'] msg = "" if len(regressed_failures) > 0: msg = msg + """ @@ -42,19 +41,28 @@ but their recipes claim otherwise by setting UPSTREAM_VERSION_UNKNOWN. Please re def test_maintainers(self): """ - Summary: Test that oe-core recipes have a maintainer + Summary: Test that oe-core recipes have a maintainer and entries in maintainers list have a recipe Expected: All oe-core recipes (except a few special static/testing ones) should have a maintainer listed in maintainers.inc file. + Expected: All entries in maintainers list should have a recipe file that matches them Product: oe-core Author: Alexander Kanavin <alex.kanavin@gmail.com> """ def is_exception(pkg): - exceptions = ["packagegroup-", "initramfs-", "systemd-machine-units", "target-sdk-provides-dummy"] + exceptions = ["packagegroup-",] for i in exceptions: if i in pkg: return True return False - feature = 'require conf/distro/include/maintainers.inc\n' + def is_maintainer_exception(entry): + exceptions = ["musl", "newlib", "picolibc", "linux-yocto", "linux-dummy", "mesa-gl", "libgfortran", "libx11-compose-data", + "cve-update-nvd2-native",] + for i in exceptions: + if i in entry: + return True + return False + + feature = 'require conf/distro/include/maintainers.inc\nLICENSE_FLAGS_ACCEPTED += " commercial"\nPARSE_ALL_RECIPES = "1"\nPACKAGE_CLASSES = "package_ipk package_deb package_rpm"\n' self.write_config(feature) with bb.tinfoil.Tinfoil() as tinfoil: @@ -62,6 +70,11 @@ but their recipes claim otherwise by setting UPSTREAM_VERSION_UNKNOWN. Please re with_maintainer_list = [] no_maintainer_list = [] + + missing_recipes = [] + recipes = [] + prefix = "RECIPE_MAINTAINER:pn-" + # We could have used all_recipes() here, but this method will find # every recipe if we ever move to setting RECIPE_MAINTAINER in recipe files # instead of maintainers.inc @@ -71,6 +84,7 @@ but their recipes claim otherwise by setting UPSTREAM_VERSION_UNKNOWN. Please re continue rd = tinfoil.parse_recipe_file(fn, appends=False) pn = rd.getVar('PN') + recipes.append(pn) if is_exception(pn): continue if rd.getVar('RECIPE_MAINTAINER'): @@ -78,6 +92,15 @@ but their recipes claim otherwise by setting UPSTREAM_VERSION_UNKNOWN. Please re else: no_maintainer_list.append((pn, fn)) + maintainers = tinfoil.config_data.keys() + for key in maintainers: + if key.startswith(prefix): + recipe = tinfoil.config_data.expand(key[len(prefix):]) + if is_maintainer_exception(recipe): + continue + if recipe not in recipes: + missing_recipes.append(recipe) + if no_maintainer_list: self.fail(""" The following recipes do not have a maintainer assigned to them. Please add an entry to meta/conf/distro/include/maintainers.inc file. @@ -87,3 +110,20 @@ The following recipes do not have a maintainer assigned to them. Please add an e self.fail(""" The list of oe-core recipes with maintainers is empty. This may indicate that the test has regressed and needs fixing. """) + + if missing_recipes: + self.fail(""" +Unable to find recipes for the following entries in maintainers.inc: +""" + "\n".join(['%s' % i for i in missing_recipes])) + + def test_common_include_recipes(self): + """ + Summary: Test that obtaining recipes that share includes between them returns a sane result + Expected: At least cmake and qemu entries are present in the output + Product: oe-core + Author: Alexander Kanavin <alex.kanavin@gmail.com> + """ + recipes = oe.recipeutils.get_common_include_recipes() + + self.assertIn({'qemu-system-native', 'qemu', 'qemu-native'}, recipes) + self.assertIn({'cmake-native', 'cmake'}, recipes) diff --git a/meta/lib/oeqa/selftest/cases/efibootpartition.py b/meta/lib/oeqa/selftest/cases/efibootpartition.py index a61cf9bcb3..fa74103dec 100644 --- a/meta/lib/oeqa/selftest/cases/efibootpartition.py +++ b/meta/lib/oeqa/selftest/cases/efibootpartition.py @@ -5,42 +5,29 @@ # SPDX-License-Identifier: MIT # -import re - from oeqa.selftest.case import OESelftestTestCase -from oeqa.utils.commands import bitbake, runqemu, get_bb_var +from oeqa.utils.commands import bitbake, runqemu +from oeqa.core.decorator.data import skipIfNotMachine +import oe.types class GenericEFITest(OESelftestTestCase): """EFI booting test class""" + @skipIfNotMachine("qemux86-64", "test is qemux86-64 specific currently") + def test_boot_efi(self): + cmd = "runqemu nographic serial wic ovmf" + if oe.types.qemu_use_kvm(self.td.get('QEMU_USE_KVM', 0), self.td["TARGET_ARCH"]): + cmd += " kvm" + image = "core-image-minimal" - cmd_common = "runqemu nographic serial wic ovmf" - efi_provider = "systemd-boot" - image = "core-image-minimal" - machine = "qemux86-64" - recipes_built = False - - @classmethod - def setUpLocal(self): - super(GenericEFITest, self).setUpLocal(self) - - self.write_config(self, -""" -EFI_PROVIDER = "%s" -IMAGE_FSTYPES_pn-%s_append = " wic" -MACHINE = "%s" -MACHINE_FEATURES_append = " efi" + self.write_config(""" +EFI_PROVIDER = "systemd-boot" +IMAGE_FSTYPES:pn-%s:append = " wic" +MACHINE_FEATURES:append = " efi" WKS_FILE = "efi-bootdisk.wks.in" -IMAGE_INSTALL_append = " grub-efi systemd-boot kernel-image-bzimage" +IMAGE_INSTALL:append = " grub-efi systemd-boot kernel-image-bzimage" """ -% (self.efi_provider, self.image, self.machine)) - if not self.recipes_built: - bitbake("ovmf") - bitbake(self.image) - self.recipes_built = True +% (image)) - @classmethod - def test_boot_efi(self): - """Test generic boot partition with qemu""" - cmd = "%s %s" % (self.cmd_common, self.machine) - with runqemu(self.image, ssh=False, launch_cmd=cmd) as qemu: + bitbake(image + " ovmf") + with runqemu(image, ssh=False, launch_cmd=cmd) as qemu: self.assertTrue(qemu.runner.logged, "Failed: %s" % cmd) diff --git a/meta/lib/oeqa/selftest/cases/eSDK.py b/meta/lib/oeqa/selftest/cases/esdk.py index 862849af35..9f5de2cde7 100644 --- a/meta/lib/oeqa/selftest/cases/eSDK.py +++ b/meta/lib/oeqa/selftest/cases/esdk.py @@ -1,4 +1,6 @@ # +# Copyright OpenEmbedded Contributors +# # SPDX-License-Identifier: MIT # @@ -8,7 +10,7 @@ import os import glob import time from oeqa.selftest.case import OESelftestTestCase -from oeqa.utils.commands import runCmd, bitbake, get_bb_var, get_bb_vars +from oeqa.utils.commands import runCmd, bitbake, get_bb_vars class oeSDKExtSelfTest(OESelftestTestCase): """ @@ -63,7 +65,7 @@ class oeSDKExtSelfTest(OESelftestTestCase): cls.env_eSDK = oeSDKExtSelfTest.get_esdk_environment('', cls.tmpdir_eSDKQA) sstate_config=""" -SDK_LOCAL_CONF_WHITELIST = "SSTATE_MIRRORS" +ESDK_LOCALCONF_ALLOW = "SSTATE_MIRRORS" SSTATE_MIRRORS = "file://.* file://%s/PATH" CORE_IMAGE_EXTRA_INSTALL = "perl" """ % sstate_dir @@ -91,7 +93,7 @@ CORE_IMAGE_EXTRA_INSTALL = "perl" # Configure eSDK to use sstate mirror from poky sstate_config=""" -SDK_LOCAL_CONF_WHITELIST = "SSTATE_MIRRORS" +ESDK_LOCALCONF_ALLOW = "SSTATE_MIRRORS" SSTATE_MIRRORS = "file://.* file://%s/PATH" """ % bb_vars["SSTATE_DIR"] with open(os.path.join(cls.tmpdir_eSDKQA, 'conf', 'local.conf'), 'a+') as f: @@ -100,7 +102,7 @@ SSTATE_MIRRORS = "file://.* file://%s/PATH" @classmethod def tearDownClass(cls): for i in range(0, 10): - if os.path.exists(os.path.join(cls.tmpdir_eSDKQA, 'bitbake.lock')): + if os.path.exists(os.path.join(cls.tmpdir_eSDKQA, 'bitbake.lock')) or os.path.exists(os.path.join(cls.tmpdir_eSDKQA, 'cache/hashserv.db-wal')): time.sleep(1) else: break diff --git a/meta/lib/oeqa/selftest/cases/externalsrc.py b/meta/lib/oeqa/selftest/cases/externalsrc.py new file mode 100644 index 0000000000..1d800dc82c --- /dev/null +++ b/meta/lib/oeqa/selftest/cases/externalsrc.py @@ -0,0 +1,44 @@ +# +# Copyright OpenEmbedded Contributors +# +# SPDX-License-Identifier: MIT +# + +import os +import shutil +import tempfile + +from oeqa.selftest.case import OESelftestTestCase +from oeqa.utils.commands import get_bb_var, runCmd + +class ExternalSrc(OESelftestTestCase): + # test that srctree_hash_files does not crash + # we should be actually checking do_compile[file-checksums] but oeqa currently does not support it + # so we check only that a recipe with externalsrc can be parsed + def test_externalsrc_srctree_hash_files(self): + test_recipe = "git-submodule-test" + git_url = "git://git.yoctoproject.org/git-submodule-test" + externalsrc_dir = tempfile.TemporaryDirectory(prefix="externalsrc").name + + self.write_config( + """ +INHERIT += "externalsrc" +EXTERNALSRC:pn-%s = "%s" +""" % (test_recipe, externalsrc_dir) + ) + + # test with git without submodules + runCmd('git clone %s %s' % (git_url, externalsrc_dir)) + os.unlink(externalsrc_dir + "/.gitmodules") + open(".gitmodules", 'w').close() # local file .gitmodules in cwd should not affect externalsrc parsing + self.assertEqual(get_bb_var("S", test_recipe), externalsrc_dir, msg = "S does not equal to EXTERNALSRC") + os.unlink(".gitmodules") + + # test with git with submodules + runCmd('git checkout .gitmodules', cwd=externalsrc_dir) + runCmd('git submodule update --init --recursive', cwd=externalsrc_dir) + self.assertEqual(get_bb_var("S", test_recipe), externalsrc_dir, msg = "S does not equal to EXTERNALSRC") + + # test without git + shutil.rmtree(os.path.join(externalsrc_dir, ".git")) + self.assertEqual(get_bb_var("S", test_recipe), externalsrc_dir, msg = "S does not equal to EXTERNALSRC") diff --git a/meta/lib/oeqa/selftest/cases/fetch.py b/meta/lib/oeqa/selftest/cases/fetch.py index 76cbadf2ff..44099176fc 100644 --- a/meta/lib/oeqa/selftest/cases/fetch.py +++ b/meta/lib/oeqa/selftest/cases/fetch.py @@ -1,7 +1,12 @@ # +# Copyright OpenEmbedded Contributors +# # SPDX-License-Identifier: MIT # +import tempfile +import textwrap +import bb.tinfoil import oe.path from oeqa.selftest.case import OESelftestTestCase from oeqa.utils.commands import bitbake @@ -21,8 +26,8 @@ class Fetch(OESelftestTestCase): # No mirrors, should use git to fetch successfully features = """ DL_DIR = "%s" -MIRRORS_forcevariable = "" -PREMIRRORS_forcevariable = "" +MIRRORS:forcevariable = "" +PREMIRRORS:forcevariable = "" """ % dldir self.write_config(features) oe.path.remove(dldir, recurse=True) @@ -31,9 +36,10 @@ PREMIRRORS_forcevariable = "" # No mirrors and broken git, should fail features = """ DL_DIR = "%s" +SRC_URI:pn-dbus-wait = "git://git.yoctoproject.org/dbus-wait;branch=master;protocol=git" GIT_PROXY_COMMAND = "false" -MIRRORS_forcevariable = "" -PREMIRRORS_forcevariable = "" +MIRRORS:forcevariable = "" +PREMIRRORS:forcevariable = "" """ % dldir self.write_config(features) oe.path.remove(dldir, recurse=True) @@ -43,9 +49,62 @@ PREMIRRORS_forcevariable = "" # Broken git but a specific mirror features = """ DL_DIR = "%s" +SRC_URI:pn-dbus-wait = "git://git.yoctoproject.org/dbus-wait;branch=master;protocol=git" GIT_PROXY_COMMAND = "false" -MIRRORS_forcevariable = "git://.*/.* http://downloads.yoctoproject.org/mirror/sources/" +MIRRORS:forcevariable = "git://.*/.* http://downloads.yoctoproject.org/mirror/sources/" """ % dldir self.write_config(features) oe.path.remove(dldir, recurse=True) bitbake("dbus-wait -c fetch -f") + + +class Dependencies(OESelftestTestCase): + def write_recipe(self, content, tempdir): + f = os.path.join(tempdir, "test.bb") + with open(f, "w") as fd: + fd.write(content) + return f + + def test_dependencies(self): + """ + Verify that the correct dependencies are generated for specific SRC_URI entries. + """ + + with bb.tinfoil.Tinfoil() as tinfoil, tempfile.TemporaryDirectory(prefix="selftest-fetch") as tempdir: + tinfoil.prepare(config_only=False, quiet=2) + + r = """ + LICENSE="CLOSED" + SRC_URI="http://example.com/tarball.zip" + """ + f = self.write_recipe(textwrap.dedent(r), tempdir) + d = tinfoil.parse_recipe_file(f) + self.assertIn("wget-native", d.getVarFlag("do_fetch", "depends")) + self.assertIn("unzip-native", d.getVarFlag("do_unpack", "depends")) + + # Verify that the downloadfilename overrides the URI + r = """ + LICENSE="CLOSED" + SRC_URI="https://example.com/tarball;downloadfilename=something.zip" + """ + f = self.write_recipe(textwrap.dedent(r), tempdir) + d = tinfoil.parse_recipe_file(f) + self.assertIn("wget-native", d.getVarFlag("do_fetch", "depends")) + self.assertIn("unzip-native", d.getVarFlag("do_unpack", "depends") or "") + + r = """ + LICENSE="CLOSED" + SRC_URI="ftp://example.com/tarball.lz" + """ + f = self.write_recipe(textwrap.dedent(r), tempdir) + d = tinfoil.parse_recipe_file(f) + self.assertIn("wget-native", d.getVarFlag("do_fetch", "depends")) + self.assertIn("lzip-native", d.getVarFlag("do_unpack", "depends")) + + r = """ + LICENSE="CLOSED" + SRC_URI="git://example.com/repo;branch=master;rev=ffffffffffffffffffffffffffffffffffffffff" + """ + f = self.write_recipe(textwrap.dedent(r), tempdir) + d = tinfoil.parse_recipe_file(f) + self.assertIn("git-native", d.getVarFlag("do_fetch", "depends")) diff --git a/meta/lib/oeqa/selftest/cases/fitimage.py b/meta/lib/oeqa/selftest/cases/fitimage.py new file mode 100644 index 0000000000..0b5f4602fb --- /dev/null +++ b/meta/lib/oeqa/selftest/cases/fitimage.py @@ -0,0 +1,854 @@ +# +# Copyright OpenEmbedded Contributors +# +# SPDX-License-Identifier: MIT +# + +from oeqa.selftest.case import OESelftestTestCase +from oeqa.utils.commands import runCmd, bitbake, get_bb_var, get_bb_vars +import os +import re + +class FitImageTests(OESelftestTestCase): + + def _setup_uboot_tools_native(self): + """build u-boot-tools-native and return RECIPE_SYSROOT_NATIVE""" + bitbake("u-boot-tools-native -c addto_recipe_sysroot") + return get_bb_var('RECIPE_SYSROOT_NATIVE', 'u-boot-tools-native') + + def _verify_fit_image_signature(self, uboot_tools_sysroot_native, fitimage_path, dtb_path, conf_name=None): + """Verify the signature of a fit contfiguration + + The fit_check_sign utility from u-boot-tools-native is called. + uboot-fit_check_sign -f fitImage -k $dtb_name -c conf-$dtb_name + """ + fit_check_sign_path = os.path.join(uboot_tools_sysroot_native, 'usr', 'bin', 'uboot-fit_check_sign') + cmd = '%s -f %s -k %s' % (fit_check_sign_path, fitimage_path, dtb_path) + if conf_name: + cmd += ' -c %s' % conf_name + result = runCmd(cmd) + self.logger.debug("%s\nreturned: %s\n%s", cmd, str(result.status), result.output) + self.assertIn("Signature check OK", result.output) + + @staticmethod + def _find_string_in_bin_file(file_path, search_string): + """find stings in a binary file + + Shell equivalent: strings "$1" | grep "$2" | wc -l + return number of matches + """ + found_positions = 0 + with open(file_path, 'rb') as file: + byte = file.read(1) + current_position = 0 + current_match = 0 + while byte: + char = byte.decode('ascii', errors='ignore') + if char == search_string[current_match]: + current_match += 1 + if current_match == len(search_string): + found_positions += 1 + current_match = 0 + else: + current_match = 0 + current_position += 1 + byte = file.read(1) + return found_positions + + + def test_fit_image(self): + """ + Summary: Check if FIT image and Image Tree Source (its) are built + and the Image Tree Source has the correct fields. + Expected: 1. fitImage and fitImage-its can be built + 2. The type, load address, entrypoint address and + default values of kernel and ramdisk are as expected + in the Image Tree Source. Not all the fields are tested, + only the key fields that wont vary between different + architectures. + Product: oe-core + Author: Usama Arif <usama.arif@arm.com> + """ + config = """ +# Enable creation of fitImage +KERNEL_IMAGETYPE = "Image" +KERNEL_IMAGETYPES += " fitImage " +KERNEL_CLASSES = " kernel-fitimage " + +# RAM disk variables including load address and entrypoint for kernel and RAM disk +IMAGE_FSTYPES += "cpio.gz" +INITRAMFS_IMAGE = "core-image-minimal" +# core-image-minimal is used as initramfs here, drop the rootfs suffix +IMAGE_NAME_SUFFIX:pn-core-image-minimal = "" +UBOOT_RD_LOADADDRESS = "0x88000000" +UBOOT_RD_ENTRYPOINT = "0x88000000" +UBOOT_LOADADDRESS = "0x80080000" +UBOOT_ENTRYPOINT = "0x80080000" +FIT_DESC = "A model description" +""" + self.write_config(config) + + # fitImage is created as part of linux recipe + image = "virtual/kernel" + bitbake(image) + bb_vars = get_bb_vars(['DEPLOY_DIR_IMAGE', 'INITRAMFS_IMAGE_NAME', 'KERNEL_FIT_LINK_NAME'], image) + + fitimage_its_path = os.path.join(bb_vars['DEPLOY_DIR_IMAGE'], + "fitImage-its-%s-%s" % (bb_vars['INITRAMFS_IMAGE_NAME'], bb_vars['KERNEL_FIT_LINK_NAME'])) + fitimage_path = os.path.join(bb_vars['DEPLOY_DIR_IMAGE'], + "fitImage-%s-%s" % (bb_vars['INITRAMFS_IMAGE_NAME'], bb_vars['KERNEL_FIT_LINK_NAME'])) + + self.assertExists(fitimage_its_path, "%s image tree source doesn't exist" % (fitimage_its_path)) + self.assertExists(fitimage_path, "%s FIT image doesn't exist" % (fitimage_path)) + + # Check that the type, load address, entrypoint address and default + # values for kernel and ramdisk in Image Tree Source are as expected. + # The order of fields in the below array is important. Not all the + # fields are tested, only the key fields that wont vary between + # different architectures. + its_field_check = [ + 'description = "A model description";', + 'type = "kernel";', + 'load = <0x80080000>;', + 'entry = <0x80080000>;', + 'type = "ramdisk";', + 'load = <0x88000000>;', + 'entry = <0x88000000>;', + 'default = "conf-1";', + 'kernel = "kernel-1";', + 'ramdisk = "ramdisk-1";' + ] + + with open(fitimage_its_path) as its_file: + field_index = 0 + for line in its_file: + if field_index == len(its_field_check): + break + if its_field_check[field_index] in line: + field_index +=1 + + if field_index != len(its_field_check): # if its equal, the test passed + self.assertTrue(field_index == len(its_field_check), + "Fields in Image Tree Source File %s did not match, error in finding %s" + % (fitimage_its_path, its_field_check[field_index])) + + + def test_sign_fit_image(self): + """ + Summary: Check if FIT image and Image Tree Source (its) are created + and signed correctly. + Expected: 1) its and FIT image are built successfully + 2) Scanning the its file indicates signing is enabled + as requested by UBOOT_SIGN_ENABLE (using keys generated + via FIT_GENERATE_KEYS) + 3) Dumping the FIT image indicates signature values + are present (including for images as enabled via + FIT_SIGN_INDIVIDUAL) + 4) Examination of the do_assemble_fitimage runfile/logfile + indicate that UBOOT_MKIMAGE, UBOOT_MKIMAGE_SIGN and + UBOOT_MKIMAGE_SIGN_ARGS are working as expected. + Product: oe-core + Author: Paul Eggleton <paul.eggleton@microsoft.com> based upon + work by Usama Arif <usama.arif@arm.com> + """ + a_comment = "a smart comment" + config = """ +# Enable creation of fitImage +MACHINE = "beaglebone-yocto" +KERNEL_IMAGETYPES += " fitImage " +KERNEL_CLASSES = " kernel-fitimage " +UBOOT_SIGN_ENABLE = "1" +FIT_GENERATE_KEYS = "1" +UBOOT_SIGN_KEYDIR = "${TOPDIR}/signing-keys" +UBOOT_SIGN_IMG_KEYNAME = "img-oe-selftest" +UBOOT_SIGN_KEYNAME = "cfg-oe-selftest" +FIT_SIGN_INDIVIDUAL = "1" +UBOOT_MKIMAGE_SIGN_ARGS = "-c '%s'" +""" % a_comment + + self.write_config(config) + + # fitImage is created as part of linux recipe + image = "virtual/kernel" + bitbake(image) + bb_vars = get_bb_vars(['DEPLOY_DIR_IMAGE', 'KERNEL_FIT_LINK_NAME'], image) + + fitimage_its_path = os.path.join(bb_vars['DEPLOY_DIR_IMAGE'], + "fitImage-its-%s" % (bb_vars['KERNEL_FIT_LINK_NAME'])) + fitimage_path = os.path.join(bb_vars['DEPLOY_DIR_IMAGE'], + "fitImage-%s.bin" % (bb_vars['KERNEL_FIT_LINK_NAME'])) + + self.assertExists(fitimage_its_path, "%s image tree source doesn't exist" % (fitimage_its_path)) + self.assertExists(fitimage_path, "%s FIT image doesn't exist" % (fitimage_path)) + + req_itspaths = [ + ['/', 'images', 'kernel-1'], + ['/', 'images', 'kernel-1', 'signature-1'], + ['/', 'images', 'fdt-am335x-boneblack.dtb'], + ['/', 'images', 'fdt-am335x-boneblack.dtb', 'signature-1'], + ['/', 'configurations', 'conf-am335x-boneblack.dtb'], + ['/', 'configurations', 'conf-am335x-boneblack.dtb', 'signature-1'], + ] + + itspath = [] + itspaths = [] + linect = 0 + sigs = {} + with open(fitimage_its_path) as its_file: + linect += 1 + for line in its_file: + line = line.strip() + if line.endswith('};'): + itspath.pop() + elif line.endswith('{'): + itspath.append(line[:-1].strip()) + itspaths.append(itspath[:]) + elif itspath and itspath[-1] == 'signature-1': + itsdotpath = '.'.join(itspath) + if not itsdotpath in sigs: + sigs[itsdotpath] = {} + if not '=' in line or not line.endswith(';'): + self.fail('Unexpected formatting in %s sigs section line %d:%s' % (fitimage_its_path, linect, line)) + key, value = line.split('=', 1) + sigs[itsdotpath][key.rstrip()] = value.lstrip().rstrip(';') + + for reqpath in req_itspaths: + if not reqpath in itspaths: + self.fail('Missing section in its file: %s' % reqpath) + + reqsigvalues_image = { + 'algo': '"sha256,rsa2048"', + 'key-name-hint': '"img-oe-selftest"', + } + reqsigvalues_config = { + 'algo': '"sha256,rsa2048"', + 'key-name-hint': '"cfg-oe-selftest"', + 'sign-images': '"kernel", "fdt"', + } + + for itspath, values in sigs.items(): + if 'conf-' in itspath: + reqsigvalues = reqsigvalues_config + else: + reqsigvalues = reqsigvalues_image + for reqkey, reqvalue in reqsigvalues.items(): + value = values.get(reqkey, None) + if value is None: + self.fail('Missing key "%s" in its file signature section %s' % (reqkey, itspath)) + self.assertEqual(value, reqvalue) + + # Dump the image to see if it really got signed + uboot_tools_sysroot_native = self._setup_uboot_tools_native() + dumpimage_path = os.path.join(uboot_tools_sysroot_native, 'usr', 'bin', 'dumpimage') + result = runCmd('%s -l %s' % (dumpimage_path, fitimage_path)) + in_signed = None + signed_sections = {} + for line in result.output.splitlines(): + if line.startswith((' Configuration', ' Image')): + in_signed = re.search(r'\((.*)\)', line).groups()[0] + elif re.match('^ *', line) in (' ', ''): + in_signed = None + elif in_signed: + if not in_signed in signed_sections: + signed_sections[in_signed] = {} + key, value = line.split(':', 1) + signed_sections[in_signed][key.strip()] = value.strip() + self.assertIn('kernel-1', signed_sections) + self.assertIn('fdt-am335x-boneblack.dtb', signed_sections) + self.assertIn('conf-am335x-boneblack.dtb', signed_sections) + for signed_section, values in signed_sections.items(): + value = values.get('Sign algo', None) + if signed_section.startswith("conf"): + self.assertEqual(value, 'sha256,rsa2048:cfg-oe-selftest', 'Signature algorithm for %s not expected value' % signed_section) + else: + self.assertEqual(value, 'sha256,rsa2048:img-oe-selftest', 'Signature algorithm for %s not expected value' % signed_section) + value = values.get('Sign value', None) + self.assertEqual(len(value), 512, 'Signature value for section %s not expected length' % signed_section) + + # Search for the string passed to mkimage: 1 kernel + 3 DTBs + config per DTB = 7 sections + # Looks like mkimage supports to add a comment but does not support to read it back. + found_comments = FitImageTests._find_string_in_bin_file(fitimage_path, a_comment) + self.assertEqual(found_comments, 7, "Expected 7 signed and commented section in the fitImage.") + + # Verify the signature for all configurations = DTBs + for dtb in ['am335x-bone.dtb', 'am335x-boneblack.dtb', 'am335x-bonegreen.dtb']: + self._verify_fit_image_signature(uboot_tools_sysroot_native, fitimage_path, + os.path.join(bb_vars['DEPLOY_DIR_IMAGE'], dtb), 'conf-' + dtb) + + def test_uboot_fit_image(self): + """ + Summary: Check if Uboot FIT image and Image Tree Source + (its) are built and the Image Tree Source has the + correct fields. + Expected: 1. u-boot-fitImage and u-boot-its can be built + 2. The type, load address, entrypoint address and + default values of U-boot image are correct in the + Image Tree Source. Not all the fields are tested, + only the key fields that wont vary between + different architectures. + Product: oe-core + Author: Klaus Heinrich Kiwi <klaus@linux.vnet.ibm.com> + based on work by Usama Arif <usama.arif@arm.com> + """ + config = """ +# We need at least CONFIG_SPL_LOAD_FIT and CONFIG_SPL_OF_CONTROL set +MACHINE = "qemuarm" +UBOOT_MACHINE = "am57xx_evm_defconfig" +SPL_BINARY = "MLO" + +# Enable creation of the U-Boot fitImage +UBOOT_FITIMAGE_ENABLE = "1" + +# (U-boot) fitImage properties +UBOOT_LOADADDRESS = "0x80080000" +UBOOT_ENTRYPOINT = "0x80080000" +UBOOT_FIT_DESC = "A model description" + +# Enable creation of Kernel fitImage +KERNEL_IMAGETYPES += " fitImage " +KERNEL_CLASSES = " kernel-fitimage" +UBOOT_SIGN_ENABLE = "1" +FIT_GENERATE_KEYS = "1" +UBOOT_SIGN_KEYDIR = "${TOPDIR}/signing-keys" +UBOOT_SIGN_IMG_KEYNAME = "img-oe-selftest" +UBOOT_SIGN_KEYNAME = "cfg-oe-selftest" +FIT_SIGN_INDIVIDUAL = "1" +""" + self.write_config(config) + + # The U-Boot fitImage is created as part of the U-Boot recipe + bitbake("virtual/bootloader") + + deploy_dir_image = get_bb_var('DEPLOY_DIR_IMAGE') + machine = get_bb_var('MACHINE') + fitimage_its_path = os.path.join(deploy_dir_image, + "u-boot-its-%s" % (machine,)) + fitimage_path = os.path.join(deploy_dir_image, + "u-boot-fitImage-%s" % (machine,)) + + self.assertExists(fitimage_its_path, "%s image tree source doesn't exist" % (fitimage_its_path)) + self.assertExists(fitimage_path, "%s FIT image doesn't exist" % (fitimage_path)) + + # Check that the type, load address, entrypoint address and default + # values for kernel and ramdisk in Image Tree Source are as expected. + # The order of fields in the below array is important. Not all the + # fields are tested, only the key fields that wont vary between + # different architectures. + its_field_check = [ + 'description = "A model description";', + 'type = "standalone";', + 'load = <0x80080000>;', + 'entry = <0x80080000>;', + 'default = "conf";', + 'loadables = "uboot";', + 'fdt = "fdt";' + ] + + with open(fitimage_its_path) as its_file: + field_index = 0 + for line in its_file: + if field_index == len(its_field_check): + break + if its_field_check[field_index] in line: + field_index +=1 + + if field_index != len(its_field_check): # if its equal, the test passed + self.assertTrue(field_index == len(its_field_check), + "Fields in Image Tree Source File %s did not match, error in finding %s" + % (fitimage_its_path, its_field_check[field_index])) + + def test_uboot_sign_fit_image(self): + """ + Summary: Check if Uboot FIT image and Image Tree Source + (its) are built and the Image Tree Source has the + correct fields, in the scenario where the Kernel + is also creating/signing it's fitImage. + Expected: 1. u-boot-fitImage and u-boot-its can be built + 2. The type, load address, entrypoint address and + default values of U-boot image are correct in the + Image Tree Source. Not all the fields are tested, + only the key fields that wont vary between + different architectures. + Product: oe-core + Author: Klaus Heinrich Kiwi <klaus@linux.vnet.ibm.com> + based on work by Usama Arif <usama.arif@arm.com> + """ + config = """ +# We need at least CONFIG_SPL_LOAD_FIT and CONFIG_SPL_OF_CONTROL set +MACHINE = "qemuarm" +UBOOT_MACHINE = "am57xx_evm_defconfig" +SPL_BINARY = "MLO" + +# Enable creation of the U-Boot fitImage +UBOOT_FITIMAGE_ENABLE = "1" + +# (U-boot) fitImage properties +UBOOT_LOADADDRESS = "0x80080000" +UBOOT_ENTRYPOINT = "0x80080000" +UBOOT_FIT_DESC = "A model description" +KERNEL_IMAGETYPES += " fitImage " +KERNEL_CLASSES = " kernel-fitimage " +UBOOT_SIGN_ENABLE = "1" +FIT_GENERATE_KEYS = "1" +UBOOT_SIGN_KEYDIR = "${TOPDIR}/signing-keys" +UBOOT_SIGN_IMG_KEYNAME = "img-oe-selftest" +UBOOT_SIGN_KEYNAME = "cfg-oe-selftest" +FIT_SIGN_INDIVIDUAL = "1" +UBOOT_MKIMAGE_SIGN_ARGS = "-c 'a smart U-Boot comment'" +""" + self.write_config(config) + + # The U-Boot fitImage is created as part of the U-Boot recipe + bitbake("virtual/bootloader") + + deploy_dir_image = get_bb_var('DEPLOY_DIR_IMAGE') + machine = get_bb_var('MACHINE') + fitimage_its_path = os.path.join(deploy_dir_image, + "u-boot-its-%s" % (machine,)) + fitimage_path = os.path.join(deploy_dir_image, + "u-boot-fitImage-%s" % (machine,)) + + self.assertExists(fitimage_its_path, "%s image tree source doesn't exist" % (fitimage_its_path)) + self.assertExists(fitimage_path, "%s FIT image doesn't exist" % (fitimage_path)) + + # Check that the type, load address, entrypoint address and default + # values for kernel and ramdisk in Image Tree Source are as expected. + # The order of fields in the below array is important. Not all the + # fields are tested, only the key fields that wont vary between + # different architectures. + its_field_check = [ + 'description = "A model description";', + 'type = "standalone";', + 'load = <0x80080000>;', + 'entry = <0x80080000>;', + 'default = "conf";', + 'loadables = "uboot";', + 'fdt = "fdt";' + ] + + with open(fitimage_its_path) as its_file: + field_index = 0 + for line in its_file: + if field_index == len(its_field_check): + break + if its_field_check[field_index] in line: + field_index +=1 + + if field_index != len(its_field_check): # if its equal, the test passed + self.assertTrue(field_index == len(its_field_check), + "Fields in Image Tree Source File %s did not match, error in finding %s" + % (fitimage_its_path, its_field_check[field_index])) + + + def test_sign_standalone_uboot_fit_image(self): + """ + Summary: Check if U-Boot FIT image and Image Tree Source (its) are + created and signed correctly for the scenario where only + the U-Boot proper fitImage is being created and signed. + Expected: 1) U-Boot its and FIT image are built successfully + 2) Scanning the its file indicates signing is enabled + as requested by SPL_SIGN_ENABLE (using keys generated + via UBOOT_FIT_GENERATE_KEYS) + 3) Dumping the FIT image indicates signature values + are present + 4) Examination of the do_uboot_assemble_fitimage + runfile/logfile indicate that UBOOT_MKIMAGE, UBOOT_MKIMAGE_SIGN + and SPL_MKIMAGE_SIGN_ARGS are working as expected. + Product: oe-core + Author: Klaus Heinrich Kiwi <klaus@linux.vnet.ibm.com> based upon + work by Paul Eggleton <paul.eggleton@microsoft.com> and + Usama Arif <usama.arif@arm.com> + """ + a_comment = "a smart U-Boot comment" + config = """ +# There's no U-boot deconfig with CONFIG_FIT_SIGNATURE yet, so we need at +# least CONFIG_SPL_LOAD_FIT and CONFIG_SPL_OF_CONTROL set +MACHINE = "qemuarm" +UBOOT_MACHINE = "am57xx_evm_defconfig" +SPL_BINARY = "MLO" +# The kernel-fitimage class is a dependency even if we're only +# creating/signing the U-Boot fitImage +KERNEL_CLASSES = " kernel-fitimage" +# Enable creation and signing of the U-Boot fitImage +UBOOT_FITIMAGE_ENABLE = "1" +SPL_SIGN_ENABLE = "1" +SPL_SIGN_KEYNAME = "spl-oe-selftest" +SPL_SIGN_KEYDIR = "${TOPDIR}/signing-keys" +UBOOT_DTB_BINARY = "u-boot.dtb" +UBOOT_ENTRYPOINT = "0x80000000" +UBOOT_LOADADDRESS = "0x80000000" +UBOOT_DTB_LOADADDRESS = "0x82000000" +UBOOT_ARCH = "arm" +SPL_MKIMAGE_DTCOPTS = "-I dts -O dtb -p 2000" +SPL_MKIMAGE_SIGN_ARGS = "-c '%s'" +UBOOT_EXTLINUX = "0" +UBOOT_FIT_GENERATE_KEYS = "1" +UBOOT_FIT_HASH_ALG = "sha256" +""" % a_comment + + self.write_config(config) + + # The U-Boot fitImage is created as part of the U-Boot recipe + bitbake("virtual/bootloader") + + deploy_dir_image = get_bb_var('DEPLOY_DIR_IMAGE') + machine = get_bb_var('MACHINE') + fitimage_its_path = os.path.join(deploy_dir_image, + "u-boot-its-%s" % (machine,)) + fitimage_path = os.path.join(deploy_dir_image, + "u-boot-fitImage-%s" % (machine,)) + + self.assertExists(fitimage_its_path, "%s image tree source doesn't exist" % (fitimage_its_path)) + self.assertExists(fitimage_path, "%s FIT image doesn't exist" % (fitimage_path)) + + req_itspaths = [ + ['/', 'images', 'uboot'], + ['/', 'images', 'uboot', 'signature'], + ['/', 'images', 'fdt'], + ['/', 'images', 'fdt', 'signature'], + ] + + itspath = [] + itspaths = [] + linect = 0 + sigs = {} + with open(fitimage_its_path) as its_file: + linect += 1 + for line in its_file: + line = line.strip() + if line.endswith('};'): + itspath.pop() + elif line.endswith('{'): + itspath.append(line[:-1].strip()) + itspaths.append(itspath[:]) + elif itspath and itspath[-1] == 'signature': + itsdotpath = '.'.join(itspath) + if not itsdotpath in sigs: + sigs[itsdotpath] = {} + if not '=' in line or not line.endswith(';'): + self.fail('Unexpected formatting in %s sigs section line %d:%s' % (fitimage_its_path, linect, line)) + key, value = line.split('=', 1) + sigs[itsdotpath][key.rstrip()] = value.lstrip().rstrip(';') + + for reqpath in req_itspaths: + if not reqpath in itspaths: + self.fail('Missing section in its file: %s' % reqpath) + + reqsigvalues_image = { + 'algo': '"sha256,rsa2048"', + 'key-name-hint': '"spl-oe-selftest"', + } + + for itspath, values in sigs.items(): + reqsigvalues = reqsigvalues_image + for reqkey, reqvalue in reqsigvalues.items(): + value = values.get(reqkey, None) + if value is None: + self.fail('Missing key "%s" in its file signature section %s' % (reqkey, itspath)) + self.assertEqual(value, reqvalue) + + # Dump the image to see if it really got signed + uboot_tools_sysroot_native = self._setup_uboot_tools_native() + dumpimage_path = os.path.join(uboot_tools_sysroot_native, 'usr', 'bin', 'dumpimage') + result = runCmd('%s -l %s' % (dumpimage_path, fitimage_path)) + in_signed = None + signed_sections = {} + for line in result.output.splitlines(): + if line.startswith((' Image')): + in_signed = re.search(r'\((.*)\)', line).groups()[0] + elif re.match(' \w', line): + in_signed = None + elif in_signed: + if not in_signed in signed_sections: + signed_sections[in_signed] = {} + key, value = line.split(':', 1) + signed_sections[in_signed][key.strip()] = value.strip() + self.assertIn('uboot', signed_sections) + self.assertIn('fdt', signed_sections) + for signed_section, values in signed_sections.items(): + value = values.get('Sign algo', None) + self.assertEqual(value, 'sha256,rsa2048:spl-oe-selftest', 'Signature algorithm for %s not expected value' % signed_section) + value = values.get('Sign value', None) + self.assertEqual(len(value), 512, 'Signature value for section %s not expected length' % signed_section) + + # Check for SPL_MKIMAGE_SIGN_ARGS + # Looks like mkimage supports to add a comment but does not support to read it back. + found_comments = FitImageTests._find_string_in_bin_file(fitimage_path, a_comment) + self.assertEqual(found_comments, 2, "Expected 2 signed and commented section in the fitImage.") + + # Verify the signature + self._verify_fit_image_signature(uboot_tools_sysroot_native, fitimage_path, + os.path.join(deploy_dir_image, 'u-boot-spl.dtb')) + + + def test_sign_cascaded_uboot_fit_image(self): + """ + Summary: Check if U-Boot FIT image and Image Tree Source (its) are + created and signed correctly for the scenario where both + U-Boot proper and Kernel fitImages are being created and + signed. + Expected: 1) U-Boot its and FIT image are built successfully + 2) Scanning the its file indicates signing is enabled + as requested by SPL_SIGN_ENABLE (using keys generated + via UBOOT_FIT_GENERATE_KEYS) + 3) Dumping the FIT image indicates signature values + are present + 4) Examination of the do_uboot_assemble_fitimage + runfile/logfile indicate that UBOOT_MKIMAGE, UBOOT_MKIMAGE_SIGN + and SPL_MKIMAGE_SIGN_ARGS are working as expected. + Product: oe-core + Author: Klaus Heinrich Kiwi <klaus@linux.vnet.ibm.com> based upon + work by Paul Eggleton <paul.eggleton@microsoft.com> and + Usama Arif <usama.arif@arm.com> + """ + a_comment = "a smart cascaded U-Boot comment" + config = """ +# There's no U-boot deconfig with CONFIG_FIT_SIGNATURE yet, so we need at +# least CONFIG_SPL_LOAD_FIT and CONFIG_SPL_OF_CONTROL set +MACHINE = "qemuarm" +UBOOT_MACHINE = "am57xx_evm_defconfig" +SPL_BINARY = "MLO" +# Enable creation and signing of the U-Boot fitImage +UBOOT_FITIMAGE_ENABLE = "1" +SPL_SIGN_ENABLE = "1" +SPL_SIGN_KEYNAME = "spl-cascaded-oe-selftest" +SPL_SIGN_KEYDIR = "${TOPDIR}/signing-keys" +UBOOT_DTB_BINARY = "u-boot.dtb" +UBOOT_ENTRYPOINT = "0x80000000" +UBOOT_LOADADDRESS = "0x80000000" +UBOOT_MKIMAGE_DTCOPTS = "-I dts -O dtb -p 2000" +UBOOT_MKIMAGE_SIGN_ARGS = "-c '%s'" +UBOOT_DTB_LOADADDRESS = "0x82000000" +UBOOT_ARCH = "arm" +SPL_MKIMAGE_DTCOPTS = "-I dts -O dtb -p 2000" +SPL_MKIMAGE_SIGN_ARGS = "-c 'a smart cascaded U-Boot comment'" +UBOOT_EXTLINUX = "0" +UBOOT_FIT_GENERATE_KEYS = "1" +UBOOT_FIT_HASH_ALG = "sha256" +KERNEL_IMAGETYPES += " fitImage " +KERNEL_CLASSES = " kernel-fitimage " +UBOOT_SIGN_ENABLE = "1" +FIT_GENERATE_KEYS = "1" +UBOOT_SIGN_KEYDIR = "${TOPDIR}/signing-keys" +UBOOT_SIGN_IMG_KEYNAME = "img-oe-selftest" +UBOOT_SIGN_KEYNAME = "cfg-oe-selftest" +FIT_SIGN_INDIVIDUAL = "1" +""" % a_comment + self.write_config(config) + + # The U-Boot fitImage is created as part of the U-Boot recipe + bitbake("virtual/bootloader") + + deploy_dir_image = get_bb_var('DEPLOY_DIR_IMAGE') + machine = get_bb_var('MACHINE') + fitimage_its_path = os.path.join(deploy_dir_image, + "u-boot-its-%s" % (machine,)) + fitimage_path = os.path.join(deploy_dir_image, + "u-boot-fitImage-%s" % (machine,)) + + self.assertExists(fitimage_its_path, "%s image tree source doesn't exist" % (fitimage_its_path)) + self.assertExists(fitimage_path, "%s FIT image doesn't exist" % (fitimage_path)) + + req_itspaths = [ + ['/', 'images', 'uboot'], + ['/', 'images', 'uboot', 'signature'], + ['/', 'images', 'fdt'], + ['/', 'images', 'fdt', 'signature'], + ] + + itspath = [] + itspaths = [] + linect = 0 + sigs = {} + with open(fitimage_its_path) as its_file: + linect += 1 + for line in its_file: + line = line.strip() + if line.endswith('};'): + itspath.pop() + elif line.endswith('{'): + itspath.append(line[:-1].strip()) + itspaths.append(itspath[:]) + elif itspath and itspath[-1] == 'signature': + itsdotpath = '.'.join(itspath) + if not itsdotpath in sigs: + sigs[itsdotpath] = {} + if not '=' in line or not line.endswith(';'): + self.fail('Unexpected formatting in %s sigs section line %d:%s' % (fitimage_its_path, linect, line)) + key, value = line.split('=', 1) + sigs[itsdotpath][key.rstrip()] = value.lstrip().rstrip(';') + + for reqpath in req_itspaths: + if not reqpath in itspaths: + self.fail('Missing section in its file: %s' % reqpath) + + reqsigvalues_image = { + 'algo': '"sha256,rsa2048"', + 'key-name-hint': '"spl-cascaded-oe-selftest"', + } + + for itspath, values in sigs.items(): + reqsigvalues = reqsigvalues_image + for reqkey, reqvalue in reqsigvalues.items(): + value = values.get(reqkey, None) + if value is None: + self.fail('Missing key "%s" in its file signature section %s' % (reqkey, itspath)) + self.assertEqual(value, reqvalue) + + # Dump the image to see if it really got signed + uboot_tools_sysroot_native = self._setup_uboot_tools_native() + dumpimage_path = os.path.join(uboot_tools_sysroot_native, 'usr', 'bin', 'dumpimage') + result = runCmd('%s -l %s' % (dumpimage_path, fitimage_path)) + in_signed = None + signed_sections = {} + for line in result.output.splitlines(): + if line.startswith((' Image')): + in_signed = re.search(r'\((.*)\)', line).groups()[0] + elif re.match(' \w', line): + in_signed = None + elif in_signed: + if not in_signed in signed_sections: + signed_sections[in_signed] = {} + key, value = line.split(':', 1) + signed_sections[in_signed][key.strip()] = value.strip() + self.assertIn('uboot', signed_sections) + self.assertIn('fdt', signed_sections) + for signed_section, values in signed_sections.items(): + value = values.get('Sign algo', None) + self.assertEqual(value, 'sha256,rsa2048:spl-cascaded-oe-selftest', 'Signature algorithm for %s not expected value' % signed_section) + value = values.get('Sign value', None) + self.assertEqual(len(value), 512, 'Signature value for section %s not expected length' % signed_section) + + # Check for SPL_MKIMAGE_SIGN_ARGS + # Looks like mkimage supports to add a comment but does not support to read it back. + found_comments = FitImageTests._find_string_in_bin_file(fitimage_path, a_comment) + self.assertEqual(found_comments, 2, "Expected 2 signed and commented section in the fitImage.") + + # Verify the signature + self._verify_fit_image_signature(uboot_tools_sysroot_native, fitimage_path, + os.path.join(deploy_dir_image, 'u-boot-spl.dtb')) + + + def test_initramfs_bundle(self): + """ + Summary: Verifies the content of the initramfs bundle node in the FIT Image Tree Source (its) + The FIT settings are set by the test case. + The machine used is beaglebone-yocto. + Expected: 1. The ITS is generated with initramfs bundle support + 2. All the fields in the kernel node are as expected (matching the + conf settings) + 3. The kernel is included in all the available configurations and + its hash is included in the configuration signature + + Product: oe-core + Author: Abdellatif El Khlifi <abdellatif.elkhlifi@arm.com> + """ + + config = """ +DISTRO="poky" +MACHINE = "beaglebone-yocto" +INITRAMFS_IMAGE_BUNDLE = "1" +INITRAMFS_IMAGE = "core-image-minimal-initramfs" +INITRAMFS_SCRIPTS = "" +UBOOT_MACHINE = "am335x_evm_defconfig" +KERNEL_CLASSES = " kernel-fitimage " +KERNEL_IMAGETYPES = "fitImage" +UBOOT_SIGN_ENABLE = "1" +UBOOT_SIGN_KEYNAME = "beaglebonekey" +UBOOT_SIGN_KEYDIR ?= "${DEPLOY_DIR_IMAGE}" +UBOOT_DTB_BINARY = "u-boot.dtb" +UBOOT_ENTRYPOINT = "0x80000000" +UBOOT_LOADADDRESS = "0x80000000" +UBOOT_DTB_LOADADDRESS = "0x82000000" +UBOOT_ARCH = "arm" +UBOOT_MKIMAGE_DTCOPTS = "-I dts -O dtb -p 2000" +UBOOT_MKIMAGE_KERNEL_TYPE = "kernel" +UBOOT_EXTLINUX = "0" +FIT_GENERATE_KEYS = "1" +KERNEL_IMAGETYPE_REPLACEMENT = "zImage" +FIT_KERNEL_COMP_ALG = "none" +FIT_HASH_ALG = "sha256" +""" + self.write_config(config) + + # fitImage is created as part of linux recipe + bitbake("virtual/kernel") + + bb_vars = get_bb_vars([ + 'DEPLOY_DIR_IMAGE', + 'FIT_HASH_ALG', + 'FIT_KERNEL_COMP_ALG', + 'INITRAMFS_IMAGE', + 'MACHINE', + 'UBOOT_ARCH', + 'UBOOT_ENTRYPOINT', + 'UBOOT_LOADADDRESS', + 'UBOOT_MKIMAGE_KERNEL_TYPE' + ], + 'virtual/kernel') + fitimage_its_path = os.path.join(bb_vars['DEPLOY_DIR_IMAGE'], + "fitImage-its-%s-%s-%s" % (bb_vars['INITRAMFS_IMAGE'], bb_vars['MACHINE'], bb_vars['MACHINE'])) + fitimage_path = os.path.join(bb_vars['DEPLOY_DIR_IMAGE'],"fitImage") + + self.assertExists(fitimage_its_path, "%s image tree source doesn't exist" % (fitimage_its_path)) + self.assertExists(fitimage_path, "%s FIT image doesn't exist" % (fitimage_path)) + + its_file = open(fitimage_its_path) + + its_lines = [line.strip() for line in its_file.readlines()] + + exp_node_lines = [ + 'kernel-1 {', + 'description = "Linux kernel";', + 'data = /incbin/("linux.bin");', + 'type = "' + str(bb_vars['UBOOT_MKIMAGE_KERNEL_TYPE']) + '";', + 'arch = "' + str(bb_vars['UBOOT_ARCH']) + '";', + 'os = "linux";', + 'compression = "' + str(bb_vars['FIT_KERNEL_COMP_ALG']) + '";', + 'load = <' + str(bb_vars['UBOOT_LOADADDRESS']) + '>;', + 'entry = <' + str(bb_vars['UBOOT_ENTRYPOINT']) + '>;', + 'hash-1 {', + 'algo = "' + str(bb_vars['FIT_HASH_ALG']) +'";', + '};', + '};' + ] + + node_str = exp_node_lines[0] + + print ("checking kernel node\n") + self.assertIn(node_str, its_lines) + + node_start_idx = its_lines.index(node_str) + node = its_lines[node_start_idx:(node_start_idx + len(exp_node_lines))] + + # Remove the absolute path. This refers to WORKDIR which is not always predictable. + re_data = re.compile(r'^data = /incbin/\(.*/linux\.bin"\);$') + node = [re.sub(re_data, 'data = /incbin/("linux.bin");', cfg_str) for cfg_str in node] + + self.assertEqual(node, exp_node_lines, "kernel node does not match expectation") + + rx_configs = re.compile("^conf-.*") + its_configs = list(filter(rx_configs.match, its_lines)) + + for cfg_str in its_configs: + cfg_start_idx = its_lines.index(cfg_str) + line_idx = cfg_start_idx + 2 + node_end = False + while node_end == False: + if its_lines[line_idx] == "};" and its_lines[line_idx-1] == "};" : + node_end = True + line_idx = line_idx + 1 + + node = its_lines[cfg_start_idx:line_idx] + print("checking configuration " + cfg_str.rstrip(" {")) + rx_desc_line = re.compile(r'^description = ".*Linux kernel.*') + self.assertEqual(len(list(filter(rx_desc_line.match, node))), 1, "kernel keyword not found in the description line") + + self.assertIn('kernel = "kernel-1";', node) + + rx_sign_line = re.compile(r'^sign-images = .*kernel.*') + self.assertEqual(len(list(filter(rx_sign_line.match, node))), 1, "kernel hash not signed") + + # Verify the signature + uboot_tools_sysroot_native = self._setup_uboot_tools_native() + self._verify_fit_image_signature(uboot_tools_sysroot_native, fitimage_path, os.path.join(bb_vars['DEPLOY_DIR_IMAGE'], 'am335x-bone.dtb')) diff --git a/meta/lib/oeqa/selftest/cases/gcc.py b/meta/lib/oeqa/selftest/cases/gcc.py index 3efe15228f..4cc0894d42 100644 --- a/meta/lib/oeqa/selftest/cases/gcc.py +++ b/meta/lib/oeqa/selftest/cases/gcc.py @@ -1,9 +1,14 @@ +# +# Copyright OpenEmbedded Contributors +# # SPDX-License-Identifier: MIT +# import os +import time from oeqa.core.decorator import OETestTag from oeqa.core.case import OEPTestResultTestCase from oeqa.selftest.case import OESelftestTestCase -from oeqa.utils.commands import bitbake, get_bb_var, get_bb_vars, runqemu, Command +from oeqa.utils.commands import bitbake, get_bb_var, get_bb_vars, runqemu def parse_values(content): for i in content: @@ -32,15 +37,20 @@ class GccSelfTestBase(OESelftestTestCase, OEPTestResultTestCase): features = [] features.append('MAKE_CHECK_TARGETS = "{0}"'.format(" ".join(targets))) if ssh is not None: - features.append('TOOLCHAIN_TEST_TARGET = "ssh"') + features.append('TOOLCHAIN_TEST_TARGET = "linux-ssh"') features.append('TOOLCHAIN_TEST_HOST = "{0}"'.format(ssh)) features.append('TOOLCHAIN_TEST_HOST_USER = "root"') features.append('TOOLCHAIN_TEST_HOST_PORT = "22"') self.write_config("\n".join(features)) recipe = "gcc-runtime" + + start_time = time.time() + bitbake("{} -c check".format(recipe)) + end_time = time.time() + bb_vars = get_bb_vars(["B", "TARGET_SYS"], recipe) builddir, target_sys = bb_vars["B"], bb_vars["TARGET_SYS"] @@ -54,7 +64,7 @@ class GccSelfTestBase(OESelftestTestCase, OEPTestResultTestCase): ptestsuite = "gcc-{}".format(suite) if suite != "gcc" else suite ptestsuite = ptestsuite + "-user" if ssh is None else ptestsuite - self.ptest_section(ptestsuite, logfile = logpath) + self.ptest_section(ptestsuite, duration = int(end_time - start_time), logfile = logpath) with open(sumspath, "r") as f: for test, result in parse_values(f): self.ptest_result(ptestsuite, test, result) @@ -114,37 +124,44 @@ class GccLibItmSelfTest(GccSelfTestBase): self.run_check("libitm") @OETestTag("toolchain-system") +@OETestTag("runqemu") class GccCrossSelfTestSystemEmulated(GccSelfTestBase): def test_cross_gcc(self): self.run_check_emulated("gcc") @OETestTag("toolchain-system") +@OETestTag("runqemu") class GxxCrossSelfTestSystemEmulated(GccSelfTestBase): def test_cross_gxx(self): self.run_check_emulated("g++") @OETestTag("toolchain-system") +@OETestTag("runqemu") class GccLibAtomicSelfTestSystemEmulated(GccSelfTestBase): def test_libatomic(self): self.run_check_emulated("libatomic") @OETestTag("toolchain-system") +@OETestTag("runqemu") class GccLibGompSelfTestSystemEmulated(GccSelfTestBase): def test_libgomp(self): self.run_check_emulated("libgomp") @OETestTag("toolchain-system") +@OETestTag("runqemu") class GccLibStdCxxSelfTestSystemEmulated(GccSelfTestBase): def test_libstdcxx(self): self.run_check_emulated("libstdc++-v3") @OETestTag("toolchain-system") +@OETestTag("runqemu") class GccLibSspSelfTestSystemEmulated(GccSelfTestBase): def test_libssp(self): self.check_skip("libssp") self.run_check_emulated("libssp") @OETestTag("toolchain-system") +@OETestTag("runqemu") class GccLibItmSelfTestSystemEmulated(GccSelfTestBase): def test_libitm(self): self.check_skip("libitm") diff --git a/meta/lib/oeqa/selftest/cases/gdbserver.py b/meta/lib/oeqa/selftest/cases/gdbserver.py new file mode 100644 index 0000000000..9da97ae780 --- /dev/null +++ b/meta/lib/oeqa/selftest/cases/gdbserver.py @@ -0,0 +1,67 @@ +# +# Copyright OpenEmbedded Contributors +# +# SPDX-License-Identifier: MIT +# +import os +import time +import tempfile +import shutil +import concurrent.futures + +from oeqa.selftest.case import OESelftestTestCase +from oeqa.utils.commands import bitbake, get_bb_var, get_bb_vars , runqemu, runCmd + +class GdbServerTest(OESelftestTestCase): + def test_gdb_server(self): + target_arch = self.td["TARGET_ARCH"] + target_sys = self.td["TARGET_SYS"] + + features = """ +IMAGE_GEN_DEBUGFS = "1" +IMAGE_FSTYPES_DEBUGFS = "tar.bz2" +CORE_IMAGE_EXTRA_INSTALL = "gdbserver" + """ + self.write_config(features) + + gdb_recipe = "gdb-cross-" + target_arch + gdb_binary = target_sys + "-gdb" + + bitbake("core-image-minimal %s:do_addto_recipe_sysroot" % gdb_recipe) + + native_sysroot = get_bb_var("RECIPE_SYSROOT_NATIVE", gdb_recipe) + r = runCmd("%s --version" % gdb_binary, native_sysroot=native_sysroot, target_sys=target_sys) + self.assertEqual(r.status, 0) + self.assertIn("GNU gdb", r.output) + image = 'core-image-minimal' + bb_vars = get_bb_vars(['DEPLOY_DIR_IMAGE', 'IMAGE_LINK_NAME'], image) + + with tempfile.TemporaryDirectory(prefix="debugfs-") as debugfs: + filename = os.path.join(bb_vars['DEPLOY_DIR_IMAGE'], "%s-dbg.tar.bz2" % bb_vars['IMAGE_LINK_NAME']) + shutil.unpack_archive(filename, debugfs) + filename = os.path.join(bb_vars['DEPLOY_DIR_IMAGE'], "%s.tar.bz2" % bb_vars['IMAGE_LINK_NAME']) + shutil.unpack_archive(filename, debugfs) + + with runqemu("core-image-minimal", runqemuparams="nographic") as qemu: + status, output = qemu.run_serial("kmod --help") + self.assertIn("modprobe", output) + + with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor: + def run_gdb(): + for _ in range(5): + time.sleep(2) + cmd = "%s --batch -ex 'set sysroot %s' -ex \"target extended-remote %s:9999\" -ex \"info line kmod_help\"" % (gdb_binary, debugfs, qemu.ip) + self.logger.warning("starting gdb %s" % cmd) + r = runCmd(cmd, native_sysroot=native_sysroot, target_sys=target_sys) + self.assertEqual(0, r.status) + line_re = r"Line \d+ of \"/usr/src/debug/kmod/.*/tools/kmod.c\" starts at address 0x[0-9A-Fa-f]+ <kmod_help>" + self.assertRegex(r.output, line_re) + break + else: + self.fail("Timed out connecting to gdb") + future = executor.submit(run_gdb) + + status, output = qemu.run_serial("gdbserver --once :9999 kmod --help") + self.assertEqual(status, 1) + # The future either returns None, or raises an exception + future.result() diff --git a/meta/lib/oeqa/selftest/cases/gitarchivetests.py b/meta/lib/oeqa/selftest/cases/gitarchivetests.py new file mode 100644 index 0000000000..71382089c1 --- /dev/null +++ b/meta/lib/oeqa/selftest/cases/gitarchivetests.py @@ -0,0 +1,136 @@ +# +# Copyright OpenEmbedded Contributors +# +# SPDX-License-Identifier: MIT +# + +import os +import sys +basepath = os.path.abspath(os.path.dirname(__file__) + '/../../../../../') +lib_path = basepath + '/scripts/lib' +sys.path = sys.path + [lib_path] +import oeqa.utils.gitarchive as ga +from oeqa.utils.git import GitError +import tempfile +import shutil +import scriptutils +import logging +from oeqa.selftest.case import OESelftestTestCase + +logger = scriptutils.logger_create('resulttool') + +def create_fake_repository(commit, tag_list=[], add_remote=True): + """ Create a testing git directory + + Initialize a simple git repository with one initial commit, and as many + tags on this commit as listed in tag_list + Returns both git directory path and gitarchive git object + If commit is true, fake data will be commited, otherwise it will stay in staging area + If commit is true and tag_lsit is non empty, all tags in tag_list will be + created on the initial commit + Fake remote will also be added to make git ls-remote work + """ + fake_data_file = "fake_data.txt" + tempdir = tempfile.mkdtemp(prefix='fake_results.') + repo = ga.init_git_repo(tempdir, False, False, logger) + if add_remote: + repo.run_cmd(["remote", "add", "origin", "."]) + with open(os.path.join(tempdir, fake_data_file), "w") as fake_data: + fake_data.write("Fake data") + if commit: + repo.run_cmd(["add", fake_data_file]) + repo.run_cmd(["commit", "-m", "\"Add fake data\""]) + for tag in tag_list: + repo.run_cmd(["tag", tag]) + + return tempdir, repo + +def delete_fake_repository(path): + shutil.rmtree(path) + +def tag_exists(git_obj, target_tag): + for tag in git_obj.run_cmd(["tag"]).splitlines(): + if target_tag == tag: + return True + return False + +class GitArchiveTests(OESelftestTestCase): + TEST_BRANCH="main" + TEST_COMMIT="0f7d5df" + TEST_COMMIT_COUNT="42" + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.log = logging.getLogger('gitarchivetests') + cls.log.setLevel(logging.DEBUG) + + def test_create_first_test_tag(self): + path, git_obj = create_fake_repository(False) + keywords = {'commit': self.TEST_COMMIT, 'branch': self.TEST_BRANCH, "commit_count": self.TEST_COMMIT_COUNT} + target_tag = f"{self.TEST_BRANCH}/{self.TEST_COMMIT_COUNT}-g{self.TEST_COMMIT}/0" + + ga.gitarchive(path, path, True, False, + "Results of {branch}:{commit}", "branch: {branch}\ncommit: {commit}", "{branch}", + False, "{branch}/{commit_count}-g{commit}/{tag_number}", + 'Test run #{tag_number} of {branch}:{commit}', '', + [], [], False, keywords, logger) + self.assertTrue(tag_exists(git_obj, target_tag), msg=f"Tag {target_tag} has not been created") + delete_fake_repository(path) + + def test_create_second_test_tag(self): + first_tag = f"{self.TEST_BRANCH}/{self.TEST_COMMIT_COUNT}-g{self.TEST_COMMIT}/0" + second_tag = f"{self.TEST_BRANCH}/{self.TEST_COMMIT_COUNT}-g{self.TEST_COMMIT}/1" + keywords = {'commit': self.TEST_COMMIT, 'branch': self.TEST_BRANCH, "commit_count": self.TEST_COMMIT_COUNT} + + path, git_obj = create_fake_repository(True, [first_tag]) + ga.gitarchive(path, path, True, False, + "Results of {branch}:{commit}", "branch: {branch}\ncommit: {commit}", "{branch}", + False, "{branch}/{commit_count}-g{commit}/{tag_number}", + 'Test run #{tag_number} of {branch}:{commit}', '', + [], [], False, keywords, logger) + self.assertTrue(tag_exists(git_obj, second_tag), msg=f"Second tag {second_tag} has not been created") + delete_fake_repository(path) + + def test_get_revs_on_branch(self): + fake_tags_list=["main/10-g0f7d5df/0", "main/10-g0f7d5df/1", "foo/20-g2468f5d/0"] + tag_name = "{branch}/{commit_number}-g{commit}/{tag_number}" + + path, git_obj = create_fake_repository(True, fake_tags_list) + revs = ga.get_test_revs(logger, git_obj, tag_name, branch="main") + self.assertEqual(len(revs), 1) + self.assertEqual(revs[0].commit, "0f7d5df") + self.assertEqual(len(revs[0].tags), 2) + self.assertEqual(revs[0].tags, ['main/10-g0f7d5df/0', 'main/10-g0f7d5df/1']) + delete_fake_repository(path) + + def test_get_tags_without_valid_remote(self): + url = 'git://git.yoctoproject.org/poky' + path, git_obj = create_fake_repository(False, None, False) + + tags = ga.get_tags(git_obj, self.log, pattern="yocto-*", url=url) + """Test for some well established tags (released tags)""" + self.assertIn("yocto-4.0", tags) + self.assertIn("yocto-4.1", tags) + self.assertIn("yocto-4.2", tags) + delete_fake_repository(path) + + def test_get_tags_with_only_local_tag(self): + fake_tags_list=["main/10-g0f7d5df/0", "main/10-g0f7d5df/1", "foo/20-g2468f5d/0"] + path, git_obj = create_fake_repository(True, fake_tags_list, False) + + """No remote is configured and no url is passed: get_tags must fall + back to local tags + """ + tags = ga.get_tags(git_obj, self.log) + self.assertCountEqual(tags, fake_tags_list) + delete_fake_repository(path) + + def test_get_tags_without_valid_remote_and_wrong_url(self): + url = 'git://git.foo.org/bar' + path, git_obj = create_fake_repository(False, None, False) + + """Test for some well established tags (released tags)""" + with self.assertRaises(GitError): + tags = ga.get_tags(git_obj, self.log, pattern="yocto-*", url=url) + delete_fake_repository(path) diff --git a/meta/lib/oeqa/selftest/cases/glibc.py b/meta/lib/oeqa/selftest/cases/glibc.py index c687f6ef93..bd56b2f6e7 100644 --- a/meta/lib/oeqa/selftest/cases/glibc.py +++ b/meta/lib/oeqa/selftest/cases/glibc.py @@ -1,10 +1,15 @@ +# +# Copyright OpenEmbedded Contributors +# # SPDX-License-Identifier: MIT +# import os +import time import contextlib from oeqa.core.decorator import OETestTag from oeqa.core.case import OEPTestResultTestCase from oeqa.selftest.case import OESelftestTestCase -from oeqa.utils.commands import bitbake, get_bb_var, get_bb_vars, runqemu, Command +from oeqa.utils.commands import bitbake, get_bb_var, runqemu from oeqa.utils.nfs import unfs_server def parse_values(content): @@ -24,16 +29,20 @@ class GlibcSelfTestBase(OESelftestTestCase, OEPTestResultTestCase): features.append('TOOLCHAIN_TEST_HOST_USER = "root"') features.append('TOOLCHAIN_TEST_HOST_PORT = "22"') # force single threaded test execution - features.append('EGLIBCPARALLELISM_task-check_pn-glibc-testsuite = "PARALLELMFLAGS="-j1""') + features.append('EGLIBCPARALLELISM:task-check:pn-glibc-testsuite = "PARALLELMFLAGS="-j1""') self.write_config("\n".join(features)) + start_time = time.time() + bitbake("glibc-testsuite -c check") + end_time = time.time() + builddir = get_bb_var("B", "glibc-testsuite") ptestsuite = "glibc-user" if ssh is None else "glibc" - self.ptest_section(ptestsuite) - with open(os.path.join(builddir, "tests.sum"), "r") as f: + self.ptest_section(ptestsuite, duration = int(end_time - start_time)) + with open(os.path.join(builddir, "tests.sum"), "r", errors='replace') as f: for test, result in parse_values(f): self.ptest_result(ptestsuite, test, result) @@ -41,7 +50,7 @@ class GlibcSelfTestBase(OESelftestTestCase, OEPTestResultTestCase): with contextlib.ExitStack() as s: # use the base work dir, as the nfs mount, since the recipe directory may not exist tmpdir = get_bb_var("BASE_WORKDIR") - nfsport, mountport = s.enter_context(unfs_server(tmpdir)) + nfsport, mountport = s.enter_context(unfs_server(tmpdir, udp = False)) # build core-image-minimal with required packages default_installed_packages = [ @@ -61,7 +70,7 @@ class GlibcSelfTestBase(OESelftestTestCase, OEPTestResultTestCase): bitbake("core-image-minimal") # start runqemu - qemu = s.enter_context(runqemu("core-image-minimal", runqemuparams = "nographic")) + qemu = s.enter_context(runqemu("core-image-minimal", runqemuparams = "nographic", qemuparams = "-m 1024")) # validate that SSH is working status, _ = qemu.run("uname") @@ -70,7 +79,7 @@ class GlibcSelfTestBase(OESelftestTestCase, OEPTestResultTestCase): # setup nfs mount if qemu.run("mkdir -p \"{0}\"".format(tmpdir))[0] != 0: raise Exception("Failed to setup NFS mount directory on target") - mountcmd = "mount -o noac,nfsvers=3,port={0},udp,mountport={1} \"{2}:{3}\" \"{3}\"".format(nfsport, mountport, qemu.server_ip, tmpdir) + mountcmd = "mount -o noac,nfsvers=3,port={0},mountport={1} \"{2}:{3}\" \"{3}\"".format(nfsport, mountport, qemu.server_ip, tmpdir) status, output = qemu.run(mountcmd) if status != 0: raise Exception("Failed to setup NFS mount on target ({})".format(repr(output))) @@ -83,6 +92,7 @@ class GlibcSelfTest(GlibcSelfTestBase): self.run_check() @OETestTag("toolchain-system") +@OETestTag("runqemu") class GlibcSelfTestSystemEmulated(GlibcSelfTestBase): def test_glibc(self): self.run_check_emulated() diff --git a/meta/lib/oeqa/selftest/cases/gotoolchain.py b/meta/lib/oeqa/selftest/cases/gotoolchain.py index 3119520f0d..ee2cf4b09a 100644 --- a/meta/lib/oeqa/selftest/cases/gotoolchain.py +++ b/meta/lib/oeqa/selftest/cases/gotoolchain.py @@ -1,4 +1,6 @@ # +# Copyright OpenEmbedded Contributors +# # SPDX-License-Identifier: MIT # @@ -46,26 +48,28 @@ class oeGoToolchainSelfTest(OESelftestTestCase): shutil.rmtree(cls.tmpdir_SDKQA, ignore_errors=True) super(oeGoToolchainSelfTest, cls).tearDownClass() - def run_sdk_go_command(self, gocmd): - cmd = "cd %s; " % self.tmpdir_SDKQA + def run_sdk_go_command(self, gocmd, proj, name): + cmd = "cd %s/src/%s/%s; " % (self.go_path, proj, name) cmd = cmd + ". %s; " % self.env_SDK cmd = cmd + "export GOPATH=%s; " % self.go_path + cmd = cmd + "export GOFLAGS=-modcacherw; " + cmd = cmd + "export CGO_ENABLED=1; " + cmd = cmd + "export GOPROXY=https://proxy.golang.org,direct; " cmd = cmd + "${CROSS_COMPILE}go %s" % gocmd return runCmd(cmd).status def test_go_dep_build(self): - proj = "github.com/golang" - name = "dep" - ver = "v0.3.1" + proj = "github.com/direnv" + name = "direnv" + ver = "v2.27.0" archive = ".tar.gz" url = "https://%s/%s/archive/%s%s" % (proj, name, ver, archive) runCmd("cd %s; wget %s" % (self.tmpdir_SDKQA, url)) runCmd("cd %s; tar -xf %s" % (self.tmpdir_SDKQA, ver+archive)) runCmd("mkdir -p %s/src/%s" % (self.go_path, proj)) - runCmd("mv %s/dep-0.3.1 %s/src/%s/%s" + runCmd("mv %s/direnv-2.27.0 %s/src/%s/%s" % (self.tmpdir_SDKQA, self.go_path, proj, name)) - retv = self.run_sdk_go_command('build %s/%s/cmd/dep' - % (proj, name)) + retv = self.run_sdk_go_command('build', proj, name) self.assertEqual(retv, 0, msg="Running go build failed for %s" % name) diff --git a/meta/lib/oeqa/selftest/cases/image_typedep.py b/meta/lib/oeqa/selftest/cases/image_typedep.py index 52e1080f13..17c98baf14 100644 --- a/meta/lib/oeqa/selftest/cases/image_typedep.py +++ b/meta/lib/oeqa/selftest/cases/image_typedep.py @@ -1,4 +1,6 @@ # +# Copyright OpenEmbedded Contributors +# # SPDX-License-Identifier: MIT # @@ -9,7 +11,7 @@ from oeqa.utils.commands import bitbake class ImageTypeDepTests(OESelftestTestCase): - # Verify that when specifying a IMAGE_TYPEDEP_ of the form "foo.bar" that + # Verify that when specifying a IMAGE_TYPEDEP: of the form "foo.bar" that # the conversion type bar gets added as a dep as well def test_conversion_typedep_added(self): @@ -22,7 +24,7 @@ LICENSE = "MIT" IMAGE_FSTYPES = "testfstype" IMAGE_TYPES_MASKED += "testfstype" -IMAGE_TYPEDEP_testfstype = "tar.bz2" +IMAGE_TYPEDEP:testfstype = "tar.bz2" inherit image diff --git a/meta/lib/oeqa/selftest/cases/imagefeatures.py b/meta/lib/oeqa/selftest/cases/imagefeatures.py index 5c519ac3d6..94d01ba116 100644 --- a/meta/lib/oeqa/selftest/cases/imagefeatures.py +++ b/meta/lib/oeqa/selftest/cases/imagefeatures.py @@ -1,10 +1,14 @@ # +# Copyright OpenEmbedded Contributors +# # SPDX-License-Identifier: MIT # from oeqa.selftest.case import OESelftestTestCase -from oeqa.utils.commands import runCmd, bitbake, get_bb_var, runqemu +from oeqa.core.decorator import OETestTag +from oeqa.utils.commands import runCmd, bitbake, get_bb_var, get_bb_vars, runqemu from oeqa.utils.sshcontrol import SSHControl +import glob import os import json @@ -13,6 +17,7 @@ class ImageFeatures(OESelftestTestCase): test_user = 'tester' root_user = 'root' + @OETestTag("runqemu") def test_non_root_user_can_connect_via_ssh_without_password(self): """ Summary: Check if non root user can connect via ssh without password @@ -38,6 +43,7 @@ class ImageFeatures(OESelftestTestCase): status, output = ssh.run("true") self.assertEqual(status, 0, 'ssh to user %s failed with %s' % (user, output)) + @OETestTag("runqemu") def test_all_users_can_connect_via_ssh_without_password(self): """ Summary: Check if all users can connect via ssh without password @@ -67,18 +73,6 @@ class ImageFeatures(OESelftestTestCase): self.assertEqual(status, 0, 'ssh to user tester failed with %s' % output) - def test_clutter_image_can_be_built(self): - """ - Summary: Check if clutter image can be built - Expected: 1. core-image-clutter can be built - Product: oe-core - Author: Ionut Chisanovici <ionutx.chisanovici@intel.com> - AutomatedBy: Daniel Istrate <daniel.alexandrux.istrate@intel.com> - """ - - # Build a core-image-clutter - bitbake('core-image-clutter') - def test_wayland_support_in_image(self): """ Summary: Check Wayland support in image @@ -108,12 +102,11 @@ class ImageFeatures(OESelftestTestCase): features = 'IMAGE_FSTYPES += " ext4 ext4.bmap ext4.bmap.gz"' self.write_config(features) - image_name = 'core-image-minimal' - bitbake(image_name) + image = 'core-image-minimal' + bitbake(image) + bb_vars = get_bb_vars(['DEPLOY_DIR_IMAGE', 'IMAGE_LINK_NAME'], image) - deploy_dir_image = get_bb_var('DEPLOY_DIR_IMAGE') - link_name = get_bb_var('IMAGE_LINK_NAME', image_name) - image_path = os.path.join(deploy_dir_image, "%s.ext4" % link_name) + image_path = os.path.join(bb_vars['DEPLOY_DIR_IMAGE'], "%s.ext4" % bb_vars['IMAGE_LINK_NAME']) bmap_path = "%s.bmap" % image_path gzip_path = "%s.gz" % bmap_path @@ -126,8 +119,8 @@ class ImageFeatures(OESelftestTestCase): image_stat = os.stat(image_path) self.assertGreater(image_stat.st_size, image_stat.st_blocks * 512) - # check if the resulting gzip is valid - self.assertTrue(runCmd('gzip -t %s' % gzip_path)) + # check if the resulting gzip is valid, --force is needed in case gzip_path is a symlink + self.assertTrue(runCmd('gzip --test --force %s' % gzip_path)) def test_hypervisor_fmts(self): """ @@ -142,17 +135,16 @@ class ImageFeatures(OESelftestTestCase): img_types = [ 'vmdk', 'vdi', 'qcow2' ] features = "" for itype in img_types: - features += 'IMAGE_FSTYPES += "wic.%s"\n' % itype + features += 'IMAGE_FSTYPES += "ext4.%s"\n' % itype self.write_config(features) - image_name = 'core-image-minimal' - bitbake(image_name) + image = 'core-image-minimal' + bitbake(image) + bb_vars = get_bb_vars(['DEPLOY_DIR_IMAGE', 'IMAGE_LINK_NAME'], image) - deploy_dir_image = get_bb_var('DEPLOY_DIR_IMAGE') - link_name = get_bb_var('IMAGE_LINK_NAME', image_name) for itype in img_types: - image_path = os.path.join(deploy_dir_image, "%s.wic.%s" % - (link_name, itype)) + image_path = os.path.join(bb_vars['DEPLOY_DIR_IMAGE'], "%s.ext4.%s" % + (bb_vars['IMAGE_LINK_NAME'], itype)) # check if result image file is in deploy directory self.assertTrue(os.path.exists(image_path)) @@ -172,24 +164,22 @@ class ImageFeatures(OESelftestTestCase): """ Summary: Check for chaining many CONVERSION_CMDs together Expected: 1. core-image-minimal can be built with - ext4.bmap.gz.bz2.lzo.xz.u-boot and also create a + ext4.bmap.gz.bz2.zst.xz.u-boot and also create a sha256sum 2. The above image has a valid sha256sum Product: oe-core Author: Tom Rini <trini@konsulko.com> """ - conv = "ext4.bmap.gz.bz2.lzo.xz.u-boot" + conv = "ext4.bmap.gz.bz2.zst.xz.u-boot" features = 'IMAGE_FSTYPES += "%s %s.sha256sum"' % (conv, conv) self.write_config(features) - image_name = 'core-image-minimal' - bitbake(image_name) - - deploy_dir_image = get_bb_var('DEPLOY_DIR_IMAGE') - link_name = get_bb_var('IMAGE_LINK_NAME', image_name) - image_path = os.path.join(deploy_dir_image, "%s.%s" % - (link_name, conv)) + image = 'core-image-minimal' + bitbake(image) + bb_vars = get_bb_vars(['DEPLOY_DIR_IMAGE', 'IMAGE_LINK_NAME'], image) + image_path = os.path.join(bb_vars['DEPLOY_DIR_IMAGE'], "%s.%s" % + (bb_vars['IMAGE_LINK_NAME'], conv)) # check if resulting image is in the deploy directory self.assertTrue(os.path.exists(image_path)) @@ -197,7 +187,7 @@ class ImageFeatures(OESelftestTestCase): # check if the resulting sha256sum agrees self.assertTrue(runCmd('cd %s;sha256sum -c %s.%s.sha256sum' % - (deploy_dir_image, link_name, conv))) + (bb_vars['DEPLOY_DIR_IMAGE'], bb_vars['IMAGE_LINK_NAME'], conv))) def test_image_fstypes(self): """ @@ -206,26 +196,43 @@ class ImageFeatures(OESelftestTestCase): Product: oe-core Author: Ed Bartosh <ed.bartosh@linux.intel.com> """ - image_name = 'core-image-minimal' + image = 'core-image-minimal' - all_image_types = set(get_bb_var("IMAGE_TYPES", image_name).split()) - blacklist = set(('container', 'elf', 'f2fs', 'multiubi', 'tar.zst')) - img_types = all_image_types - blacklist + all_image_types = set(get_bb_var("IMAGE_TYPES", image).split()) + skip_image_types = set(('container', 'elf', 'f2fs', 'tar.zst', 'wic.zst', 'squashfs-lzo', 'vfat')) + img_types = all_image_types - skip_image_types - config = 'IMAGE_FSTYPES += "%s"\n'\ - 'MKUBIFS_ARGS ?= "-m 2048 -e 129024 -c 2047"\n'\ - 'UBINIZE_ARGS ?= "-m 2048 -p 128KiB -s 512"' % ' '.join(img_types) + config = """ +IMAGE_FSTYPES += "%s" +WKS_FILE = "wictestdisk.wks" +MKUBIFS_ARGS ?= "-m 2048 -e 129024 -c 2047" +UBINIZE_ARGS ?= "-m 2048 -p 128KiB -s 512" +MULTIUBI_BUILD += "mtd_2_128" +MKUBIFS_ARGS_mtd_2_128 ?= "-m 2048 -e 129024 -c 2047" +UBINIZE_ARGS_mtd_2_128 ?= "-m 2048 -p 128KiB -s 512" +MULTIUBI_BUILD += "mtd_4_256" +MKUBIFS_ARGS_mtd_4_256 ?= "-m 4096 -e 253952 -c 4096" +UBINIZE_ARGS_mtd_4_256 ?= "-m 4096 -p 256KiB" +""" % ' '.join(img_types) self.write_config(config) - bitbake(image_name) + bitbake(image) + bb_vars = get_bb_vars(['DEPLOY_DIR_IMAGE', 'IMAGE_LINK_NAME', 'MULTIUBI_BUILD'], image) - deploy_dir_image = get_bb_var('DEPLOY_DIR_IMAGE') - link_name = get_bb_var('IMAGE_LINK_NAME', image_name) for itype in img_types: - image_path = os.path.join(deploy_dir_image, "%s.%s" % (link_name, itype)) - # check if result image is in deploy directory - self.assertTrue(os.path.exists(image_path), - "%s image %s doesn't exist" % (itype, image_path)) + if itype == 'multiubi': + # For multiubi build we need to manage MULTIUBI_BUILD entry to append + # specific name to IMAGE_LINK_NAME + for vname in bb_vars['MULTIUBI_BUILD'].split(): + image_path = os.path.join(bb_vars['DEPLOY_DIR_IMAGE'], "%s_%s.ubifs" % (bb_vars['IMAGE_LINK_NAME'], vname)) + # check if result image is in deploy directory + self.assertTrue(os.path.exists(image_path), + "%s image %s doesn't exist" % (itype, image_path)) + else: + image_path = os.path.join(bb_vars['DEPLOY_DIR_IMAGE'], "%s.%s" % (bb_vars['IMAGE_LINK_NAME'], itype)) + # check if result image is in deploy directory + self.assertTrue(os.path.exists(image_path), + "%s image %s doesn't exist" % (itype, image_path)) def test_useradd_static(self): config = """ @@ -239,16 +246,11 @@ USERADD_GID_TABLES += "files/static-group" def test_no_busybox_base_utils(self): config = """ -# Enable x11 -DISTRO_FEATURES_append += "x11" +# Enable wayland +DISTRO_FEATURES:append = " pam opengl wayland" # Switch to systemd -DISTRO_FEATURES += "systemd" -VIRTUAL-RUNTIME_init_manager = "systemd" -VIRTUAL-RUNTIME_initscripts = "" -VIRTUAL-RUNTIME_syslog = "" -VIRTUAL-RUNTIME_login_manager = "shadow-base" -DISTRO_FEATURES_BACKFILL_CONSIDERED = "sysvinit" +INIT_MANAGER = "systemd" # Replace busybox PREFERRED_PROVIDER_virtual/base-utils = "packagegroup-core-base-utils" @@ -256,9 +258,74 @@ VIRTUAL-RUNTIME_base-utils = "packagegroup-core-base-utils" VIRTUAL-RUNTIME_base-utils-hwclock = "util-linux-hwclock" VIRTUAL-RUNTIME_base-utils-syslog = "" -# Blacklist busybox -PNBLACKLIST[busybox] = "Don't build this" +# Skip busybox +SKIP_RECIPE[busybox] = "Don't build this" """ self.write_config(config) - bitbake("--graphviz core-image-sato") + bitbake("--graphviz core-image-weston") + + def test_image_gen_debugfs(self): + """ + Summary: Check debugfs generation + Expected: 1. core-image-minimal can be build with IMAGE_GEN_DEBUGFS variable set + 2. debug filesystem is created when variable set + 3. debug symbols available + Product: oe-core + Author: Humberto Ibarra <humberto.ibarra.lopez@intel.com> + Yeoh Ee Peng <ee.peng.yeoh@intel.com> + """ + + image = 'core-image-minimal' + image_fstypes_debugfs = 'tar.bz2' + features = 'IMAGE_GEN_DEBUGFS = "1"\n' + features += 'IMAGE_FSTYPES_DEBUGFS = "%s"\n' % image_fstypes_debugfs + self.write_config(features) + + bitbake(image) + bb_vars = get_bb_vars(['DEPLOY_DIR_IMAGE', 'IMAGE_LINK_NAME'], image) + + dbg_tar_file = os.path.join(bb_vars['DEPLOY_DIR_IMAGE'], "%s-dbg.%s" % (bb_vars['IMAGE_LINK_NAME'], image_fstypes_debugfs)) + self.assertTrue(os.path.exists(dbg_tar_file), 'debug filesystem not generated at %s' % dbg_tar_file) + result = runCmd('cd %s; tar xvf %s' % (bb_vars['DEPLOY_DIR_IMAGE'], dbg_tar_file)) + self.assertEqual(result.status, 0, msg='Failed to extract %s: %s' % (dbg_tar_file, result.output)) + result = runCmd('find %s -name %s' % (bb_vars['DEPLOY_DIR_IMAGE'], "udevadm")) + self.assertTrue("udevadm" in result.output, msg='Failed to find udevadm: %s' % result.output) + dbg_symbols_targets = result.output.splitlines() + self.assertTrue(dbg_symbols_targets, msg='Failed to split udevadm: %s' % dbg_symbols_targets) + for t in dbg_symbols_targets: + result = runCmd('objdump --syms %s | grep debug' % t) + self.assertTrue("debug" in result.output, msg='Failed to find debug symbol: %s' % result.output) + + def test_empty_image(self): + """Test creation of image with no packages""" + image = 'test-empty-image' + bitbake(image) + bb_vars = get_bb_vars(['DEPLOY_DIR_IMAGE', 'IMAGE_LINK_NAME'], image) + manifest = os.path.join(bb_vars['DEPLOY_DIR_IMAGE'], "%s.manifest" % bb_vars['IMAGE_LINK_NAME']) + self.assertTrue(os.path.exists(manifest)) + + with open(manifest, "r") as f: + self.assertEqual(len(f.read().strip()),0) + + def test_mandb(self): + """ + Test that an image containing manpages has working man and apropos commands. + """ + config = """ +DISTRO_FEATURES:append = " api-documentation" +CORE_IMAGE_EXTRA_INSTALL = "man-pages" +""" + self.write_config(config) + bitbake("core-image-minimal") + + with runqemu('core-image-minimal', ssh=False, runqemuparams='nographic') as qemu: + # This manpage is provided by man-pages + status, output = qemu.run_serial("apropos 8859") + self.assertEqual(status, 1, 'Failed to run apropos: %s' % (output)) + self.assertIn("iso_8859_15", output) + + # This manpage is provided by man-pages + status, output = qemu.run_serial("man --pager=cat intro") + self.assertEqual(status, 1, 'Failed to run man: %s' % (output)) + self.assertIn("introduction to user commands", output) diff --git a/meta/lib/oeqa/selftest/cases/incompatible_lic.py b/meta/lib/oeqa/selftest/cases/incompatible_lic.py index 904b5b4094..be5484bca4 100644 --- a/meta/lib/oeqa/selftest/cases/incompatible_lic.py +++ b/meta/lib/oeqa/selftest/cases/incompatible_lic.py @@ -1,10 +1,68 @@ +# +# Copyright OpenEmbedded Contributors +# +# SPDX-License-Identifier: MIT +# from oeqa.selftest.case import OESelftestTestCase from oeqa.utils.commands import bitbake +class IncompatibleLicenseTestObsolete(OESelftestTestCase): + + def lic_test(self, pn, pn_lic, lic, error_msg=None): + if not error_msg: + error_msg = 'ERROR: Nothing PROVIDES \'%s\'\n%s was skipped: it has incompatible license(s): %s' % (pn, pn, pn_lic) + + self.write_config("INCOMPATIBLE_LICENSE += \"%s\"" % (lic)) + + result = bitbake('%s --dry-run' % (pn), ignore_status=True) + if error_msg not in result.output: + raise AssertionError(result.output) + + # Verify that a package with an SPDX license cannot be built when + # INCOMPATIBLE_LICENSE contains an alias (in SPDXLICENSEMAP) of this SPDX + # license + def test_incompatible_alias_spdx_license(self): + self.lic_test('incompatible-license', 'GPL-3.0-only', 'GPLv3', "is an obsolete license, please use an SPDX reference in INCOMPATIBLE_LICENSE") + + # Verify that a package with an SPDX license cannot be built when + # INCOMPATIBLE_LICENSE contains a wildcarded alias license matching this + # SPDX license + def test_incompatible_alias_spdx_license_wildcard(self): + self.lic_test('incompatible-license', 'GPL-3.0-only', '*GPLv3', "*GPLv3 is an invalid license wildcard entry") + + # Verify that a package with an alias (from SPDXLICENSEMAP) to an SPDX + # license cannot be built when INCOMPATIBLE_LICENSE contains this alias + def test_incompatible_alias_spdx_license_alias(self): + self.lic_test('incompatible-license-alias', 'GPL-3.0-only', 'GPLv3', "is an obsolete license, please use an SPDX reference in INCOMPATIBLE_LICENSE") + + # Verify that a package with an alias (from SPDXLICENSEMAP) to an SPDX + # license cannot be built when INCOMPATIBLE_LICENSE contains a wildcarded + # license matching this SPDX license + def test_incompatible_spdx_license_alias_wildcard(self): + self.lic_test('incompatible-license-alias', 'GPL-3.0-only', '*GPL-3.0', "*GPL-3.0 is an invalid license wildcard entry") + + # Verify that a package with an alias (from SPDXLICENSEMAP) to an SPDX + # license cannot be built when INCOMPATIBLE_LICENSE contains a wildcarded + # alias license matching the SPDX license + def test_incompatible_alias_spdx_license_alias_wildcard(self): + self.lic_test('incompatible-license-alias', 'GPL-3.0-only', '*GPLv3', "*GPLv3 is an invalid license wildcard entry") + + + # Verify that a package with multiple SPDX licenses cannot be built when + # INCOMPATIBLE_LICENSE contains a wildcard to some of them + def test_incompatible_spdx_licenses_wildcard(self): + self.lic_test('incompatible-licenses', 'GPL-3.0-only LGPL-3.0-only', '*GPL-3.0-only', "*GPL-3.0-only is an invalid license wildcard entry") + + + # Verify that a package with multiple SPDX licenses cannot be built when + # INCOMPATIBLE_LICENSE contains a wildcard matching all licenses + def test_incompatible_all_licenses_wildcard(self): + self.lic_test('incompatible-licenses', 'GPL-2.0-only GPL-3.0-only LGPL-3.0-only', '*', "* is an invalid license wildcard entry") + class IncompatibleLicenseTests(OESelftestTestCase): def lic_test(self, pn, pn_lic, lic): - error_msg = 'ERROR: Nothing PROVIDES \'%s\'\n%s was skipped: it has an incompatible license: %s' % (pn, pn, pn_lic) + error_msg = 'ERROR: Nothing PROVIDES \'%s\'\n%s was skipped: it has incompatible license(s): %s' % (pn, pn, pn_lic) self.write_config("INCOMPATIBLE_LICENSE += \"%s\"" % (lic)) @@ -12,30 +70,29 @@ class IncompatibleLicenseTests(OESelftestTestCase): if error_msg not in result.output: raise AssertionError(result.output) - # Verify that a package with an SPDX license (from SRC_DISTRIBUTE_LICENSES) - # cannot be built when INCOMPATIBLE_LICENSE contains this SPDX license + # Verify that a package with an SPDX license cannot be built when + # INCOMPATIBLE_LICENSE contains this SPDX license def test_incompatible_spdx_license(self): - self.lic_test('incompatible-license', 'GPL-3.0', 'GPL-3.0') + self.lic_test('incompatible-license', 'GPL-3.0-only', 'GPL-3.0-only') - # Verify that a package with an SPDX license (from SRC_DISTRIBUTE_LICENSES) - # cannot be built when INCOMPATIBLE_LICENSE contains an alias (in - # SPDXLICENSEMAP) of this SPDX license - def test_incompatible_alias_spdx_license(self): - self.lic_test('incompatible-license', 'GPL-3.0', 'GPLv3') + # Verify that a package with an SPDX license cannot be built when + # INCOMPATIBLE_LICENSE contains a wildcarded license matching this SPDX + # license + def test_incompatible_spdx_license_wildcard(self): + self.lic_test('incompatible-license', 'GPL-3.0-only', 'GPL-3.0*') # Verify that a package with an alias (from SPDXLICENSEMAP) to an SPDX # license cannot be built when INCOMPATIBLE_LICENSE contains this SPDX # license def test_incompatible_spdx_license_alias(self): - self.lic_test('incompatible-license-alias', 'GPLv3', 'GPL-3.0') + self.lic_test('incompatible-license-alias', 'GPL-3.0-only', 'GPL-3.0-only') - # Verify that a package with an alias (from SPDXLICENSEMAP) to an SPDX - # license cannot be built when INCOMPATIBLE_LICENSE contains this alias - def test_incompatible_alias_spdx_license_alias(self): - self.lic_test('incompatible-license-alias', 'GPLv3', 'GPLv3') + # Verify that a package with multiple SPDX licenses cannot be built when + # INCOMPATIBLE_LICENSE contains some of them + def test_incompatible_spdx_licenses(self): + self.lic_test('incompatible-licenses', 'GPL-3.0-only LGPL-3.0-only', 'GPL-3.0-only LGPL-3.0-only') - # Verify that a package with a non-SPDX license (neither in - # SRC_DISTRIBUTE_LICENSES nor in SPDXLICENSEMAP) cannot be built when + # Verify that a package with a non-SPDX license cannot be built when # INCOMPATIBLE_LICENSE contains this license def test_incompatible_nonspdx_license(self): self.lic_test('incompatible-nonspdx-license', 'FooLicense', 'FooLicense') @@ -43,51 +100,53 @@ class IncompatibleLicenseTests(OESelftestTestCase): class IncompatibleLicensePerImageTests(OESelftestTestCase): def default_config(self): return """ -IMAGE_INSTALL_append = "bash" -INCOMPATIBLE_LICENSE_pn-core-image-minimal = "GPL-3.0 LGPL-3.0" +IMAGE_INSTALL:append = " bash" +INCOMPATIBLE_LICENSE:pn-core-image-minimal = "GPL-3.0* LGPL-3.0*" """ def test_bash_default(self): self.write_config(self.default_config()) - error_msg = "ERROR: core-image-minimal-1.0-r0 do_rootfs: Package bash has an incompatible license GPLv3+ and cannot be installed into the image." + error_msg = "ERROR: core-image-minimal-1.0-r0 do_rootfs: Package bash cannot be installed into the image because it has incompatible license(s): GPL-3.0-or-later" result = bitbake('core-image-minimal', ignore_status=True) if error_msg not in result.output: raise AssertionError(result.output) def test_bash_and_license(self): - self.write_config(self.default_config() + '\nLICENSE_append_pn-bash = " & SomeLicense"') - error_msg = "ERROR: core-image-minimal-1.0-r0 do_rootfs: Package bash has an incompatible license GPLv3+ & SomeLicense and cannot be installed into the image." + self.disable_class("create-spdx") + self.write_config(self.default_config() + '\nLICENSE:append:pn-bash = " & SomeLicense"\nERROR_QA:remove:pn-bash = "license-exists"') + error_msg = "ERROR: core-image-minimal-1.0-r0 do_rootfs: Package bash cannot be installed into the image because it has incompatible license(s): GPL-3.0-or-later" result = bitbake('core-image-minimal', ignore_status=True) if error_msg not in result.output: raise AssertionError(result.output) def test_bash_or_license(self): - self.write_config(self.default_config() + '\nLICENSE_append_pn-bash = " | SomeLicense"') + self.disable_class("create-spdx") + self.write_config(self.default_config() + '\nLICENSE:append:pn-bash = " | SomeLicense"\nERROR_QA:remove:pn-bash = "license-exists"\nERROR_QA:remove:pn-core-image-minimal = "license-file-missing"') bitbake('core-image-minimal') - def test_bash_whitelist(self): - self.write_config(self.default_config() + '\nWHITELIST_GPL-3.0_pn-core-image-minimal = "bash"') + def test_bash_license_exceptions(self): + self.write_config(self.default_config() + '\nINCOMPATIBLE_LICENSE_EXCEPTIONS:pn-core-image-minimal = "bash:GPL-3.0-or-later"\nERROR_QA:remove:pn-core-image-minimal = "license-exception"') bitbake('core-image-minimal') class NoGPL3InImagesTests(OESelftestTestCase): def test_core_image_minimal(self): self.write_config(""" -INCOMPATIBLE_LICENSE_pn-core-image-minimal = "GPL-3.0 LGPL-3.0" +INCOMPATIBLE_LICENSE:pn-core-image-minimal = "GPL-3.0* LGPL-3.0*" """) bitbake('core-image-minimal') - def test_core_image_full_cmdline(self): + def test_core_image_full_cmdline_weston(self): self.write_config(""" -INHERIT += "testimage"\n -INCOMPATIBLE_LICENSE_pn-core-image-full-cmdline = "GPL-3.0 LGPL-3.0"\n -RDEPENDS_packagegroup-core-full-cmdline-utils_remove = "bash bc coreutils cpio ed findutils gawk grep mc mc-fish mc-helpers mc-helpers-perl sed tar time"\n -RDEPENDS_packagegroup-core-full-cmdline-dev-utils_remove = "diffutils m4 make patch"\n -RDEPENDS_packagegroup-core-full-cmdline-multiuser_remove = "gzip"\n +IMAGE_CLASSES += "testimage" +INCOMPATIBLE_LICENSE:pn-core-image-full-cmdline = "GPL-3.0* LGPL-3.0*" +INCOMPATIBLE_LICENSE:pn-core-image-weston = "GPL-3.0* LGPL-3.0*" + +require conf/distro/include/no-gplv3.inc """) - bitbake('core-image-full-cmdline') - bitbake('-c testimage core-image-full-cmdline') + bitbake('core-image-full-cmdline core-image-weston') + bitbake('-c testimage core-image-full-cmdline core-image-weston') diff --git a/meta/lib/oeqa/selftest/cases/intercept.py b/meta/lib/oeqa/selftest/cases/intercept.py new file mode 100644 index 0000000000..12583c3099 --- /dev/null +++ b/meta/lib/oeqa/selftest/cases/intercept.py @@ -0,0 +1,21 @@ +# +# Copyright OpenEmbedded Contributors +# +# SPDX-License-Identifier: MIT +# + +from oeqa.selftest.case import OESelftestTestCase +from oeqa.utils.commands import bitbake + +class GitCheck(OESelftestTestCase): + def test_git_intercept(self): + """ + Git binaries with CVE-2022-24765 fixed will refuse to operate on a + repository which is owned by a different user. This breaks our + do_install task as that runs inside pseudo, so the git repository is + owned by the build user but git is running as (fake)root. + + We have an intercept which disables pseudo, so verify that it works. + """ + bitbake("git-submodule-test -c test_git_as_user") + bitbake("git-submodule-test -c test_git_as_root") diff --git a/meta/lib/oeqa/selftest/cases/kerneldevelopment.py b/meta/lib/oeqa/selftest/cases/kerneldevelopment.py index a61876ee61..b1f78a0cd1 100644 --- a/meta/lib/oeqa/selftest/cases/kerneldevelopment.py +++ b/meta/lib/oeqa/selftest/cases/kerneldevelopment.py @@ -1,3 +1,9 @@ +# +# Copyright OpenEmbedded Contributors +# +# SPDX-License-Identifier: MIT +# + import os from oeqa.selftest.case import OESelftestTestCase from oeqa.utils.commands import runCmd, get_bb_var @@ -58,7 +64,8 @@ class KernelDev(OESelftestTestCase): recipe_append = os.path.join(self.recipeskernel_dir, 'linux-yocto_%.bbappend') with open(recipe_append, 'w+') as fh: fh.write('SRC_URI += "file://%s"\n' % patch_name) - fh.write('FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:"') + fh.write('ERROR_QA:remove:pn-linux-yocto = "patch-status"\n') + fh.write('FILESEXTRAPATHS:prepend := "${THISDIR}/${PN}:"') runCmd('bitbake virtual/kernel -c clean') runCmd('bitbake virtual/kernel -c patch') diff --git a/meta/lib/oeqa/selftest/cases/layerappend.py b/meta/lib/oeqa/selftest/cases/layerappend.py index 05e9426fc6..64b17117cc 100644 --- a/meta/lib/oeqa/selftest/cases/layerappend.py +++ b/meta/lib/oeqa/selftest/cases/layerappend.py @@ -1,11 +1,13 @@ # +# Copyright OpenEmbedded Contributors +# # SPDX-License-Identifier: MIT # import os from oeqa.selftest.case import OESelftestTestCase -from oeqa.utils.commands import runCmd, bitbake, get_bb_var +from oeqa.utils.commands import bitbake, get_bb_var import oeqa.utils.ftools as ftools class LayerAppendTests(OESelftestTestCase): @@ -30,20 +32,20 @@ python do_build() { addtask build """ append = """ -FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:" +FILESEXTRAPATHS:prepend := "${THISDIR}/${PN}:" -SRC_URI_append = " file://appendtest.txt" +SRC_URI:append = " file://appendtest.txt" -sysroot_stage_all_append() { - install -m 644 ${WORKDIR}/appendtest.txt ${SYSROOT_DESTDIR}/ +sysroot_stage_all:append() { + install -m 644 ${UNPACKDIR}/appendtest.txt ${SYSROOT_DESTDIR}/ } """ append2 = """ -FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:" +FILESEXTRAPATHS:prepend := "${THISDIR}/${PN}:" -SRC_URI_append = " file://appendtest.txt" +SRC_URI:append = " file://appendtest.txt" """ layerappend = '' diff --git a/meta/lib/oeqa/selftest/cases/liboe.py b/meta/lib/oeqa/selftest/cases/liboe.py index afe8f8809f..d5ffffdcb4 100644 --- a/meta/lib/oeqa/selftest/cases/liboe.py +++ b/meta/lib/oeqa/selftest/cases/liboe.py @@ -1,4 +1,6 @@ # +# Copyright OpenEmbedded Contributors +# # SPDX-License-Identifier: MIT # @@ -97,6 +99,6 @@ class LibOE(OESelftestTestCase): dstcnt = len(os.listdir(dst)) srccnt = len(os.listdir(src)) - self.assertEquals(dstcnt, len(testfiles), "Number of files in dst (%s) differs from number of files in src(%s)." % (dstcnt, srccnt)) + self.assertEqual(dstcnt, len(testfiles), "Number of files in dst (%s) differs from number of files in src(%s)." % (dstcnt, srccnt)) oe.path.remove(testloc) diff --git a/meta/lib/oeqa/selftest/cases/lic_checksum.py b/meta/lib/oeqa/selftest/cases/lic_checksum.py index bae935d697..2d0b805b90 100644 --- a/meta/lib/oeqa/selftest/cases/lic_checksum.py +++ b/meta/lib/oeqa/selftest/cases/lic_checksum.py @@ -1,16 +1,36 @@ # +# Copyright OpenEmbedded Contributors +# # SPDX-License-Identifier: MIT # import os import tempfile +import urllib from oeqa.selftest.case import OESelftestTestCase from oeqa.utils.commands import bitbake -from oeqa.utils import CommandError class LicenseTests(OESelftestTestCase): + def test_checksum_with_space(self): + bitbake_cmd = '-c populate_lic emptytest' + + lic_file, lic_path = tempfile.mkstemp(" -afterspace") + os.close(lic_file) + #self.track_for_cleanup(lic_path) + + self.write_config("INHERIT:remove = \"report-error\"") + + self.write_recipeinc('emptytest', """ +INHIBIT_DEFAULT_DEPS = "1" +LIC_FILES_CHKSUM = "file://%s;md5=d41d8cd98f00b204e9800998ecf8427e" +SRC_URI = "file://%s;md5=d41d8cd98f00b204e9800998ecf8427e" +""" % (urllib.parse.quote(lic_path), urllib.parse.quote(lic_path))) + result = bitbake(bitbake_cmd) + self.delete_recipeinc('emptytest') + + # Verify that changing a license file that has an absolute path causes # the license qa to fail due to a mismatched md5sum. def test_nonmatching_checksum(self): @@ -21,7 +41,7 @@ class LicenseTests(OESelftestTestCase): os.close(lic_file) self.track_for_cleanup(lic_path) - self.write_config("INHERIT_remove = \"report-error\"") + self.write_config("INHERIT:remove = \"report-error\"") self.write_recipeinc('emptytest', """ INHIBIT_DEFAULT_DEPS = "1" @@ -34,5 +54,6 @@ SRC_URI = "file://%s;md5=d41d8cd98f00b204e9800998ecf8427e" f.write("data") result = bitbake(bitbake_cmd, ignore_status=True) + self.delete_recipeinc('emptytest') if error_msg not in result.output: raise AssertionError(result.output) diff --git a/meta/lib/oeqa/selftest/cases/locales.py b/meta/lib/oeqa/selftest/cases/locales.py new file mode 100644 index 0000000000..ac4888ef66 --- /dev/null +++ b/meta/lib/oeqa/selftest/cases/locales.py @@ -0,0 +1,54 @@ +# +# SPDX-License-Identifier: MIT +# + +from oeqa.selftest.case import OESelftestTestCase +from oeqa.core.decorator import OETestTag +from oeqa.utils.commands import bitbake, runqemu + +class LocalesTest(OESelftestTestCase): + + @OETestTag("runqemu") + + def run_locales_test(self, binary_enabled): + features = [] + features.append('EXTRA_IMAGE_FEATURES = "empty-root-password allow-empty-password allow-root-login"') + features.append('IMAGE_INSTALL:append = " glibc-utils localedef"') + features.append('GLIBC_GENERATE_LOCALES = "en_US.UTF-8 fr_FR.UTF-8 en_US.ISO-8859-1 de_DE.UTF-8 fr_FR.ISO-8859-1 zh_HK.BIG5-HKSCS tr_TR.UTF-8"') + features.append('IMAGE_LINGUAS:append = " en-us fr-fr"') + if binary_enabled: + features.append('ENABLE_BINARY_LOCALE_GENERATION = "1"') + else: + features.append('ENABLE_BINARY_LOCALE_GENERATION = "0"') + self.write_config("\n".join(features)) + + # Build a core-image-minimal + bitbake('core-image-minimal') + + with runqemu("core-image-minimal", ssh=False, runqemuparams='nographic') as qemu: + cmd = "locale -a" + status, output = qemu.run_serial(cmd) + # output must includes fr_FR or fr_FR.UTF-8 + self.assertEqual(status, 1, msg='locale test command failed: output: %s' % output) + self.assertIn("fr_FR", output, msg='locale -a test failed: output: %s' % output) + + cmd = "localedef --list-archive -v" + status, output = qemu.run_serial(cmd) + # output must includes fr_FR.utf8 + self.assertEqual(status, 1, msg='localedef test command failed: output: %s' % output) + self.assertIn("fr_FR.utf8", output, msg='localedef test failed: output: %s' % output) + + def test_locales_on(self): + """ + Summary: Test the locales are generated + Expected: 1. Check the locale exist in the locale-archive + 2. Check the locale exist for the glibc + 3. Check the locale can be generated + Product: oe-core + Author: Louis Rannou <lrannou@baylibre.com> + AutomatedBy: Louis Rannou <lrannou@baylibre.com> + """ + self.run_locales_test(True) + + def test_locales_off(self): + self.run_locales_test(False) diff --git a/meta/lib/oeqa/selftest/cases/manifest.py b/meta/lib/oeqa/selftest/cases/manifest.py index 5d13f35468..07a6c80489 100644 --- a/meta/lib/oeqa/selftest/cases/manifest.py +++ b/meta/lib/oeqa/selftest/cases/manifest.py @@ -1,11 +1,13 @@ # +# Copyright OpenEmbedded Contributors +# # SPDX-License-Identifier: MIT # import os from oeqa.selftest.case import OESelftestTestCase -from oeqa.utils.commands import get_bb_var, get_bb_vars, bitbake +from oeqa.utils.commands import get_bb_var, bitbake class ManifestEntry: '''A manifest item of a collection able to list missing packages''' diff --git a/meta/lib/oeqa/selftest/cases/meta_ide.py b/meta/lib/oeqa/selftest/cases/meta_ide.py index 03901a2f32..5a17ca52ea 100644 --- a/meta/lib/oeqa/selftest/cases/meta_ide.py +++ b/meta/lib/oeqa/selftest/cases/meta_ide.py @@ -1,4 +1,6 @@ # +# Copyright OpenEmbedded Contributors +# # SPDX-License-Identifier: MIT # @@ -16,13 +18,15 @@ class MetaIDE(OESelftestTestCase): def setUpClass(cls): super(MetaIDE, cls).setUpClass() bitbake('meta-ide-support') - bb_vars = get_bb_vars(['MULTIMACH_TARGET_SYS', 'TMPDIR', 'COREBASE']) - cls.environment_script = 'environment-setup-%s' % bb_vars['MULTIMACH_TARGET_SYS'] - cls.tmpdir = bb_vars['TMPDIR'] - cls.environment_script_path = '%s/%s' % (cls.tmpdir, cls.environment_script) + bitbake('build-sysroots -c build_native_sysroot') + bitbake('build-sysroots -c build_target_sysroot') + bb_vars = get_bb_vars(['MACHINE_ARCH', 'TARGET_VENDOR', 'TARGET_OS', 'DEPLOY_DIR_IMAGE', 'COREBASE']) + cls.environment_script = 'environment-setup-%s%s-%s' % (bb_vars['MACHINE_ARCH'], bb_vars['TARGET_VENDOR'], bb_vars['TARGET_OS']) + cls.deploydir = bb_vars['DEPLOY_DIR_IMAGE'] + cls.environment_script_path = '%s/%s' % (cls.deploydir, cls.environment_script) cls.corebasedir = bb_vars['COREBASE'] cls.tmpdir_metaideQA = tempfile.mkdtemp(prefix='metaide') - + @classmethod def tearDownClass(cls): shutil.rmtree(cls.tmpdir_metaideQA, ignore_errors=True) @@ -40,12 +44,17 @@ class MetaIDE(OESelftestTestCase): def test_meta_ide_can_build_cpio_project(self): dl_dir = self.td.get('DL_DIR', None) self.project = SDKBuildProject(self.tmpdir_metaideQA + "/cpio/", self.environment_script_path, - "https://ftp.gnu.org/gnu/cpio/cpio-2.12.tar.gz", + "https://ftp.gnu.org/gnu/cpio/cpio-2.15.tar.gz", self.tmpdir_metaideQA, self.td['DATETIME'], dl_dir=dl_dir) self.project.download_archive() - self.assertEqual(self.project.run_configure(), 0, + self.assertEqual(self.project.run_configure('$CONFIGURE_FLAGS'), 0, msg="Running configure failed") self.assertEqual(self.project.run_make(), 0, msg="Running make failed") self.assertEqual(self.project.run_install(), 0, msg="Running make install failed") + + def test_meta_ide_can_run_sdk_tests(self): + bitbake('-c populate_sysroot gtk+3') + bitbake('build-sysroots -c build_target_sysroot') + bitbake('-c testsdk meta-ide-support') diff --git a/meta/lib/oeqa/selftest/cases/minidebuginfo.py b/meta/lib/oeqa/selftest/cases/minidebuginfo.py new file mode 100644 index 0000000000..a8923460f9 --- /dev/null +++ b/meta/lib/oeqa/selftest/cases/minidebuginfo.py @@ -0,0 +1,60 @@ +# +# Copyright OpenEmbedded Contributors +# +# SPDX-License-Identifier: MIT +# +import os +import subprocess +import tempfile +import shutil + +from oeqa.core.decorator import OETestTag +from oeqa.selftest.case import OESelftestTestCase +from oeqa.utils.commands import bitbake, get_bb_var, get_bb_vars, runCmd + + +class Minidebuginfo(OESelftestTestCase): + def test_minidebuginfo(self): + target_sys = get_bb_var("TARGET_SYS") + binutils = "binutils-cross-{}".format(get_bb_var("TARGET_ARCH")) + + image = 'core-image-minimal' + bb_vars = get_bb_vars(['DEPLOY_DIR_IMAGE', 'IMAGE_LINK_NAME', 'READELF'], image) + + self.write_config(""" +DISTRO_FEATURES:append = " minidebuginfo" +IMAGE_FSTYPES = "tar.bz2" +""") + bitbake("{} {}:do_addto_recipe_sysroot".format(image, binutils)) + + native_sysroot = get_bb_var("RECIPE_SYSROOT_NATIVE", binutils) + + # confirm that executables and shared libraries contain an ELF section + # ".gnu_debugdata" which stores minidebuginfo. + with tempfile.TemporaryDirectory(prefix = "unpackfs-") as unpackedfs: + filename = os.path.join(bb_vars['DEPLOY_DIR_IMAGE'], "{}.tar.bz2".format(bb_vars['IMAGE_LINK_NAME'])) + shutil.unpack_archive(filename, unpackedfs) + + r = runCmd([bb_vars['READELF'], "-W", "-S", os.path.join(unpackedfs, "bin", "busybox")], + native_sysroot = native_sysroot, target_sys = target_sys) + self.assertIn(".gnu_debugdata", r.output) + + r = runCmd([bb_vars['READELF'], "-W", "-S", os.path.join(unpackedfs, "lib", "libc.so.6")], + native_sysroot = native_sysroot, target_sys = target_sys) + self.assertIn(".gnu_debugdata", r.output) + + @OETestTag("runqemu") + def test_minidebuginfo_qemu(self): + """ + Test minidebuginfo inside a qemu. + This runs test_systemd_coredump_minidebuginfo and other minidebuginfo runtime tests which may be added in the future. + """ + + self.write_config(""" +DISTRO_FEATURES:append = " minidebuginfo" +INIT_MANAGER = "systemd" +IMAGE_CLASSES += "testimage" +TEST_SUITES = "ping ssh systemd" + """) + bitbake('core-image-minimal') + bitbake('-c testimage core-image-minimal') diff --git a/meta/lib/oeqa/selftest/cases/multiconfig.py b/meta/lib/oeqa/selftest/cases/multiconfig.py index 39b92f2439..f509cbf607 100644 --- a/meta/lib/oeqa/selftest/cases/multiconfig.py +++ b/meta/lib/oeqa/selftest/cases/multiconfig.py @@ -1,4 +1,6 @@ # +# Copyright OpenEmbedded Contributors +# # SPDX-License-Identifier: MIT # @@ -17,7 +19,7 @@ class MultiConfig(OESelftestTestCase): """ config = """ -IMAGE_INSTALL_append_pn-core-image-full-cmdline = " multiconfig-image-packager-tiny multiconfig-image-packager-musl" +IMAGE_INSTALL:append:pn-core-image-full-cmdline = " multiconfig-image-packager-tiny multiconfig-image-packager-musl" BBMULTICONFIG = "tiny musl" """ self.write_config(config) @@ -52,7 +54,7 @@ TMPDIR = "${TOPDIR}/tmp-mc-tiny" self.write_config(config) testconfig = textwrap.dedent('''\ - MCTESTVAR_append = "1" + MCTESTVAR:append = "1" ''') self.write_config(testconfig, 'test') @@ -64,9 +66,22 @@ TMPDIR = "${TOPDIR}/tmp-mc-tiny" self.assertIn('MCTESTVAR=test1', result.output.splitlines()) testconfig = textwrap.dedent('''\ - MCTESTVAR_append = "2" + MCTESTVAR:append = "2" ''') self.write_config(testconfig, 'test') result = bitbake('mc:test:multiconfig-test-parse -c showvar') self.assertIn('MCTESTVAR=test2', result.output.splitlines()) + + def test_multiconfig_inlayer(self): + """ + Test that a multiconfig from meta-selftest works. + """ + + config = """ +BBMULTICONFIG = "muslmc" +""" + self.write_config(config) + + # Build a core-image-minimal, only dry run needed to check config is present + bitbake('mc:muslmc:bash -n') diff --git a/meta/lib/oeqa/selftest/cases/newlib.py b/meta/lib/oeqa/selftest/cases/newlib.py new file mode 100644 index 0000000000..fe57aa51f2 --- /dev/null +++ b/meta/lib/oeqa/selftest/cases/newlib.py @@ -0,0 +1,13 @@ +# +# Copyright OpenEmbedded Contributors +# +# SPDX-License-Identifier: MIT +# + +from oeqa.selftest.case import OESelftestTestCase +from oeqa.utils.commands import bitbake + +class NewlibTest(OESelftestTestCase): + def test_newlib(self): + self.write_config('TCLIBC = "newlib"') + bitbake("newlib libgloss") diff --git a/meta/lib/oeqa/selftest/cases/oelib/buildhistory.py b/meta/lib/oeqa/selftest/cases/oelib/buildhistory.py index 6d80827652..042ccdd2b4 100644 --- a/meta/lib/oeqa/selftest/cases/oelib/buildhistory.py +++ b/meta/lib/oeqa/selftest/cases/oelib/buildhistory.py @@ -1,24 +1,27 @@ # +# Copyright OpenEmbedded Contributors +# # SPDX-License-Identifier: MIT # import os +import sys from oeqa.selftest.case import OESelftestTestCase import tempfile +import operator from oeqa.utils.commands import get_bb_var class TestBlobParsing(OESelftestTestCase): def setUp(self): - import time self.repo_path = tempfile.mkdtemp(prefix='selftest-buildhistory', dir=get_bb_var('TOPDIR')) try: from git import Repo self.repo = Repo.init(self.repo_path) - except ImportError: - self.skipTest('Python module GitPython is not present') + except ImportError as e: + self.skipTest('Python module GitPython is not present (%s) (%s)' % (e, sys.path)) self.test_file = "test" self.var_map = {} @@ -27,6 +30,16 @@ class TestBlobParsing(OESelftestTestCase): import shutil shutil.rmtree(self.repo_path) + @property + def heads_default(self): + """ + Support repos defaulting to master or to main branch + """ + try: + return self.repo.heads.main + except AttributeError: + return self.repo.heads.master + def commit_vars(self, to_add={}, to_remove = [], msg="A commit message"): if len(to_add) == 0 and len(to_remove) == 0: return @@ -45,7 +58,7 @@ class TestBlobParsing(OESelftestTestCase): def test_blob_to_dict(self): """ - Test convertion of git blobs to dictionary + Test conversion of git blobs to dictionary """ from oe.buildhistory_analysis import blob_to_dict valuesmap = { "foo" : "1", "bar" : "2" } @@ -64,10 +77,10 @@ class TestBlobParsing(OESelftestTestCase): changesmap = { "foo-2" : ("2", "8"), "bar" : ("","4"), "bar-2" : ("","5")} self.commit_vars(to_add = { "foo" : "1", "foo-2" : "2", "foo-3" : "3" }) - blob1 = self.repo.heads.master.commit.tree.blobs[0] + blob1 = self.heads_default.commit.tree.blobs[0] self.commit_vars(to_add = { "foo-2" : "8", "bar" : "4", "bar-2" : "5" }) - blob2 = self.repo.heads.master.commit.tree.blobs[0] + blob2 = self.heads_default.commit.tree.blobs[0] change_records = compare_dict_blobs(os.path.join(self.repo_path, self.test_file), blob1, blob2, False, False) @@ -83,10 +96,10 @@ class TestBlobParsing(OESelftestTestCase): defaultmap = { x : ("default", "1") for x in ["PKG", "PKGE", "PKGV", "PKGR"]} self.commit_vars(to_add = { "foo" : "1" }) - blob1 = self.repo.heads.master.commit.tree.blobs[0] + blob1 = self.heads_default.commit.tree.blobs[0] self.commit_vars(to_add = { "PKG" : "1", "PKGE" : "1", "PKGV" : "1", "PKGR" : "1" }) - blob2 = self.repo.heads.master.commit.tree.blobs[0] + blob2 = self.heads_default.commit.tree.blobs[0] change_records = compare_dict_blobs(os.path.join(self.repo_path, self.test_file), blob1, blob2, False, False) @@ -97,3 +110,48 @@ class TestBlobParsing(OESelftestTestCase): var_changes[x.fieldname] = (oldvalue, x.newvalue) self.assertEqual(defaultmap, var_changes, "Defaults not set properly") + +class TestFileListCompare(OESelftestTestCase): + + def test_compare_file_lists(self): + # Test that a directory tree that moves location such as /lib/modules/5.4.40-yocto-standard -> /lib/modules/5.4.43-yocto-standard + # is correctly identified as a move + from oe.buildhistory_analysis import compare_file_lists, FileChange + + with open(self.tc.files_dir + "/buildhistory_filelist1.txt", "r") as f: + filelist1 = f.readlines() + with open(self.tc.files_dir + "/buildhistory_filelist2.txt", "r") as f: + filelist2 = f.readlines() + + expectedResult = [ + '/lib/libcap.so.2 changed symlink target from libcap.so.2.33 to libcap.so.2.34', + '/lib/libcap.so.2.33 moved to /lib/libcap.so.2.34', + '/lib/modules/5.4.40-yocto-standard moved to /lib/modules/5.4.43-yocto-standard', + '/lib/modules/5.4.43-yocto-standard/modules.builtin.alias.bin was added', + '/usr/bin/gawk-5.0.1 moved to /usr/bin/gawk-5.1.0', + '/usr/lib/libbtrfsutil.so changed symlink target from libbtrfsutil.so.1.1.1 to libbtrfsutil.so.1.2.0', + '/usr/lib/libbtrfsutil.so.1 changed symlink target from libbtrfsutil.so.1.1.1 to libbtrfsutil.so.1.2.0', + '/usr/lib/libbtrfsutil.so.1.1.1 moved to /usr/lib/libbtrfsutil.so.1.2.0', + '/usr/lib/libkmod.so changed symlink target from libkmod.so.2.3.4 to libkmod.so.2.3.5', + '/usr/lib/libkmod.so.2 changed symlink target from libkmod.so.2.3.4 to libkmod.so.2.3.5', + '/usr/lib/libkmod.so.2.3.4 moved to /usr/lib/libkmod.so.2.3.5', + '/usr/lib/libpixman-1.so.0 changed symlink target from libpixman-1.so.0.38.4 to libpixman-1.so.0.40.0', + '/usr/lib/libpixman-1.so.0.38.4 moved to /usr/lib/libpixman-1.so.0.40.0', + '/usr/lib/opkg/alternatives/rtcwake was added', + '/usr/lib/python3.8/site-packages/PyGObject-3.34.0.egg-info moved to /usr/lib/python3.8/site-packages/PyGObject-3.36.1.egg-info', + '/usr/lib/python3.8/site-packages/btrfsutil-1.1.1-py3.8.egg-info moved to /usr/lib/python3.8/site-packages/btrfsutil-1.2.0-py3.8.egg-info', + '/usr/lib/python3.8/site-packages/pycairo-1.19.0.egg-info moved to /usr/lib/python3.8/site-packages/pycairo-1.19.1.egg-info', + '/usr/sbin/rtcwake changed type from file to symlink', + '/usr/sbin/rtcwake changed permissions from rwxr-xr-x to rwxrwxrwx', + '/usr/sbin/rtcwake changed symlink target from None to /usr/sbin/rtcwake.util-linux', + '/usr/sbin/rtcwake.util-linux was added' + ] + + result = compare_file_lists(filelist1, filelist2) + rendered = [] + for entry in sorted(result, key=operator.attrgetter("path")): + rendered.append(str(entry)) + + self.maxDiff = None + self.assertCountEqual(rendered, expectedResult) + diff --git a/meta/lib/oeqa/selftest/cases/oelib/elf.py b/meta/lib/oeqa/selftest/cases/oelib/elf.py index d0a28090f2..7bf550b6fd 100644 --- a/meta/lib/oeqa/selftest/cases/oelib/elf.py +++ b/meta/lib/oeqa/selftest/cases/oelib/elf.py @@ -1,4 +1,6 @@ # +# Copyright OpenEmbedded Contributors +# # SPDX-License-Identifier: MIT # @@ -21,6 +23,6 @@ class TestElf(TestCase): self.assertEqual(oe.qa.elf_machine_to_string(0xB7), "AArch64") self.assertEqual(oe.qa.elf_machine_to_string(0xF7), "BPF") - self.assertEqual(oe.qa.elf_machine_to_string(0x00), "Unknown (0)") + self.assertEqual(oe.qa.elf_machine_to_string(0x00), "Unset") self.assertEqual(oe.qa.elf_machine_to_string(0xDEADBEEF), "Unknown (3735928559)") self.assertEqual(oe.qa.elf_machine_to_string("foobar"), "Unknown ('foobar')") diff --git a/meta/lib/oeqa/selftest/cases/oelib/license.py b/meta/lib/oeqa/selftest/cases/oelib/license.py index 6ebbee589f..5eea12e761 100644 --- a/meta/lib/oeqa/selftest/cases/oelib/license.py +++ b/meta/lib/oeqa/selftest/cases/oelib/license.py @@ -1,4 +1,6 @@ # +# Copyright OpenEmbedded Contributors +# # SPDX-License-Identifier: MIT # @@ -15,11 +17,11 @@ class SeenVisitor(oe.license.LicenseVisitor): class TestSingleLicense(TestCase): licenses = [ - "GPLv2", - "LGPL-2.0", - "Artistic", + "GPL-2.0-only", + "LGPL-2.0-only", + "Artistic-1.0", "MIT", - "GPLv3+", + "GPL-3.0-or-later", "FOO_BAR", ] invalid_licenses = ["GPL/BSD"] @@ -67,9 +69,9 @@ class TestComplexCombinations(TestSimpleCombinations): "FOO & (BAR | BAZ)&MOO": ["FOO", "BAR", "MOO"], "(ALPHA|(BETA&THETA)|OMEGA)&DELTA": ["OMEGA", "DELTA"], "((ALPHA|BETA)&FOO)|BAZ": ["BETA", "FOO"], - "(GPL-2.0|Proprietary)&BSD-4-clause&MIT": ["GPL-2.0", "BSD-4-clause", "MIT"], + "(GPL-2.0-only|Proprietary)&BSD-4-clause&MIT": ["GPL-2.0-only", "BSD-4-clause", "MIT"], } - preferred = ["BAR", "OMEGA", "BETA", "GPL-2.0"] + preferred = ["BAR", "OMEGA", "BETA", "GPL-2.0-only"] class TestIsIncluded(TestCase): tests = { @@ -87,12 +89,12 @@ class TestIsIncluded(TestCase): [True, ["BAR", "FOOBAR"]], ("(FOO | BAR) & FOOBAR | BAZ & MOO & BARFOO", None, "FOO"): [True, ["BAZ", "MOO", "BARFOO"]], - ("GPL-3.0 & GPL-2.0 & LGPL-2.1 | Proprietary", None, None): - [True, ["GPL-3.0", "GPL-2.0", "LGPL-2.1"]], - ("GPL-3.0 & GPL-2.0 & LGPL-2.1 | Proprietary", None, "GPL-3.0"): + ("GPL-3.0-or-later & GPL-2.0-only & LGPL-2.1-only | Proprietary", None, None): + [True, ["GPL-3.0-or-later", "GPL-2.0-only", "LGPL-2.1-only"]], + ("GPL-3.0-or-later & GPL-2.0-only & LGPL-2.1-only | Proprietary", None, "GPL-3.0-or-later"): [True, ["Proprietary"]], - ("GPL-3.0 & GPL-2.0 & LGPL-2.1 | Proprietary", None, "GPL-3.0 Proprietary"): - [False, ["GPL-3.0"]] + ("GPL-3.0-or-later & GPL-2.0-only & LGPL-2.1-only | Proprietary", None, "GPL-3.0-or-later Proprietary"): + [False, ["GPL-3.0-or-later"]] } def test_tests(self): diff --git a/meta/lib/oeqa/selftest/cases/oelib/path.py b/meta/lib/oeqa/selftest/cases/oelib/path.py index a1cfa08c09..b963e447e3 100644 --- a/meta/lib/oeqa/selftest/cases/oelib/path.py +++ b/meta/lib/oeqa/selftest/cases/oelib/path.py @@ -1,4 +1,6 @@ # +# Copyright OpenEmbedded Contributors +# # SPDX-License-Identifier: MIT # diff --git a/meta/lib/oeqa/selftest/cases/oelib/types.py b/meta/lib/oeqa/selftest/cases/oelib/types.py index 7eb49e6f95..58318b18b2 100644 --- a/meta/lib/oeqa/selftest/cases/oelib/types.py +++ b/meta/lib/oeqa/selftest/cases/oelib/types.py @@ -1,4 +1,6 @@ # +# Copyright OpenEmbedded Contributors +# # SPDX-License-Identifier: MIT # diff --git a/meta/lib/oeqa/selftest/cases/oelib/utils.py b/meta/lib/oeqa/selftest/cases/oelib/utils.py index a7214beb4c..0cb46425a0 100644 --- a/meta/lib/oeqa/selftest/cases/oelib/utils.py +++ b/meta/lib/oeqa/selftest/cases/oelib/utils.py @@ -1,4 +1,6 @@ # +# Copyright OpenEmbedded Contributors +# # SPDX-License-Identifier: MIT # @@ -64,7 +66,7 @@ class TestMultiprocessLaunch(TestCase): import bb def testfunction(item, d): - if item == "2" or item == "1": + if item == "2": raise KeyError("Invalid number %s" % item) return "Found %s" % item @@ -99,5 +101,4 @@ class TestMultiprocessLaunch(TestCase): # Assert the function prints exceptions with captured_output() as (out, err): self.assertRaises(bb.BBHandledException, multiprocess_launch, testfunction, ["1", "2", "3", "4", "5", "6"], d, extraargs=(d,)) - self.assertIn("KeyError: 'Invalid number 1'", out.getvalue()) self.assertIn("KeyError: 'Invalid number 2'", out.getvalue()) diff --git a/meta/lib/oeqa/selftest/cases/oescripts.py b/meta/lib/oeqa/selftest/cases/oescripts.py index 41cbe04808..bfbc33b08d 100644 --- a/meta/lib/oeqa/selftest/cases/oescripts.py +++ b/meta/lib/oeqa/selftest/cases/oescripts.py @@ -1,13 +1,16 @@ # +# Copyright OpenEmbedded Contributors +# # SPDX-License-Identifier: MIT # import os import shutil +import importlib import unittest from oeqa.selftest.case import OESelftestTestCase from oeqa.selftest.cases.buildhistory import BuildhistoryBase -from oeqa.utils.commands import Command, runCmd, bitbake, get_bb_var, get_test_layer +from oeqa.utils.commands import runCmd, bitbake, get_bb_var from oeqa.utils import CommandError class BuildhistoryDiffTests(BuildhistoryBase): @@ -20,7 +23,7 @@ class BuildhistoryDiffTests(BuildhistoryBase): pkgv = result.output.rstrip() result = runCmd("buildhistory-diff -p %s" % get_bb_var('BUILDHISTORY_DIR')) expected_endlines = [ - "xcursor-transparent-theme-dev: RDEPENDS: removed \"xcursor-transparent-theme (['= %s-r1'])\", added \"xcursor-transparent-theme (['= %s-r0'])\"" % (pkgv, pkgv), + "xcursor-transparent-theme-dev: RRECOMMENDS: removed \"xcursor-transparent-theme (['= %s-r1'])\", added \"xcursor-transparent-theme (['= %s-r0'])\"" % (pkgv, pkgv), "xcursor-transparent-theme-staticdev: RDEPENDS: removed \"xcursor-transparent-theme-dev (['= %s-r1'])\", added \"xcursor-transparent-theme-dev (['= %s-r0'])\"" % (pkgv, pkgv) ] for line in result.output.splitlines(): @@ -33,22 +36,16 @@ class BuildhistoryDiffTests(BuildhistoryBase): if expected_endlines: self.fail('Missing expected line endings:\n %s' % '\n '.join(expected_endlines)) -class OEScriptTests(OESelftestTestCase): +@unittest.skipUnless(importlib.util.find_spec("cairo"), "Python cairo module is not present") +class OEPybootchartguyTests(OESelftestTestCase): @classmethod def setUpClass(cls): - super(OEScriptTests, cls).setUpClass() - try: - import cairo - except ImportError: - raise unittest.SkipTest('Python module cairo is not present') + super().setUpClass() bitbake("core-image-minimal -c rootfs -f") cls.tmpdir = get_bb_var('TMPDIR') cls.buildstats = cls.tmpdir + "/buildstats/" + sorted(os.listdir(cls.tmpdir + "/buildstats"))[-1] - - scripts_dir = os.path.join(get_bb_var('COREBASE'), 'scripts') - -class OEPybootchartguyTests(OEScriptTests): + cls.scripts_dir = os.path.join(get_bb_var('COREBASE'), 'scripts') def test_pybootchartguy_help(self): runCmd('%s/pybootchartgui/pybootchartgui.py --help' % self.scripts_dir) @@ -68,7 +65,10 @@ class OEPybootchartguyTests(OEScriptTests): class OEGitproxyTests(OESelftestTestCase): - scripts_dir = os.path.join(get_bb_var('COREBASE'), 'scripts') + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.scripts_dir = os.path.join(get_bb_var('COREBASE'), 'scripts') def test_oegitproxy_help(self): try: @@ -126,15 +126,22 @@ class OEGitproxyTests(OESelftestTestCase): class OeRunNativeTest(OESelftestTestCase): def test_oe_run_native(self): bitbake("qemu-helper-native -c addto_recipe_sysroot") - result = runCmd("oe-run-native qemu-helper-native tunctl -h") - self.assertIn("Delete: tunctl -d device-name [-f tun-clone-device]", result.output) + result = runCmd("oe-run-native qemu-helper-native qemu-oe-bridge-helper --help") + self.assertIn("Helper function to find and exec qemu-bridge-helper", result.output) + +class OEListPackageconfigTests(OESelftestTestCase): + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.scripts_dir = os.path.join(get_bb_var('COREBASE'), 'scripts') -class OEListPackageconfigTests(OEScriptTests): #oe-core.scripts.List_all_the_PACKAGECONFIG's_flags def check_endlines(self, results, expected_endlines): for line in results.output.splitlines(): for el in expected_endlines: - if line == el: + if line and line.split()[0] == el.split()[0] and \ + ' '.join(sorted(el.split())) in ' '.join(sorted(line.split())): expected_endlines.remove(el) break @@ -150,8 +157,8 @@ class OEListPackageconfigTests(OEScriptTests): results = runCmd('%s/contrib/list-packageconfig-flags.py' % self.scripts_dir) expected_endlines = [] expected_endlines.append("RECIPE NAME PACKAGECONFIG FLAGS") - expected_endlines.append("pinentry gtk2 libcap ncurses qt secret") - expected_endlines.append("tar acl") + expected_endlines.append("pinentry gtk2 ncurses qt secret") + expected_endlines.append("tar acl selinux") self.check_endlines(results, expected_endlines) @@ -168,21 +175,20 @@ class OEListPackageconfigTests(OEScriptTests): def test_packageconfig_flags_option_all(self): results = runCmd('%s/contrib/list-packageconfig-flags.py -a' % self.scripts_dir) expected_endlines = [] - expected_endlines.append("pinentry-1.1.0") - expected_endlines.append("PACKAGECONFIG ncurses libcap") + expected_endlines.append("pinentry-1.3.1") + expected_endlines.append("PACKAGECONFIG ncurses") expected_endlines.append("PACKAGECONFIG[qt] --enable-pinentry-qt, --disable-pinentry-qt, qtbase-native qtbase") expected_endlines.append("PACKAGECONFIG[gtk2] --enable-pinentry-gtk2, --disable-pinentry-gtk2, gtk+ glib-2.0") - expected_endlines.append("PACKAGECONFIG[libcap] --with-libcap, --without-libcap, libcap") expected_endlines.append("PACKAGECONFIG[ncurses] --enable-ncurses --with-ncurses-include-dir=${STAGING_INCDIR}, --disable-ncurses, ncurses") expected_endlines.append("PACKAGECONFIG[secret] --enable-libsecret, --disable-libsecret, libsecret") self.check_endlines(results, expected_endlines) - def test_packageconfig_flags_optiins_preferred_only(self): + def test_packageconfig_flags_options_preferred_only(self): results = runCmd('%s/contrib/list-packageconfig-flags.py -p' % self.scripts_dir) expected_endlines = [] expected_endlines.append("RECIPE NAME PACKAGECONFIG FLAGS") - expected_endlines.append("pinentry gtk2 libcap ncurses qt secret") + expected_endlines.append("pinentry gtk2 ncurses qt secret") self.check_endlines(results, expected_endlines) diff --git a/meta/lib/oeqa/selftest/cases/overlayfs.py b/meta/lib/oeqa/selftest/cases/overlayfs.py new file mode 100644 index 0000000000..580fbdcb9c --- /dev/null +++ b/meta/lib/oeqa/selftest/cases/overlayfs.py @@ -0,0 +1,541 @@ +# +# Copyright OpenEmbedded Contributors +# +# SPDX-License-Identifier: MIT +# + +from oeqa.selftest.case import OESelftestTestCase +from oeqa.utils.commands import bitbake, runqemu, get_bb_vars +from oeqa.core.decorator import OETestTag +from oeqa.core.decorator.data import skipIfNotMachine + +def getline_qemu(out, line): + for l in out.split('\n'): + if line in l: + return l + +def getline(res, line): + return getline_qemu(res.output, line) + +class OverlayFSTests(OESelftestTestCase): + """Overlayfs class usage tests""" + + def add_overlay_conf_to_machine(self): + machine_inc = """ +OVERLAYFS_MOUNT_POINT[mnt-overlay] = "/mnt/overlay" +""" + self.set_machine_config(machine_inc) + + def test_distro_features_missing(self): + """ + Summary: Check that required DISTRO_FEATURES are set + Expected: Fail when either systemd or overlayfs are not in DISTRO_FEATURES + Author: Vyacheslav Yurkov <uvv.mail@gmail.com> + """ + + config = """ +IMAGE_INSTALL:append = " overlayfs-user" +""" + overlayfs_recipe_append = """ +inherit overlayfs +""" + self.write_config(config) + self.add_overlay_conf_to_machine() + self.write_recipeinc('overlayfs-user', overlayfs_recipe_append) + + res = bitbake('core-image-minimal', ignore_status=True) + line = getline(res, "overlayfs-user was skipped: missing required distro features") + self.assertTrue("overlayfs" in res.output, msg=res.output) + self.assertTrue("systemd" in res.output, msg=res.output) + self.assertTrue("ERROR: Required build target 'core-image-minimal' has no buildable providers." in res.output, msg=res.output) + + def test_not_all_units_installed(self): + """ + Summary: Test QA check that we have required mount units in the image + Expected: Fail because mount unit for overlay partition is not installed + Author: Vyacheslav Yurkov <uvv.mail@gmail.com> + """ + + config = """ +IMAGE_INSTALL:append = " overlayfs-user" +DISTRO_FEATURES:append = " systemd overlayfs usrmerge" +""" + + self.write_config(config) + self.add_overlay_conf_to_machine() + + res = bitbake('core-image-minimal', ignore_status=True) + line = getline(res, " Mount path /mnt/overlay not found in fstab and unit mnt-overlay.mount not found in systemd unit directories") + self.assertTrue(line and line.startswith("WARNING:"), msg=res.output) + line = getline(res, "Not all mount paths and units are installed in the image") + self.assertTrue(line and line.startswith("ERROR:"), msg=res.output) + + def test_not_all_units_installed_but_qa_skipped(self): + """ + Summary: Test skipping the QA check + Expected: Image is created successfully + Author: Claudius Heine <ch@denx.de> + """ + + config = """ +IMAGE_INSTALL:append = " overlayfs-user" +DISTRO_FEATURES:append = " systemd overlayfs usrmerge" +OVERLAYFS_QA_SKIP[mnt-overlay] = "mount-configured" +""" + + self.write_config(config) + self.add_overlay_conf_to_machine() + + bitbake('core-image-minimal') + + def test_mount_unit_not_set(self): + """ + Summary: Test whether mount unit was set properly + Expected: Fail because mount unit was not set + Author: Vyacheslav Yurkov <uvv.mail@gmail.com> + """ + + config = """ +IMAGE_INSTALL:append = " overlayfs-user" +DISTRO_FEATURES:append = " systemd overlayfs usrmerge" +""" + + self.write_config(config) + + res = bitbake('core-image-minimal', ignore_status=True) + line = getline(res, "A recipe uses overlayfs class but there is no OVERLAYFS_MOUNT_POINT set in your MACHINE configuration") + self.assertTrue(line and line.startswith("Parsing recipes...ERROR:"), msg=res.output) + + def test_wrong_mount_unit_set(self): + """ + Summary: Test whether mount unit was set properly + Expected: Fail because not the correct flag used for mount unit + Author: Vyacheslav Yurkov <uvv.mail@gmail.com> + """ + + config = """ +IMAGE_INSTALL:append = " overlayfs-user" +DISTRO_FEATURES:append = " systemd overlayfs usrmerge" +""" + + wrong_machine_config = """ +OVERLAYFS_MOUNT_POINT[usr-share-overlay] = "/usr/share/overlay" +""" + + self.write_config(config) + self.set_machine_config(wrong_machine_config) + + res = bitbake('core-image-minimal', ignore_status=True) + line = getline(res, "Missing required mount point for OVERLAYFS_MOUNT_POINT[mnt-overlay] in your MACHINE configuration") + self.assertTrue(line and line.startswith("Parsing recipes...ERROR:"), msg=res.output) + + def _test_correct_image(self, recipe, data): + """ + Summary: Check that we can create an image when all parameters are + set correctly + Expected: Image is created successfully + Author: Vyacheslav Yurkov <uvv.mail@gmail.com> + """ + + config = """ +IMAGE_INSTALL:append = " overlayfs-user systemd-machine-units" +DISTRO_FEATURES:append = " overlayfs" + +# Use systemd as init manager +INIT_MANAGER = "systemd" + +# enable overlayfs in the kernel +KERNEL_EXTRA_FEATURES:append = " features/overlayfs/overlayfs.scc" +""" + + overlayfs_recipe_append = """ +OVERLAYFS_WRITABLE_PATHS[mnt-overlay] += "/usr/share/another-overlay-mount" + +SYSTEMD_SERVICE:${PN} += " \ + my-application.service \ +" + +do_install:append() { + install -d ${D}${systemd_system_unitdir} + cat <<EOT > ${D}${systemd_system_unitdir}/my-application.service +[Unit] +Description=Sample application start-up unit +After=overlayfs-user-overlays.service +Requires=overlayfs-user-overlays.service + +[Service] +Type=oneshot +ExecStart=/bin/true +RemainAfterExit=true + +[Install] +WantedBy=multi-user.target +EOT +} +""" + + self.write_config(config) + self.add_overlay_conf_to_machine() + self.write_recipeinc(recipe, data) + self.write_recipeinc('overlayfs-user', overlayfs_recipe_append) + + bitbake('core-image-minimal') + + with runqemu('core-image-minimal') as qemu: + # Check that application service started + status, output = qemu.run_serial("systemctl status my-application") + self.assertTrue("active (exited)" in output, msg=output) + + # Check that overlay mounts are dependencies of our application unit + status, output = qemu.run_serial("systemctl list-dependencies my-application") + self.assertTrue("overlayfs-user-overlays.service" in output, msg=output) + + status, output = qemu.run_serial("systemctl list-dependencies overlayfs-user-overlays") + self.assertTrue("usr-share-another\\x2doverlay\\x2dmount.mount" in output, msg=output) + self.assertTrue("usr-share-my\\x2dapplication.mount" in output, msg=output) + + # Check that we have /mnt/overlay fs mounted as tmpfs and + # /usr/share/my-application as an overlay (see overlayfs-user recipe) + status, output = qemu.run_serial("/bin/mount -t tmpfs,overlay") + + line = getline_qemu(output, "on /mnt/overlay") + self.assertTrue(line and line.startswith("tmpfs"), msg=output) + + line = getline_qemu(output, "upperdir=/mnt/overlay/upper/usr/share/my-application") + self.assertTrue(line and line.startswith("overlay"), msg=output) + + line = getline_qemu(output, "upperdir=/mnt/overlay/upper/usr/share/another-overlay-mount") + self.assertTrue(line and line.startswith("overlay"), msg=output) + + @OETestTag("runqemu") + def test_correct_image_fstab(self): + """ + Summary: Check that we can create an image when all parameters are + set correctly via fstab + Expected: Image is created successfully + Author: Stefan Herbrechtsmeier <stefan.herbrechtsmeier@weidmueller.com> + """ + + base_files_append = """ +do_install:append() { + cat <<EOT >> ${D}${sysconfdir}/fstab +tmpfs /mnt/overlay tmpfs mode=1777,strictatime,nosuid,nodev 0 0 +EOT +} +""" + + self._test_correct_image('base-files', base_files_append) + + @OETestTag("runqemu") + def test_correct_image_unit(self): + """ + Summary: Check that we can create an image when all parameters are + set correctly via mount unit + Expected: Image is created successfully + Author: Vyacheslav Yurkov <uvv.mail@gmail.com> + """ + + systemd_machine_unit_append = """ +SYSTEMD_SERVICE:${PN} += " \ + mnt-overlay.mount \ +" + +do_install:append() { + install -d ${D}${systemd_system_unitdir} + cat <<EOT > ${D}${systemd_system_unitdir}/mnt-overlay.mount +[Unit] +Description=Tmpfs directory +DefaultDependencies=no + +[Mount] +What=tmpfs +Where=/mnt/overlay +Type=tmpfs +Options=mode=1777,strictatime,nosuid,nodev + +[Install] +WantedBy=multi-user.target +EOT +} + +""" + + self._test_correct_image('systemd-machine-units', systemd_machine_unit_append) + +@OETestTag("runqemu") +class OverlayFSEtcRunTimeTests(OESelftestTestCase): + """overlayfs-etc class tests""" + + def test_all_required_variables_set(self): + """ + Summary: Check that required variables are set + Expected: Fail when any of required variables is missing + Author: Vyacheslav Yurkov <uvv.mail@gmail.com> + """ + + configBase = """ +# Use systemd as init manager +INIT_MANAGER = "systemd" + +# enable overlayfs in the kernel +KERNEL_EXTRA_FEATURES:append = " features/overlayfs/overlayfs.scc" + +# Image configuration for overlayfs-etc +EXTRA_IMAGE_FEATURES += "overlayfs-etc" +IMAGE_FEATURES:remove = "package-management" +""" + configMountPoint = """ +OVERLAYFS_ETC_MOUNT_POINT = "/data" +""" + configDevice = """ +OVERLAYFS_ETC_DEVICE = "/dev/mmcblk0p1" +""" + + self.write_config(configBase) + res = bitbake('core-image-minimal', ignore_status=True) + line = getline(res, "OVERLAYFS_ETC_MOUNT_POINT must be set in your MACHINE configuration") + self.assertTrue(line, msg=res.output) + + self.append_config(configMountPoint) + res = bitbake('core-image-minimal', ignore_status=True) + line = getline(res, "OVERLAYFS_ETC_DEVICE must be set in your MACHINE configuration") + self.assertTrue(line, msg=res.output) + + self.append_config(configDevice) + res = bitbake('core-image-minimal', ignore_status=True) + line = getline(res, "OVERLAYFS_ETC_FSTYPE should contain a valid file system type on /dev/mmcblk0p1") + self.assertTrue(line, msg=res.output) + + def test_image_feature_conflict(self): + """ + Summary: Overlayfs-etc is not allowed to be used with package-management + Expected: Feature conflict + Author: Vyacheslav Yurkov <uvv.mail@gmail.com> + """ + + config = """ +# Use systemd as init manager +INIT_MANAGER = "systemd" + +# enable overlayfs in the kernel +KERNEL_EXTRA_FEATURES:append = " features/overlayfs/overlayfs.scc" +EXTRA_IMAGE_FEATURES += "overlayfs-etc" +EXTRA_IMAGE_FEATURES += "package-management" +""" + + self.write_config(config) + + res = bitbake('core-image-minimal', ignore_status=True) + line = getline(res, "contains conflicting IMAGE_FEATURES") + self.assertTrue("overlayfs-etc" in res.output, msg=res.output) + self.assertTrue("package-management" in res.output, msg=res.output) + + # https://bugzilla.yoctoproject.org/show_bug.cgi?id=14963 + @skipIfNotMachine("qemux86-64", "tests are qemux86-64 specific currently") + def test_image_feature_is_missing(self): + """ + Summary: Overlayfs-etc class is not applied when image feature is not set + Expected: Image is created successfully but /etc is not an overlay + Author: Vyacheslav Yurkov <uvv.mail@gmail.com> + """ + + config = """ +# Use systemd as init manager +INIT_MANAGER = "systemd" + +# enable overlayfs in the kernel +KERNEL_EXTRA_FEATURES:append = " features/overlayfs/overlayfs.scc" + +IMAGE_FSTYPES += "wic" +WKS_FILE = "overlayfs_etc.wks.in" + +EXTRA_IMAGE_FEATURES += "read-only-rootfs" +# Image configuration for overlayfs-etc +OVERLAYFS_ETC_MOUNT_POINT = "/data" +OVERLAYFS_ETC_DEVICE = "/dev/sda3" +OVERLAYFS_ROOTFS_TYPE = "ext4" +""" + + self.write_config(config) + + bitbake('core-image-minimal') + + with runqemu('core-image-minimal', image_fstype='wic') as qemu: + status, output = qemu.run_serial("/bin/mount") + + line = getline_qemu(output, "upperdir=/data/overlay-etc/upper") + self.assertFalse(line, msg=output) + + @skipIfNotMachine("qemux86-64", "tests are qemux86-64 specific currently") + def test_sbin_init_preinit(self): + self.run_sbin_init(False, "ext4") + + @skipIfNotMachine("qemux86-64", "tests are qemux86-64 specific currently") + def test_sbin_init_original(self): + self.run_sbin_init(True, "ext4") + + @skipIfNotMachine("qemux86-64", "tests are qemux86-64 specific currently") + def test_sbin_init_read_only(self): + self.run_sbin_init(True, "squashfs") + + def run_sbin_init(self, origInit, rootfsType): + """ + Summary: Confirm we can replace original init and mount overlay on top of /etc + Expected: Image is created successfully and /etc is mounted as an overlay + Author: Vyacheslav Yurkov <uvv.mail@gmail.com> + """ + + config = self.get_working_config() + + args = { + 'OVERLAYFS_INIT_OPTION': "" if origInit else "init=/sbin/preinit", + 'OVERLAYFS_ETC_USE_ORIG_INIT_NAME': int(origInit == True), + 'OVERLAYFS_ROOTFS_TYPE': rootfsType, + 'OVERLAYFS_ETC_CREATE_MOUNT_DIRS': int(rootfsType == "ext4") + } + + self.write_config(config.format(**args)) + + bitbake('core-image-minimal') + testFile = "/etc/my-test-data" + + with runqemu('core-image-minimal', image_fstype='wic', discard_writes=False) as qemu: + status, output = qemu.run_serial("/bin/mount") + + line = getline_qemu(output, "/dev/sda3") + self.assertTrue("/data" in output, msg=output) + + line = getline_qemu(output, "upperdir=/data/overlay-etc/upper") + self.assertTrue(line and line.startswith("/data/overlay-etc/upper on /etc type overlay"), msg=output) + + # check that lower layer is not available + status, output = qemu.run_serial("ls -1 /data/overlay-etc/lower") + line = getline_qemu(output, "No such file or directory") + self.assertTrue(line, msg=output) + + status, output = qemu.run_serial("touch " + testFile) + status, output = qemu.run_serial("sync") + status, output = qemu.run_serial("ls -1 " + testFile) + line = getline_qemu(output, testFile) + self.assertTrue(line and line.startswith(testFile), msg=output) + + # Check that file exists in /etc after reboot + with runqemu('core-image-minimal', image_fstype='wic') as qemu: + status, output = qemu.run_serial("ls -1 " + testFile) + line = getline_qemu(output, testFile) + self.assertTrue(line and line.startswith(testFile), msg=output) + + @skipIfNotMachine("qemux86-64", "tests are qemux86-64 specific currently") + def test_lower_layer_access(self): + """ + Summary: Test that lower layer of /etc is available read-only when configured + Expected: Can't write to lower layer. The files on lower and upper different after + modification + Author: Vyacheslav Yurkov <uvv.mail@gmail.com> + """ + + config = self.get_working_config() + + configLower = """ +OVERLAYFS_ETC_EXPOSE_LOWER = "1" +IMAGE_INSTALL:append = " overlayfs-user" +""" + testFile = "lower-layer-test.txt" + + args = { + 'OVERLAYFS_INIT_OPTION': "", + 'OVERLAYFS_ETC_USE_ORIG_INIT_NAME': 1, + 'OVERLAYFS_ROOTFS_TYPE': "ext4", + 'OVERLAYFS_ETC_CREATE_MOUNT_DIRS': 1 + } + + self.write_config(config.format(**args)) + + self.append_config(configLower) + bitbake('core-image-minimal') + + with runqemu('core-image-minimal', image_fstype='wic') as qemu: + status, output = qemu.run_serial("echo \"Modified in upper\" > /etc/" + testFile) + status, output = qemu.run_serial("diff /etc/" + testFile + " /data/overlay-etc/lower/" + testFile) + line = getline_qemu(output, "Modified in upper") + self.assertTrue(line, msg=output) + line = getline_qemu(output, "Original file") + self.assertTrue(line, msg=output) + + status, output = qemu.run_serial("touch /data/overlay-etc/lower/ro-test.txt") + line = getline_qemu(output, "Read-only file system") + self.assertTrue(line, msg=output) + + @skipIfNotMachine("qemux86-64", "tests are qemux86-64 specific currently") + def test_postinst_on_target_for_read_only_rootfs(self): + """ + Summary: The purpose of this test case is to verify that post-installation + on target scripts are executed even if using read-only rootfs when + read-only-rootfs-delayed-postinsts is set + Expected: The test files are created on first boot + """ + + import oe.path + + vars = get_bb_vars(("IMAGE_ROOTFS", "sysconfdir"), "core-image-minimal") + sysconfdir = vars["sysconfdir"] + self.assertIsNotNone(sysconfdir) + # Need to use oe.path here as sysconfdir starts with / + targettestdir = os.path.join(sysconfdir, "postinst-test") + + config = self.get_working_config() + + args = { + 'OVERLAYFS_INIT_OPTION': "", + 'OVERLAYFS_ETC_USE_ORIG_INIT_NAME': 1, + 'OVERLAYFS_ROOTFS_TYPE': "ext4", + 'OVERLAYFS_ETC_CREATE_MOUNT_DIRS': 1 + } + + # read-only-rootfs is already set in get_working_config() + config += 'EXTRA_IMAGE_FEATURES += "read-only-rootfs-delayed-postinsts"\n' + config += 'CORE_IMAGE_EXTRA_INSTALL = "postinst-delayed-b"\n' + + self.write_config(config.format(**args)) + + res = bitbake('core-image-minimal') + + with runqemu('core-image-minimal', image_fstype='wic') as qemu: + for filename in ("rootfs", "delayed-a", "delayed-b"): + status, output = qemu.run_serial("test -f %s && echo found" % os.path.join(targettestdir, filename)) + self.assertIn("found", output, "%s was not present on boot" % filename) + + def get_working_config(self): + return """ +# Use systemd as init manager +INIT_MANAGER = "systemd" + +# enable overlayfs in the kernel +KERNEL_EXTRA_FEATURES:append = " \ + features/overlayfs/overlayfs.scc \ + cfg/fs/squashfs.scc" + +IMAGE_FSTYPES += "wic" +OVERLAYFS_INIT_OPTION = "{OVERLAYFS_INIT_OPTION}" +OVERLAYFS_ROOTFS_TYPE = "{OVERLAYFS_ROOTFS_TYPE}" +OVERLAYFS_ETC_CREATE_MOUNT_DIRS = "{OVERLAYFS_ETC_CREATE_MOUNT_DIRS}" +WKS_FILE = "overlayfs_etc.wks.in" + +EXTRA_IMAGE_FEATURES += "read-only-rootfs" +# Image configuration for overlayfs-etc +EXTRA_IMAGE_FEATURES += "overlayfs-etc" +IMAGE_FEATURES:remove = "package-management" +OVERLAYFS_ETC_MOUNT_POINT = "/data" +OVERLAYFS_ETC_FSTYPE = "ext4" +OVERLAYFS_ETC_DEVICE = "/dev/sda3" +OVERLAYFS_ETC_USE_ORIG_INIT_NAME = "{OVERLAYFS_ETC_USE_ORIG_INIT_NAME}" + +ROOTFS_POSTPROCESS_COMMAND += "{OVERLAYFS_ROOTFS_TYPE}_rootfs" + +ext4_rootfs() {{ +}} + +squashfs_rootfs() {{ + mkdir -p ${{IMAGE_ROOTFS}}/data +}} +""" diff --git a/meta/lib/oeqa/selftest/cases/package.py b/meta/lib/oeqa/selftest/cases/package.py index 291627877e..38ed7173fe 100644 --- a/meta/lib/oeqa/selftest/cases/package.py +++ b/meta/lib/oeqa/selftest/cases/package.py @@ -1,10 +1,11 @@ # +# Copyright OpenEmbedded Contributors +# # SPDX-License-Identifier: MIT # from oeqa.selftest.case import OESelftestTestCase from oeqa.utils.commands import bitbake, get_bb_vars, get_bb_var, runqemu -import stat import subprocess, os import oe.path import re @@ -88,6 +89,13 @@ class VersionOrdering(OESelftestTestCase): self.assertEqual(status - 100, sort, "%s %s (%d) failed" % (ver1, ver2, sort)) class PackageTests(OESelftestTestCase): + # Verify that a recipe cannot rename a package into an existing one + def test_package_name_conflict(self): + res = bitbake("packagenameconflict", ignore_status=True) + self.assertNotEqual(res.status, 0) + err = "package name already exists" + self.assertTrue(err in res.output) + # Verify that a recipe which sets up hardlink files has those preserved into split packages # Also test file sparseness is preserved def test_preserve_sparse_hardlinks(self): @@ -95,11 +103,37 @@ class PackageTests(OESelftestTestCase): dest = get_bb_var('PKGDEST', 'selftest-hardlink') bindir = get_bb_var('bindir', 'selftest-hardlink') + libdir = get_bb_var('libdir', 'selftest-hardlink') + libexecdir = get_bb_var('libexecdir', 'selftest-hardlink') def checkfiles(): # Recipe creates 4 hardlinked files, there is a copy in package/ and a copy in packages-split/ # so expect 8 in total. self.assertEqual(os.stat(dest + "/selftest-hardlink" + bindir + "/hello1").st_nlink, 8) + self.assertEqual(os.stat(dest + "/selftest-hardlink" + libexecdir + "/hello3").st_nlink, 8) + + # Check dbg version + # 2 items, a copy in both package/packages-split so 4 + self.assertEqual(os.stat(dest + "/selftest-hardlink-dbg" + bindir + "/.debug/hello1").st_nlink, 4) + self.assertEqual(os.stat(dest + "/selftest-hardlink-dbg" + libexecdir + "/.debug/hello1").st_nlink, 4) + + # Even though the libexecdir name is 'hello3' or 'hello4', that isn't the debug target name + self.assertEqual(os.path.exists(dest + "/selftest-hardlink-dbg" + libexecdir + "/.debug/hello3"), False) + self.assertEqual(os.path.exists(dest + "/selftest-hardlink-dbg" + libexecdir + "/.debug/hello4"), False) + + # Check the staticdev libraries + # 101 items, a copy in both package/packages-split so 202 + self.assertEqual(os.stat(dest + "/selftest-hardlink-staticdev" + libdir + "/libhello.a").st_nlink, 202) + self.assertEqual(os.stat(dest + "/selftest-hardlink-staticdev" + libdir + "/libhello-25.a").st_nlink, 202) + self.assertEqual(os.stat(dest + "/selftest-hardlink-staticdev" + libdir + "/libhello-50.a").st_nlink, 202) + self.assertEqual(os.stat(dest + "/selftest-hardlink-staticdev" + libdir + "/libhello-75.a").st_nlink, 202) + + # Check static dbg + # 101 items, a copy in both package/packages-split so 202 + self.assertEqual(os.stat(dest + "/selftest-hardlink-dbg" + libdir + "/.debug-static/libhello.a").st_nlink, 202) + self.assertEqual(os.stat(dest + "/selftest-hardlink-dbg" + libdir + "/.debug-static/libhello-25.a").st_nlink, 202) + self.assertEqual(os.stat(dest + "/selftest-hardlink-dbg" + libdir + "/.debug-static/libhello-50.a").st_nlink, 202) + self.assertEqual(os.stat(dest + "/selftest-hardlink-dbg" + libdir + "/.debug-static/libhello-75.a").st_nlink, 202) # Test a sparse file remains sparse sparsestat = os.stat(dest + "/selftest-hardlink" + bindir + "/sparsetest") @@ -116,9 +150,9 @@ class PackageTests(OESelftestTestCase): # Verify gdb to read symbols from separated debug hardlink file correctly def test_gdb_hardlink_debug(self): - features = 'IMAGE_INSTALL_append = " selftest-hardlink"\n' - features += 'IMAGE_INSTALL_append = " selftest-hardlink-dbg"\n' - features += 'IMAGE_INSTALL_append = " selftest-hardlink-gdb"\n' + features = 'IMAGE_INSTALL:append = " selftest-hardlink"\n' + features += 'IMAGE_INSTALL:append = " selftest-hardlink-dbg"\n' + features += 'IMAGE_INSTALL:append = " selftest-hardlink-gdb"\n' self.write_config(features) bitbake("core-image-minimal") @@ -134,8 +168,10 @@ class PackageTests(OESelftestTestCase): self.logger.error("No debugging symbols found. GDB result:\n%s" % output) return False - # Check debugging symbols works correctly - elif re.match("Breakpoint 1.*hello\.c.*4", l): + # Check debugging symbols works correctly. Don't look for a + # source file as optimisation can put the breakpoint inside + # stdio.h. + elif "Breakpoint 1 at" in l: return True self.logger.error("GDB result:\n%d: %s", status, output) @@ -148,3 +184,27 @@ class PackageTests(OESelftestTestCase): '/usr/libexec/hello4']: if not gdbtest(qemu, binary): self.fail('GDB %s failed' % binary) + + def test_preserve_ownership(self): + features = 'IMAGE_INSTALL:append = " selftest-chown"\n' + self.write_config(features) + bitbake("core-image-minimal") + + def check_ownership(qemu, expected_gid, expected_uid, path): + self.logger.info("Check ownership of %s", path) + status, output = qemu.run_serial('stat -c "%U %G" ' + path) + self.assertEqual(status, 1, "stat failed: " + output) + try: + uid, gid = output.split() + self.assertEqual(uid, expected_uid) + self.assertEqual(gid, expected_gid) + except ValueError: + self.fail("Cannot parse output: " + output) + + sysconfdir = get_bb_var('sysconfdir', 'selftest-chown') + with runqemu('core-image-minimal') as qemu: + for path in [ sysconfdir + "/selftest-chown/file", + sysconfdir + "/selftest-chown/dir", + sysconfdir + "/selftest-chown/symlink", + sysconfdir + "/selftest-chown/fifotest/fifo"]: + check_ownership(qemu, "test", "test", path) diff --git a/meta/lib/oeqa/selftest/cases/picolibc.py b/meta/lib/oeqa/selftest/cases/picolibc.py new file mode 100644 index 0000000000..e40b4fc3d3 --- /dev/null +++ b/meta/lib/oeqa/selftest/cases/picolibc.py @@ -0,0 +1,18 @@ +# +# Copyright OpenEmbedded Contributors +# +# SPDX-License-Identifier: MIT +# + +from oeqa.selftest.case import OESelftestTestCase +from oeqa.utils.commands import bitbake, get_bb_var + +class PicolibcTest(OESelftestTestCase): + + def test_picolibc(self): + compatible_machines = ['qemuarm', 'qemuarm64', 'qemuriscv32', 'qemuriscv64'] + machine = get_bb_var('MACHINE') + if machine not in compatible_machines: + self.skipTest('This test only works with machines : %s' % ' '.join(compatible_machines)) + self.write_config('TCLIBC = "picolibc"') + bitbake("picolibc-helloworld") diff --git a/meta/lib/oeqa/selftest/cases/pkgdata.py b/meta/lib/oeqa/selftest/cases/pkgdata.py index 833a1803ba..d786c33018 100644 --- a/meta/lib/oeqa/selftest/cases/pkgdata.py +++ b/meta/lib/oeqa/selftest/cases/pkgdata.py @@ -1,4 +1,6 @@ # +# Copyright OpenEmbedded Contributors +# # SPDX-License-Identifier: MIT # @@ -47,8 +49,8 @@ class OePkgdataUtilTests(OESelftestTestCase): self.assertGreater(pkgsize, 1, "Size should be greater than 1. %s" % result.output) def test_find_path(self): - result = runCmd('oe-pkgdata-util find-path /lib/libz.so.1') - self.assertEqual(result.output, 'zlib: /lib/libz.so.1') + result = runCmd('oe-pkgdata-util find-path /usr/lib/libz.so.1') + self.assertEqual(result.output, 'zlib: /usr/lib/libz.so.1') result = runCmd('oe-pkgdata-util find-path /usr/bin/m4') self.assertEqual(result.output, 'm4: /usr/bin/m4') result = runCmd('oe-pkgdata-util find-path /not/exist', ignore_status=True) @@ -120,8 +122,7 @@ class OePkgdataUtilTests(OESelftestTestCase): curpkg = line.split(':')[0] files[curpkg] = [] return files - bb_vars = get_bb_vars(['base_libdir', 'libdir', 'includedir', 'mandir']) - base_libdir = bb_vars['base_libdir'] + bb_vars = get_bb_vars(['libdir', 'includedir', 'mandir']) libdir = bb_vars['libdir'] includedir = bb_vars['includedir'] mandir = bb_vars['mandir'] @@ -138,7 +139,7 @@ class OePkgdataUtilTests(OESelftestTestCase): self.assertIn('libz1', list(files.keys()), "listed pkgs. files: %s" %result.output) self.assertIn('libz-dev', list(files.keys()), "listed pkgs. files: %s" %result.output) self.assertGreater(len(files['libz1']), 1) - libspec = os.path.join(base_libdir, 'libz.so.1.*') + libspec = os.path.join(libdir, 'libz.so.1.*') found = False for fileitem in files['libz1']: if fnmatch.fnmatchcase(fileitem, libspec): @@ -218,3 +219,9 @@ class OePkgdataUtilTests(OESelftestTestCase): def test_specify_pkgdatadir(self): result = runCmd('oe-pkgdata-util -p %s lookup-pkg zlib' % get_bb_var('PKGDATA_DIR')) self.assertEqual(result.output, 'libz1') + + def test_no_param(self): + result = runCmd('oe-pkgdata-util', ignore_status=True) + self.assertEqual(result.status, 2, "Status different than 2. output: %s" % result.output) + currpos = result.output.find('usage: oe-pkgdata-util') + self.assertTrue(currpos != -1, msg = "Test is Failed. Help is not Displayed in %s" % result.output) diff --git a/meta/lib/oeqa/selftest/cases/prservice.py b/meta/lib/oeqa/selftest/cases/prservice.py index fe1f24ea6d..8da3739c57 100644 --- a/meta/lib/oeqa/selftest/cases/prservice.py +++ b/meta/lib/oeqa/selftest/cases/prservice.py @@ -1,4 +1,6 @@ # +# Copyright OpenEmbedded Contributors +# # SPDX-License-Identifier: MIT # @@ -12,6 +14,8 @@ from oeqa.selftest.case import OESelftestTestCase from oeqa.utils.commands import runCmd, bitbake, get_bb_var from oeqa.utils.network import get_free_port +import bb.utils + class BitbakePrTests(OESelftestTestCase): @classmethod @@ -19,11 +23,21 @@ class BitbakePrTests(OESelftestTestCase): super(BitbakePrTests, cls).setUpClass() cls.pkgdata_dir = get_bb_var('PKGDATA_DIR') + cls.exported_db_path = os.path.join(cls.builddir, 'export.inc') + cls.current_db_path = os.path.join(get_bb_var('PERSISTENT_DIR'), 'prserv.sqlite3') + + def cleanup(self): + # Ensure any memory resident bitbake is stopped + bitbake("-m") + # Remove any existing export file or prserv database + bb.utils.remove(self.exported_db_path) + bb.utils.remove(self.current_db_path + "*") + def get_pr_version(self, package_name): package_data_file = os.path.join(self.pkgdata_dir, 'runtime', package_name) package_data = ftools.read_file(package_data_file) find_pr = re.search(r"PKGR: r[0-9]+\.([0-9]+)", package_data) - self.assertTrue(find_pr, "No PKG revision found in %s" % package_data_file) + self.assertTrue(find_pr, "No PKG revision found via regex 'PKGR: r[0-9]+\.([0-9]+)' in %s" % package_data_file) return int(find_pr.group(1)) def get_task_stamp(self, package_name, recipe_task): @@ -40,13 +54,14 @@ class BitbakePrTests(OESelftestTestCase): return str(stamps[0]) def increment_package_pr(self, package_name): - inc_data = "do_package_append() {\n bb.build.exec_func('do_test_prserv', d)\n}\ndo_test_prserv() {\necho \"The current date is: %s\"\n}" % datetime.datetime.now() + inc_data = "do_package:append() {\n bb.build.exec_func('do_test_prserv', d)\n}\ndo_test_prserv() {\necho \"The current date is: %s\" > ${PKGDESTWORK}/${PN}.datestamp\n}" % datetime.datetime.now() self.write_recipeinc(package_name, inc_data) res = bitbake(package_name, ignore_status=True) self.delete_recipeinc(package_name) self.assertEqual(res.status, 0, msg=res.output) def config_pr_tests(self, package_name, package_type='rpm', pr_socket='localhost:0'): + self.cleanup() config_package_data = 'PACKAGE_CLASSES = "package_%s"' % package_type self.write_config(config_package_data) config_server_data = 'PRSERV_HOST = "%s"' % pr_socket @@ -63,33 +78,35 @@ class BitbakePrTests(OESelftestTestCase): pr_2 = self.get_pr_version(package_name) stamp_2 = self.get_task_stamp(package_name, track_task) - self.assertTrue(pr_2 - pr_1 == 1, "Step between same pkg. revision is greater than 1") + self.assertTrue(pr_2 - pr_1 == 1, "New PR %s did not increment as expected (from %s), difference should be 1" % (pr_2, pr_1)) self.assertTrue(stamp_1 != stamp_2, "Different pkg rev. but same stamp: %s" % stamp_1) + self.cleanup() + def run_test_pr_export_import(self, package_name, replace_current_db=True): self.config_pr_tests(package_name) self.increment_package_pr(package_name) pr_1 = self.get_pr_version(package_name) - exported_db_path = os.path.join(self.builddir, 'export.inc') - export_result = runCmd("bitbake-prserv-tool export %s" % exported_db_path, ignore_status=True) + export_result = runCmd("bitbake-prserv-tool export %s" % self.exported_db_path, ignore_status=True) self.assertEqual(export_result.status, 0, msg="PR Service database export failed: %s" % export_result.output) - self.assertTrue(os.path.exists(exported_db_path)) + self.assertTrue(os.path.exists(self.exported_db_path), msg="%s didn't exist, tool output %s" % (self.exported_db_path, export_result.output)) if replace_current_db: - current_db_path = os.path.join(get_bb_var('PERSISTENT_DIR'), 'prserv.sqlite3') - self.assertTrue(os.path.exists(current_db_path), msg="Path to current PR Service database is invalid: %s" % current_db_path) - os.remove(current_db_path) + self.assertTrue(os.path.exists(self.current_db_path), msg="Path to current PR Service database is invalid: %s" % self.current_db_path) + os.remove(self.current_db_path) - import_result = runCmd("bitbake-prserv-tool import %s" % exported_db_path, ignore_status=True) - os.remove(exported_db_path) + import_result = runCmd("bitbake-prserv-tool import %s" % self.exported_db_path, ignore_status=True) + #os.remove(self.exported_db_path) self.assertEqual(import_result.status, 0, msg="PR Service database import failed: %s" % import_result.output) self.increment_package_pr(package_name) pr_2 = self.get_pr_version(package_name) - self.assertTrue(pr_2 - pr_1 == 1, "Step between same pkg. revision is greater than 1") + self.assertTrue(pr_2 - pr_1 == 1, "New PR %s did not increment as expected (from %s), difference should be 1" % (pr_2, pr_1)) + + self.cleanup() def test_import_export_replace_db(self): self.run_test_pr_export_import('m4') diff --git a/meta/lib/oeqa/selftest/cases/pseudo.py b/meta/lib/oeqa/selftest/cases/pseudo.py new file mode 100644 index 0000000000..3ef8786022 --- /dev/null +++ b/meta/lib/oeqa/selftest/cases/pseudo.py @@ -0,0 +1,29 @@ +# +# Copyright OpenEmbedded Contributors +# +# SPDX-License-Identifier: MIT +# + +import glob +import os +import shutil +from oeqa.utils.commands import bitbake, get_test_layer +from oeqa.selftest.case import OESelftestTestCase + +class Pseudo(OESelftestTestCase): + + def test_pseudo_pyc_creation(self): + self.write_config("") + + metaselftestpath = get_test_layer() + pycache_path = os.path.join(metaselftestpath, 'lib/__pycache__') + if os.path.exists(pycache_path): + shutil.rmtree(pycache_path) + + bitbake('pseudo-pyc-test -c install') + + test1_pyc_present = len(glob.glob(os.path.join(pycache_path, 'pseudo_pyc_test1.*.pyc'))) + self.assertTrue(test1_pyc_present, 'test1 pyc file missing, should be created outside of pseudo context.') + + test2_pyc_present = len(glob.glob(os.path.join(pycache_path, 'pseudo_pyc_test2.*.pyc'))) + self.assertFalse(test2_pyc_present, 'test2 pyc file present, should not be created in pseudo context.') diff --git a/meta/lib/oeqa/selftest/cases/recipetool.py b/meta/lib/oeqa/selftest/cases/recipetool.py index c1562c63b2..f742dd4d64 100644 --- a/meta/lib/oeqa/selftest/cases/recipetool.py +++ b/meta/lib/oeqa/selftest/cases/recipetool.py @@ -1,7 +1,10 @@ # +# Copyright OpenEmbedded Contributors +# # SPDX-License-Identifier: MIT # +import errno import os import shutil import tempfile @@ -25,7 +28,17 @@ def tearDownModule(): runCmd('rm -rf %s' % templayerdir) -class RecipetoolBase(devtool.DevtoolBase): +def needTomllib(test): + # This test require python 3.11 or above for the tomllib module or tomli module to be installed + try: + import tomllib + except ImportError: + try: + import tomli + except ImportError: + test.skipTest('Test requires python 3.11 or above for tomllib module or tomli module') + +class RecipetoolBase(devtool.DevtoolTestCase): def setUpLocal(self): super(RecipetoolBase, self).setUpLocal() @@ -35,6 +48,8 @@ class RecipetoolBase(devtool.DevtoolBase): self.testfile = os.path.join(self.tempdir, 'testfile') with open(self.testfile, 'w') as f: f.write('Test file\n') + config = 'BBMASK += "meta-poky/recipes-core/base-files/base-files_%.bbappend"\n' + self.append_config(config) def tearDownLocal(self): runCmd('rm -rf %s/recipes-*' % self.templayerdir) @@ -68,17 +83,16 @@ class RecipetoolBase(devtool.DevtoolBase): return bbappendfile, result.output -class RecipetoolTests(RecipetoolBase): +class RecipetoolAppendTests(RecipetoolBase): @classmethod def setUpClass(cls): - super(RecipetoolTests, cls).setUpClass() + super(RecipetoolAppendTests, cls).setUpClass() # Ensure we have the right data in shlibs/pkgdata cls.logger.info('Running bitbake to generate pkgdata') bitbake('-c packagedata base-files coreutils busybox selftest-recipetool-appendfile') - bb_vars = get_bb_vars(['COREBASE', 'BBPATH']) + bb_vars = get_bb_vars(['COREBASE']) cls.corebase = bb_vars['COREBASE'] - cls.bbpath = bb_vars['BBPATH'] def _try_recipetool_appendfile(self, testrecipe, destfile, newfile, options, expectedlines, expectedfiles): cmd = 'recipetool appendfile %s %s %s %s' % (self.templayerdir, destfile, newfile, options) @@ -94,7 +108,7 @@ class RecipetoolTests(RecipetoolBase): def test_recipetool_appendfile_basic(self): # Basic test - expectedlines = ['FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:"\n', + expectedlines = ['FILESEXTRAPATHS:prepend := "${THISDIR}/${PN}:"\n', '\n'] _, output = self._try_recipetool_appendfile('base-files', '/etc/motd', self.testfile, '', expectedlines, ['motd']) self.assertNotIn('WARNING: ', output) @@ -106,23 +120,29 @@ class RecipetoolTests(RecipetoolBase): self._try_recipetool_appendfile_fail('/dev/console', self.testfile, ['ERROR: /dev/console cannot be handled by this tool']) def test_recipetool_appendfile_alternatives(self): + lspath = '/bin/ls' + dirname = "base_bindir" + if "usrmerge" in get_bb_var('DISTRO_FEATURES'): + lspath = '/usr/bin/ls' + dirname = "bindir" + # Now try with a file we know should be an alternative # (this is very much a fake example, but one we know is reliably an alternative) - self._try_recipetool_appendfile_fail('/bin/ls', self.testfile, ['ERROR: File /bin/ls is an alternative possibly provided by the following recipes:', 'coreutils', 'busybox']) + self._try_recipetool_appendfile_fail(lspath, self.testfile, ['ERROR: File %s is an alternative possibly provided by the following recipes:' % lspath, 'coreutils', 'busybox']) # Need a test file - should be executable testfile2 = os.path.join(self.corebase, 'oe-init-build-env') testfile2name = os.path.basename(testfile2) - expectedlines = ['FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:"\n', + expectedlines = ['FILESEXTRAPATHS:prepend := "${THISDIR}/${PN}:"\n', '\n', 'SRC_URI += "file://%s"\n' % testfile2name, '\n', - 'do_install_append() {\n', - ' install -d ${D}${base_bindir}\n', - ' install -m 0755 ${WORKDIR}/%s ${D}${base_bindir}/ls\n' % testfile2name, + 'do_install:append() {\n', + ' install -d ${D}${%s}\n' % dirname, + ' install -m 0755 ${UNPACKDIR}/%s ${D}${%s}/ls\n' % (testfile2name, dirname), '}\n'] - self._try_recipetool_appendfile('coreutils', '/bin/ls', testfile2, '-r coreutils', expectedlines, [testfile2name]) + self._try_recipetool_appendfile('coreutils', lspath, testfile2, '-r coreutils', expectedlines, [testfile2name]) # Now try bbappending the same file again, contents should not change - bbappendfile, _ = self._try_recipetool_appendfile('coreutils', '/bin/ls', self.testfile, '-r coreutils', expectedlines, [testfile2name]) + bbappendfile, _ = self._try_recipetool_appendfile('coreutils', lspath, self.testfile, '-r coreutils', expectedlines, [testfile2name]) # But file should have copiedfile = os.path.join(os.path.dirname(bbappendfile), 'coreutils', testfile2name) result = runCmd('diff -q %s %s' % (testfile2, copiedfile), ignore_status=True) @@ -138,130 +158,117 @@ class RecipetoolTests(RecipetoolBase): def test_recipetool_appendfile_add(self): # Try arbitrary file add to a recipe - expectedlines = ['FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:"\n', + expectedlines = ['FILESEXTRAPATHS:prepend := "${THISDIR}/${PN}:"\n', '\n', 'SRC_URI += "file://testfile"\n', '\n', - 'do_install_append() {\n', + 'do_install:append() {\n', ' install -d ${D}${datadir}\n', - ' install -m 0644 ${WORKDIR}/testfile ${D}${datadir}/something\n', + ' install -m 0644 ${UNPACKDIR}/testfile ${D}${datadir}/something\n', '}\n'] self._try_recipetool_appendfile('netbase', '/usr/share/something', self.testfile, '-r netbase', expectedlines, ['testfile']) # Try adding another file, this time where the source file is executable # (so we're testing that, plus modifying an existing bbappend) testfile2 = os.path.join(self.corebase, 'oe-init-build-env') testfile2name = os.path.basename(testfile2) - expectedlines = ['FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:"\n', + expectedlines = ['FILESEXTRAPATHS:prepend := "${THISDIR}/${PN}:"\n', '\n', 'SRC_URI += "file://testfile \\\n', ' file://%s \\\n' % testfile2name, ' "\n', '\n', - 'do_install_append() {\n', + 'do_install:append() {\n', ' install -d ${D}${datadir}\n', - ' install -m 0644 ${WORKDIR}/testfile ${D}${datadir}/something\n', - ' install -m 0755 ${WORKDIR}/%s ${D}${datadir}/scriptname\n' % testfile2name, + ' install -m 0644 ${UNPACKDIR}/testfile ${D}${datadir}/something\n', + ' install -m 0755 ${UNPACKDIR}/%s ${D}${datadir}/scriptname\n' % testfile2name, '}\n'] self._try_recipetool_appendfile('netbase', '/usr/share/scriptname', testfile2, '-r netbase', expectedlines, ['testfile', testfile2name]) def test_recipetool_appendfile_add_bindir(self): # Try arbitrary file add to a recipe, this time to a location such that should be installed as executable - expectedlines = ['FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:"\n', + expectedlines = ['FILESEXTRAPATHS:prepend := "${THISDIR}/${PN}:"\n', '\n', 'SRC_URI += "file://testfile"\n', '\n', - 'do_install_append() {\n', + 'do_install:append() {\n', ' install -d ${D}${bindir}\n', - ' install -m 0755 ${WORKDIR}/testfile ${D}${bindir}/selftest-recipetool-testbin\n', + ' install -m 0755 ${UNPACKDIR}/testfile ${D}${bindir}/selftest-recipetool-testbin\n', '}\n'] _, output = self._try_recipetool_appendfile('netbase', '/usr/bin/selftest-recipetool-testbin', self.testfile, '-r netbase', expectedlines, ['testfile']) self.assertNotIn('WARNING: ', output) def test_recipetool_appendfile_add_machine(self): # Try arbitrary file add to a recipe, this time to a location such that should be installed as executable - expectedlines = ['FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:"\n', + expectedlines = ['FILESEXTRAPATHS:prepend := "${THISDIR}/${PN}:"\n', '\n', 'PACKAGE_ARCH = "${MACHINE_ARCH}"\n', '\n', - 'SRC_URI_append_mymachine = " file://testfile"\n', + 'SRC_URI:append:mymachine = " file://testfile"\n', '\n', - 'do_install_append_mymachine() {\n', + 'do_install:append:mymachine() {\n', ' install -d ${D}${datadir}\n', - ' install -m 0644 ${WORKDIR}/testfile ${D}${datadir}/something\n', + ' install -m 0644 ${UNPACKDIR}/testfile ${D}${datadir}/something\n', '}\n'] _, output = self._try_recipetool_appendfile('netbase', '/usr/share/something', self.testfile, '-r netbase -m mymachine', expectedlines, ['mymachine/testfile']) self.assertNotIn('WARNING: ', output) def test_recipetool_appendfile_orig(self): # A file that's in SRC_URI and in do_install with the same name - expectedlines = ['FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:"\n', + expectedlines = ['FILESEXTRAPATHS:prepend := "${THISDIR}/${PN}:"\n', '\n'] _, output = self._try_recipetool_appendfile('selftest-recipetool-appendfile', '/usr/share/selftest-replaceme-orig', self.testfile, '', expectedlines, ['selftest-replaceme-orig']) self.assertNotIn('WARNING: ', output) def test_recipetool_appendfile_todir(self): # A file that's in SRC_URI and in do_install with destination directory rather than file - expectedlines = ['FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:"\n', + expectedlines = ['FILESEXTRAPATHS:prepend := "${THISDIR}/${PN}:"\n', '\n'] _, output = self._try_recipetool_appendfile('selftest-recipetool-appendfile', '/usr/share/selftest-replaceme-todir', self.testfile, '', expectedlines, ['selftest-replaceme-todir']) self.assertNotIn('WARNING: ', output) def test_recipetool_appendfile_renamed(self): # A file that's in SRC_URI with a different name to the destination file - expectedlines = ['FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:"\n', + expectedlines = ['FILESEXTRAPATHS:prepend := "${THISDIR}/${PN}:"\n', '\n'] _, output = self._try_recipetool_appendfile('selftest-recipetool-appendfile', '/usr/share/selftest-replaceme-renamed', self.testfile, '', expectedlines, ['file1']) self.assertNotIn('WARNING: ', output) def test_recipetool_appendfile_subdir(self): # A file that's in SRC_URI in a subdir - expectedlines = ['FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:"\n', + expectedlines = ['FILESEXTRAPATHS:prepend := "${THISDIR}/${PN}:"\n', '\n', 'SRC_URI += "file://testfile"\n', '\n', - 'do_install_append() {\n', + 'do_install:append() {\n', ' install -d ${D}${datadir}\n', - ' install -m 0644 ${WORKDIR}/testfile ${D}${datadir}/selftest-replaceme-subdir\n', + ' install -m 0644 ${UNPACKDIR}/testfile ${D}${datadir}/selftest-replaceme-subdir\n', '}\n'] _, output = self._try_recipetool_appendfile('selftest-recipetool-appendfile', '/usr/share/selftest-replaceme-subdir', self.testfile, '', expectedlines, ['testfile']) self.assertNotIn('WARNING: ', output) - def test_recipetool_appendfile_src_glob(self): - # A file that's in SRC_URI as a glob - expectedlines = ['FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:"\n', - '\n', - 'SRC_URI += "file://testfile"\n', - '\n', - 'do_install_append() {\n', - ' install -d ${D}${datadir}\n', - ' install -m 0644 ${WORKDIR}/testfile ${D}${datadir}/selftest-replaceme-src-globfile\n', - '}\n'] - _, output = self._try_recipetool_appendfile('selftest-recipetool-appendfile', '/usr/share/selftest-replaceme-src-globfile', self.testfile, '', expectedlines, ['testfile']) - self.assertNotIn('WARNING: ', output) - def test_recipetool_appendfile_inst_glob(self): # A file that's in do_install as a glob - expectedlines = ['FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:"\n', + expectedlines = ['FILESEXTRAPATHS:prepend := "${THISDIR}/${PN}:"\n', '\n'] _, output = self._try_recipetool_appendfile('selftest-recipetool-appendfile', '/usr/share/selftest-replaceme-inst-globfile', self.testfile, '', expectedlines, ['selftest-replaceme-inst-globfile']) self.assertNotIn('WARNING: ', output) def test_recipetool_appendfile_inst_todir_glob(self): # A file that's in do_install as a glob with destination as a directory - expectedlines = ['FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:"\n', + expectedlines = ['FILESEXTRAPATHS:prepend := "${THISDIR}/${PN}:"\n', '\n'] _, output = self._try_recipetool_appendfile('selftest-recipetool-appendfile', '/usr/share/selftest-replaceme-inst-todir-globfile', self.testfile, '', expectedlines, ['selftest-replaceme-inst-todir-globfile']) self.assertNotIn('WARNING: ', output) def test_recipetool_appendfile_patch(self): # A file that's added by a patch in SRC_URI - expectedlines = ['FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:"\n', + expectedlines = ['FILESEXTRAPATHS:prepend := "${THISDIR}/${PN}:"\n', '\n', 'SRC_URI += "file://testfile"\n', '\n', - 'do_install_append() {\n', + 'do_install:append() {\n', ' install -d ${D}${sysconfdir}\n', - ' install -m 0644 ${WORKDIR}/testfile ${D}${sysconfdir}/selftest-replaceme-patched\n', + ' install -m 0644 ${UNPACKDIR}/testfile ${D}${sysconfdir}/selftest-replaceme-patched\n', '}\n'] _, output = self._try_recipetool_appendfile('selftest-recipetool-appendfile', '/etc/selftest-replaceme-patched', self.testfile, '', expectedlines, ['testfile']) for line in output.splitlines(): @@ -273,20 +280,20 @@ class RecipetoolTests(RecipetoolBase): def test_recipetool_appendfile_script(self): # Now, a file that's in SRC_URI but installed by a script (so no mention in do_install) - expectedlines = ['FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:"\n', + expectedlines = ['FILESEXTRAPATHS:prepend := "${THISDIR}/${PN}:"\n', '\n', 'SRC_URI += "file://testfile"\n', '\n', - 'do_install_append() {\n', + 'do_install:append() {\n', ' install -d ${D}${datadir}\n', - ' install -m 0644 ${WORKDIR}/testfile ${D}${datadir}/selftest-replaceme-scripted\n', + ' install -m 0644 ${UNPACKDIR}/testfile ${D}${datadir}/selftest-replaceme-scripted\n', '}\n'] _, output = self._try_recipetool_appendfile('selftest-recipetool-appendfile', '/usr/share/selftest-replaceme-scripted', self.testfile, '', expectedlines, ['testfile']) self.assertNotIn('WARNING: ', output) def test_recipetool_appendfile_inst_func(self): # A file that's installed from a function called by do_install - expectedlines = ['FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:"\n', + expectedlines = ['FILESEXTRAPATHS:prepend := "${THISDIR}/${PN}:"\n', '\n'] _, output = self._try_recipetool_appendfile('selftest-recipetool-appendfile', '/usr/share/selftest-replaceme-inst-func', self.testfile, '', expectedlines, ['selftest-replaceme-inst-func']) self.assertNotIn('WARNING: ', output) @@ -296,13 +303,13 @@ class RecipetoolTests(RecipetoolBase): # First try without specifying recipe self._try_recipetool_appendfile_fail('/usr/share/selftest-replaceme-postinst', self.testfile, ['File /usr/share/selftest-replaceme-postinst may be written out in a pre/postinstall script of the following recipes:', 'selftest-recipetool-appendfile']) # Now specify recipe - expectedlines = ['FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:"\n', + expectedlines = ['FILESEXTRAPATHS:prepend := "${THISDIR}/${PN}:"\n', '\n', 'SRC_URI += "file://testfile"\n', '\n', - 'do_install_append() {\n', + 'do_install:append() {\n', ' install -d ${D}${datadir}\n', - ' install -m 0644 ${WORKDIR}/testfile ${D}${datadir}/selftest-replaceme-postinst\n', + ' install -m 0644 ${UNPACKDIR}/testfile ${D}${datadir}/selftest-replaceme-postinst\n', '}\n'] _, output = self._try_recipetool_appendfile('selftest-recipetool-appendfile', '/usr/share/selftest-replaceme-postinst', self.testfile, '-r selftest-recipetool-appendfile', expectedlines, ['testfile']) @@ -345,6 +352,9 @@ class RecipetoolTests(RecipetoolBase): filename = try_appendfile_wc('-w') self.assertEqual(filename, recipefn.split('_')[0] + '_%.bbappend') + +class RecipetoolCreateTests(RecipetoolBase): + def test_recipetool_create(self): # Try adding a recipe tempsrc = os.path.join(self.tempdir, 'srctree') @@ -354,14 +364,13 @@ class RecipetoolTests(RecipetoolBase): result = runCmd('recipetool create -o %s %s -x %s' % (recipefile, srcuri, tempsrc)) self.assertTrue(os.path.isfile(recipefile)) checkvars = {} - checkvars['LICENSE'] = 'GPLv2' + checkvars['LICENSE'] = 'GPL-2.0-only' checkvars['LIC_FILES_CHKSUM'] = 'file://COPYING;md5=b234ee4d69f5fce4486a80fdaf4a4263' checkvars['SRC_URI'] = 'https://github.com/logrotate/logrotate/releases/download/${PV}/logrotate-${PV}.tar.xz' - checkvars['SRC_URI[md5sum]'] = 'a560c57fac87c45b2fc17406cdf79288' checkvars['SRC_URI[sha256sum]'] = '2e6a401cac9024db2288297e3be1a8ab60e7401ba8e91225218aaf4a27e82a07' self._test_recipe_contents(recipefile, checkvars, []) - def test_recipetool_create_git(self): + def test_recipetool_create_autotools(self): if 'x11' not in get_bb_var('DISTRO_FEATURES'): self.skipTest('Test requires x11 as distro feature') # Ensure we have the right data in shlibs/pkgdata @@ -370,15 +379,15 @@ class RecipetoolTests(RecipetoolBase): tempsrc = os.path.join(self.tempdir, 'srctree') os.makedirs(tempsrc) recipefile = os.path.join(self.tempdir, 'libmatchbox.bb') - srcuri = 'git://git.yoctoproject.org/libmatchbox' + srcuri = 'git://git.yoctoproject.org/libmatchbox;protocol=https' result = runCmd(['recipetool', 'create', '-o', recipefile, srcuri + ";rev=9f7cf8895ae2d39c465c04cc78e918c157420269", '-x', tempsrc]) self.assertTrue(os.path.isfile(recipefile), 'recipetool did not create recipe file; output:\n%s' % result.output) checkvars = {} - checkvars['LICENSE'] = 'LGPLv2.1' + checkvars['LICENSE'] = 'LGPL-2.1-only' checkvars['LIC_FILES_CHKSUM'] = 'file://COPYING;md5=7fbc338309ac38fefcd64b04bb903e34' checkvars['S'] = '${WORKDIR}/git' - checkvars['PV'] = '1.11+git${SRCPV}' - checkvars['SRC_URI'] = srcuri + checkvars['PV'] = '1.11+git' + checkvars['SRC_URI'] = srcuri + ';branch=master' checkvars['DEPENDS'] = set(['libcheck', 'libjpeg-turbo', 'libpng', 'libx11', 'libxext', 'pango']) inherits = ['autotools', 'pkgconfig'] self._test_recipe_contents(recipefile, checkvars, inherits) @@ -387,8 +396,8 @@ class RecipetoolTests(RecipetoolBase): # Try adding a recipe temprecipe = os.path.join(self.tempdir, 'recipe') os.makedirs(temprecipe) - pv = '1.7.3.0' - srcuri = 'http://www.dest-unreach.org/socat/download/socat-%s.tar.bz2' % pv + pv = '1.7.4.1' + srcuri = 'http://www.dest-unreach.org/socat/download/Archive/socat-%s.tar.bz2' % pv result = runCmd('recipetool create %s -o %s' % (srcuri, temprecipe)) dirlist = os.listdir(temprecipe) if len(dirlist) > 1: @@ -397,7 +406,7 @@ class RecipetoolTests(RecipetoolBase): self.fail('recipetool did not create recipe file; output:\n%s\ndirlist:\n%s' % (result.output, str(dirlist))) self.assertEqual(dirlist[0], 'socat_%s.bb' % pv, 'Recipe file incorrectly named') checkvars = {} - checkvars['LICENSE'] = set(['Unknown', 'GPLv2']) + checkvars['LICENSE'] = set(['Unknown', 'GPL-2.0-only']) checkvars['LIC_FILES_CHKSUM'] = set(['file://COPYING.OpenSSL;md5=5c9bccc77f67a8328ef4ebaf468116f4', 'file://COPYING;md5=b234ee4d69f5fce4486a80fdaf4a4263']) # We don't check DEPENDS since they are variable for this recipe depending on what's in the sysroot checkvars['S'] = None @@ -413,72 +422,294 @@ class RecipetoolTests(RecipetoolBase): result = runCmd('recipetool create -o %s %s' % (temprecipe, srcuri)) self.assertTrue(os.path.isfile(recipefile)) checkvars = {} - checkvars['LICENSE'] = set(['LGPLv2.1', 'MPL-1.1']) + checkvars['LICENSE'] = set(['LGPL-2.1-only', 'MPL-1.1-only']) checkvars['SRC_URI'] = 'http://taglib.github.io/releases/taglib-${PV}.tar.gz' - checkvars['SRC_URI[md5sum]'] = 'cee7be0ccfc892fa433d6c837df9522a' checkvars['SRC_URI[sha256sum]'] = 'b6d1a5a610aae6ff39d93de5efd0fdc787aa9e9dc1e7026fa4c961b26563526b' checkvars['DEPENDS'] = set(['boost', 'zlib']) inherits = ['cmake'] self._test_recipe_contents(recipefile, checkvars, inherits) - def test_recipetool_create_github(self): - # Basic test to see if github URL mangling works + def test_recipetool_create_npm(self): + collections = get_bb_var('BBFILE_COLLECTIONS').split() + if "openembedded-layer" not in collections: + self.skipTest("Test needs meta-oe for nodejs") + temprecipe = os.path.join(self.tempdir, 'recipe') os.makedirs(temprecipe) - recipefile = os.path.join(temprecipe, 'meson_git.bb') - srcuri = 'https://github.com/mesonbuild/meson;rev=0.32.0' - result = runCmd(['recipetool', 'create', '-o', temprecipe, srcuri]) + recipefile = os.path.join(temprecipe, 'savoirfairelinux-node-server-example_1.0.0.bb') + shrinkwrap = os.path.join(temprecipe, 'savoirfairelinux-node-server-example', 'npm-shrinkwrap.json') + srcuri = 'npm://registry.npmjs.org;package=@savoirfairelinux/node-server-example;version=1.0.0' + result = runCmd('recipetool create -o %s \'%s\'' % (temprecipe, srcuri)) self.assertTrue(os.path.isfile(recipefile)) + self.assertTrue(os.path.isfile(shrinkwrap)) checkvars = {} - checkvars['LICENSE'] = set(['Apache-2.0']) - checkvars['SRC_URI'] = 'git://github.com/mesonbuild/meson;protocol=https' + checkvars['SUMMARY'] = 'Node Server Example' + checkvars['HOMEPAGE'] = 'https://github.com/savoirfairelinux/node-server-example#readme' + checkvars['LICENSE'] = 'BSD-3-Clause & ISC & MIT & Unknown' + urls = [] + urls.append('npm://registry.npmjs.org/;package=@savoirfairelinux/node-server-example;version=${PV}') + urls.append('npmsw://${THISDIR}/${BPN}/npm-shrinkwrap.json') + checkvars['SRC_URI'] = set(urls) + checkvars['S'] = '${WORKDIR}/npm' + checkvars['LICENSE:${PN}'] = 'MIT' + checkvars['LICENSE:${PN}-base64'] = 'Unknown' + checkvars['LICENSE:${PN}-accepts'] = 'MIT' + checkvars['LICENSE:${PN}-inherits'] = 'ISC' + inherits = ['npm'] + self._test_recipe_contents(recipefile, checkvars, inherits) + + def test_recipetool_create_github(self): + # Basic test to see if github URL mangling works. Deliberately use an + # older release of Meson at present so we don't need a toml parser. + temprecipe = os.path.join(self.tempdir, 'recipe') + os.makedirs(temprecipe) + recipefile = os.path.join(temprecipe, 'python3-meson_git.bb') + srcuri = 'https://github.com/mesonbuild/meson;rev=0.52.1' + cmd = ['recipetool', 'create', '-o', temprecipe, srcuri] + result = runCmd(cmd) + self.assertTrue(os.path.isfile(recipefile), msg="recipe %s not created for command %s, output %s" % (recipefile, " ".join(cmd), result.output)) + checkvars = {} + checkvars['LICENSE'] = set(['Apache-2.0', "Unknown"]) + checkvars['SRC_URI'] = 'git://github.com/mesonbuild/meson;protocol=https;branch=0.52' inherits = ['setuptools3'] self._test_recipe_contents(recipefile, checkvars, inherits) def test_recipetool_create_python3_setuptools(self): # Test creating python3 package from tarball (using setuptools3 class) + # Use the --no-pypi switch to avoid creating a pypi enabled recipe and + # and check the created recipe as if it was a more general tarball temprecipe = os.path.join(self.tempdir, 'recipe') os.makedirs(temprecipe) pn = 'python-magic' pv = '0.4.15' recipefile = os.path.join(temprecipe, '%s_%s.bb' % (pn, pv)) srcuri = 'https://files.pythonhosted.org/packages/84/30/80932401906eaf787f2e9bd86dc458f1d2e75b064b4c187341f29516945c/python-magic-%s.tar.gz' % pv - result = runCmd('recipetool create -o %s %s' % (temprecipe, srcuri)) + result = runCmd('recipetool create --no-pypi -o %s %s' % (temprecipe, srcuri)) self.assertTrue(os.path.isfile(recipefile)) checkvars = {} checkvars['LICENSE'] = set(['MIT']) checkvars['LIC_FILES_CHKSUM'] = 'file://LICENSE;md5=16a934f165e8c3245f241e77d401bb88' checkvars['SRC_URI'] = 'https://files.pythonhosted.org/packages/84/30/80932401906eaf787f2e9bd86dc458f1d2e75b064b4c187341f29516945c/python-magic-${PV}.tar.gz' - checkvars['SRC_URI[md5sum]'] = 'e384c95a47218f66c6501cd6dd45ff59' checkvars['SRC_URI[sha256sum]'] = 'f3765c0f582d2dfc72c15f3b5a82aecfae9498bd29ca840d72f37d7bd38bfcd5' inherits = ['setuptools3'] self._test_recipe_contents(recipefile, checkvars, inherits) - def test_recipetool_create_python3_distutils(self): - # Test creating python3 package from tarball (using distutils3 class) + def test_recipetool_create_python3_setuptools_pypi_tarball(self): + # Test creating python3 package from tarball (using setuptools3 and pypi classes) temprecipe = os.path.join(self.tempdir, 'recipe') os.makedirs(temprecipe) - pn = 'docutils' - pv = '0.14' + pn = 'python-magic' + pv = '0.4.15' recipefile = os.path.join(temprecipe, '%s_%s.bb' % (pn, pv)) - srcuri = 'https://files.pythonhosted.org/packages/84/f4/5771e41fdf52aabebbadecc9381d11dea0fa34e4759b4071244fa094804c/docutils-%s.tar.gz' % pv + srcuri = 'https://files.pythonhosted.org/packages/84/30/80932401906eaf787f2e9bd86dc458f1d2e75b064b4c187341f29516945c/python-magic-%s.tar.gz' % pv + result = runCmd('recipetool create -o %s %s' % (temprecipe, srcuri)) + self.assertTrue(os.path.isfile(recipefile)) + checkvars = {} + checkvars['LICENSE'] = set(['MIT']) + checkvars['LIC_FILES_CHKSUM'] = 'file://LICENSE;md5=16a934f165e8c3245f241e77d401bb88' + checkvars['SRC_URI[sha256sum]'] = 'f3765c0f582d2dfc72c15f3b5a82aecfae9498bd29ca840d72f37d7bd38bfcd5' + checkvars['PYPI_PACKAGE'] = pn + inherits = ['setuptools3', 'pypi'] + self._test_recipe_contents(recipefile, checkvars, inherits) + + def test_recipetool_create_python3_setuptools_pypi(self): + # Test creating python3 package from pypi url (using setuptools3 and pypi classes) + # Intentionnaly using setuptools3 class here instead of any of the pep517 class + # to avoid the toml dependency and allows this test to run on host autobuilders + # with older version of python + temprecipe = os.path.join(self.tempdir, 'recipe') + os.makedirs(temprecipe) + pn = 'python-magic' + pv = '0.4.15' + recipefile = os.path.join(temprecipe, '%s_%s.bb' % (pn, pv)) + # First specify the required version in the url + srcuri = 'https://pypi.org/project/%s/%s' % (pn, pv) + runCmd('recipetool create -o %s %s' % (temprecipe, srcuri)) + self.assertTrue(os.path.isfile(recipefile)) + checkvars = {} + checkvars['LICENSE'] = set(['MIT']) + checkvars['LIC_FILES_CHKSUM'] = 'file://LICENSE;md5=16a934f165e8c3245f241e77d401bb88' + checkvars['SRC_URI[sha256sum]'] = 'f3765c0f582d2dfc72c15f3b5a82aecfae9498bd29ca840d72f37d7bd38bfcd5' + checkvars['PYPI_PACKAGE'] = pn + inherits = ['setuptools3', "pypi"] + self._test_recipe_contents(recipefile, checkvars, inherits) + + # Now specify the version as a recipetool parameter + runCmd('rm -rf %s' % recipefile) + self.assertFalse(os.path.isfile(recipefile)) + srcuri = 'https://pypi.org/project/%s' % pn + runCmd('recipetool create -o %s %s --version %s' % (temprecipe, srcuri, pv)) + self.assertTrue(os.path.isfile(recipefile)) + checkvars = {} + checkvars['LICENSE'] = set(['MIT']) + checkvars['LIC_FILES_CHKSUM'] = 'file://LICENSE;md5=16a934f165e8c3245f241e77d401bb88' + checkvars['SRC_URI[sha256sum]'] = 'f3765c0f582d2dfc72c15f3b5a82aecfae9498bd29ca840d72f37d7bd38bfcd5' + checkvars['PYPI_PACKAGE'] = pn + inherits = ['setuptools3', "pypi"] + self._test_recipe_contents(recipefile, checkvars, inherits) + + # Now, try to grab latest version of the package, so we cannot guess the name of the recipe, + # unless hardcoding the latest version but it means we will need to update the test for each release, + # so use a regexp + runCmd('rm -rf %s' % recipefile) + self.assertFalse(os.path.isfile(recipefile)) + recipefile_re = r'%s_(.*)\.bb' % pn + result = runCmd('recipetool create -o %s %s' % (temprecipe, srcuri)) + dirlist = os.listdir(temprecipe) + if len(dirlist) > 1: + self.fail('recipetool created more than just one file; output:\n%s\ndirlist:\n%s' % (result.output, str(dirlist))) + if len(dirlist) < 1 or not os.path.isfile(os.path.join(temprecipe, dirlist[0])): + self.fail('recipetool did not create recipe file; output:\n%s\ndirlist:\n%s' % (result.output, str(dirlist))) + import re + match = re.match(recipefile_re, dirlist[0]) + self.assertTrue(match) + latest_pv = match.group(1) + self.assertTrue(latest_pv != pv) + recipefile = os.path.join(temprecipe, '%s_%s.bb' % (pn, latest_pv)) + # Do not check LIC_FILES_CHKSUM and SRC_URI checksum here to avoid having updating the test on each release + checkvars = {} + checkvars['LICENSE'] = set(['MIT']) + checkvars['PYPI_PACKAGE'] = pn + inherits = ['setuptools3', "pypi"] + self._test_recipe_contents(recipefile, checkvars, inherits) + + def test_recipetool_create_python3_pep517_setuptools_build_meta(self): + # This test require python 3.11 or above for the tomllib module or tomli module to be installed + needTomllib(self) + + # Test creating python3 package from tarball (using setuptools.build_meta class) + temprecipe = os.path.join(self.tempdir, 'recipe') + os.makedirs(temprecipe) + pn = 'webcolors' + pv = '1.13' + recipefile = os.path.join(temprecipe, 'python3-%s_%s.bb' % (pn, pv)) + srcuri = 'https://files.pythonhosted.org/packages/a1/fb/f95560c6a5d4469d9c49e24cf1b5d4d21ffab5608251c6020a965fb7791c/%s-%s.tar.gz' % (pn, pv) + result = runCmd('recipetool create -o %s %s' % (temprecipe, srcuri)) + self.assertTrue(os.path.isfile(recipefile)) + checkvars = {} + checkvars['SUMMARY'] = 'A library for working with the color formats defined by HTML and CSS.' + checkvars['LICENSE'] = set(['BSD-3-Clause']) + checkvars['LIC_FILES_CHKSUM'] = 'file://LICENSE;md5=702b1ef12cf66832a88f24c8f2ee9c19' + checkvars['SRC_URI[sha256sum]'] = 'c225b674c83fa923be93d235330ce0300373d02885cef23238813b0d5668304a' + inherits = ['python_setuptools_build_meta', 'pypi'] + + self._test_recipe_contents(recipefile, checkvars, inherits) + + def test_recipetool_create_python3_pep517_poetry_core_masonry_api(self): + # This test require python 3.11 or above for the tomllib module or tomli module to be installed + needTomllib(self) + + # Test creating python3 package from tarball (using poetry.core.masonry.api class) + temprecipe = os.path.join(self.tempdir, 'recipe') + os.makedirs(temprecipe) + pn = 'iso8601' + pv = '2.1.0' + recipefile = os.path.join(temprecipe, 'python3-%s_%s.bb' % (pn, pv)) + srcuri = 'https://files.pythonhosted.org/packages/b9/f3/ef59cee614d5e0accf6fd0cbba025b93b272e626ca89fb70a3e9187c5d15/%s-%s.tar.gz' % (pn, pv) + result = runCmd('recipetool create -o %s %s' % (temprecipe, srcuri)) + self.assertTrue(os.path.isfile(recipefile)) + checkvars = {} + checkvars['SUMMARY'] = 'Simple module to parse ISO 8601 dates' + checkvars['LICENSE'] = set(['MIT']) + checkvars['LIC_FILES_CHKSUM'] = 'file://LICENSE;md5=aab31f2ef7ba214a5a341eaa47a7f367' + checkvars['SRC_URI[sha256sum]'] = '6b1d3829ee8921c4301998c909f7829fa9ed3cbdac0d3b16af2d743aed1ba8df' + inherits = ['python_poetry_core', 'pypi'] + + self._test_recipe_contents(recipefile, checkvars, inherits) + + def test_recipetool_create_python3_pep517_flit_core_buildapi(self): + # This test require python 3.11 or above for the tomllib module or tomli module to be installed + needTomllib(self) + + # Test creating python3 package from tarball (using flit_core.buildapi class) + temprecipe = os.path.join(self.tempdir, 'recipe') + os.makedirs(temprecipe) + pn = 'typing-extensions' + pv = '4.8.0' + recipefile = os.path.join(temprecipe, 'python3-%s_%s.bb' % (pn, pv)) + srcuri = 'https://files.pythonhosted.org/packages/1f/7a/8b94bb016069caa12fc9f587b28080ac33b4fbb8ca369b98bc0a4828543e/typing_extensions-%s.tar.gz' % pv + result = runCmd('recipetool create -o %s %s' % (temprecipe, srcuri)) + self.assertTrue(os.path.isfile(recipefile)) + checkvars = {} + checkvars['SUMMARY'] = 'Backported and Experimental Type Hints for Python 3.8+' + checkvars['LICENSE'] = set(['PSF-2.0']) + checkvars['LIC_FILES_CHKSUM'] = 'file://LICENSE;md5=fcf6b249c2641540219a727f35d8d2c2' + checkvars['SRC_URI[sha256sum]'] = 'df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef' + inherits = ['python_flit_core', 'pypi'] + + self._test_recipe_contents(recipefile, checkvars, inherits) + + def test_recipetool_create_python3_pep517_hatchling(self): + # This test require python 3.11 or above for the tomllib module or tomli module to be installed + needTomllib(self) + + # Test creating python3 package from tarball (using hatchling class) + temprecipe = os.path.join(self.tempdir, 'recipe') + os.makedirs(temprecipe) + pn = 'jsonschema' + pv = '4.19.1' + recipefile = os.path.join(temprecipe, 'python3-%s_%s.bb' % (pn, pv)) + srcuri = 'https://files.pythonhosted.org/packages/e4/43/087b24516db11722c8687e0caf0f66c7785c0b1c51b0ab951dfde924e3f5/jsonschema-%s.tar.gz' % pv + result = runCmd('recipetool create -o %s %s' % (temprecipe, srcuri)) + self.assertTrue(os.path.isfile(recipefile)) + checkvars = {} + checkvars['SUMMARY'] = 'An implementation of JSON Schema validation for Python' + checkvars['HOMEPAGE'] = 'https://github.com/python-jsonschema/jsonschema' + checkvars['LICENSE'] = set(['MIT']) + checkvars['LIC_FILES_CHKSUM'] = 'file://COPYING;md5=7a60a81c146ec25599a3e1dabb8610a8 file://json/LICENSE;md5=9d4de43111d33570c8fe49b4cb0e01af' + checkvars['SRC_URI[sha256sum]'] = 'ec84cc37cfa703ef7cd4928db24f9cb31428a5d0fa77747b8b51a847458e0bbf' + inherits = ['python_hatchling', 'pypi'] + + self._test_recipe_contents(recipefile, checkvars, inherits) + + def test_recipetool_create_python3_pep517_maturin(self): + # This test require python 3.11 or above for the tomllib module or tomli module to be installed + needTomllib(self) + + # Test creating python3 package from tarball (using maturin class) + temprecipe = os.path.join(self.tempdir, 'recipe') + os.makedirs(temprecipe) + pn = 'pydantic-core' + pv = '2.14.5' + recipefile = os.path.join(temprecipe, 'python3-%s_%s.bb' % (pn, pv)) + srcuri = 'https://files.pythonhosted.org/packages/64/26/cffb93fe9c6b5a91c497f37fae14a4b073ecbc47fc36a9979c7aa888b245/pydantic_core-%s.tar.gz' % pv result = runCmd('recipetool create -o %s %s' % (temprecipe, srcuri)) self.assertTrue(os.path.isfile(recipefile)) checkvars = {} - checkvars['LICENSE'] = set(['PSF', '&', 'BSD', 'GPL']) - checkvars['LIC_FILES_CHKSUM'] = 'file://COPYING.txt;md5=35a23d42b615470583563132872c97d6' - checkvars['SRC_URI'] = 'https://files.pythonhosted.org/packages/84/f4/5771e41fdf52aabebbadecc9381d11dea0fa34e4759b4071244fa094804c/docutils-${PV}.tar.gz' - checkvars['SRC_URI[md5sum]'] = 'c53768d63db3873b7d452833553469de' - checkvars['SRC_URI[sha256sum]'] = '51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274' - inherits = ['distutils3'] + checkvars['HOMEPAGE'] = 'https://github.com/pydantic/pydantic-core' + checkvars['LICENSE'] = set(['MIT']) + checkvars['LIC_FILES_CHKSUM'] = 'file://LICENSE;md5=ab599c188b4a314d2856b3a55030c75c' + checkvars['SRC_URI[sha256sum]'] = '6d30226dfc816dd0fdf120cae611dd2215117e4f9b124af8c60ab9093b6e8e71' + inherits = ['python_maturin', 'pypi'] + + self._test_recipe_contents(recipefile, checkvars, inherits) + + def test_recipetool_create_python3_pep517_mesonpy(self): + # This test require python 3.11 or above for the tomllib module or tomli module to be installed + needTomllib(self) + + # Test creating python3 package from tarball (using mesonpy class) + temprecipe = os.path.join(self.tempdir, 'recipe') + os.makedirs(temprecipe) + pn = 'siphash24' + pv = '1.4' + recipefile = os.path.join(temprecipe, 'python3-%s_%s.bb' % (pn, pv)) + srcuri = 'https://files.pythonhosted.org/packages/c2/32/b934a70592f314afcfa86c7f7e388804a8061be65b822e2aa07e573b6477/%s-%s.tar.gz' % (pn, pv) + result = runCmd('recipetool create -o %s %s' % (temprecipe, srcuri)) + self.assertTrue(os.path.isfile(recipefile)) + checkvars = {} + checkvars['SRC_URI[sha256sum]'] = '7fd65e39b2a7c8c4ddc3a168a687f4610751b0ac2ebb518783c0cdfc30bec4a0' + inherits = ['python_mesonpy', 'pypi'] + self._test_recipe_contents(recipefile, checkvars, inherits) def test_recipetool_create_github_tarball(self): - # Basic test to ensure github URL mangling doesn't apply to release tarballs + # Basic test to ensure github URL mangling doesn't apply to release tarballs. + # Deliberately use an older release of Meson at present so we don't need a toml parser. temprecipe = os.path.join(self.tempdir, 'recipe') os.makedirs(temprecipe) - pv = '0.32.0' - recipefile = os.path.join(temprecipe, 'meson_%s.bb' % pv) + pv = '0.52.1' + recipefile = os.path.join(temprecipe, 'python3-meson_%s.bb' % pv) srcuri = 'https://github.com/mesonbuild/meson/releases/download/%s/meson-%s.tar.gz' % (pv, pv) result = runCmd('recipetool create -o %s %s' % (temprecipe, srcuri)) self.assertTrue(os.path.isfile(recipefile)) @@ -488,32 +719,315 @@ class RecipetoolTests(RecipetoolBase): inherits = ['setuptools3'] self._test_recipe_contents(recipefile, checkvars, inherits) - def test_recipetool_create_git_http(self): + def _test_recipetool_create_git(self, srcuri, branch=None): # Basic test to check http git URL mangling works temprecipe = os.path.join(self.tempdir, 'recipe') os.makedirs(temprecipe) - recipefile = os.path.join(temprecipe, 'matchbox-terminal_git.bb') - srcuri = 'http://git.yoctoproject.org/git/matchbox-terminal' - result = runCmd('recipetool create -o %s %s' % (temprecipe, srcuri)) + name = srcuri.split(';')[0].split('/')[-1] + recipefile = os.path.join(temprecipe, name + '_git.bb') + options = ' -B %s' % branch if branch else '' + result = runCmd('recipetool create -o %s%s "%s"' % (temprecipe, options, srcuri)) + self.assertTrue(os.path.isfile(recipefile)) + checkvars = {} + checkvars['SRC_URI'] = srcuri + for scheme in ['http', 'https']: + if srcuri.startswith(scheme + ":"): + checkvars['SRC_URI'] = 'git%s;protocol=%s' % (srcuri[len(scheme):], scheme) + if ';branch=' not in srcuri: + checkvars['SRC_URI'] += ';branch=' + (branch or 'master') + self._test_recipe_contents(recipefile, checkvars, []) + + def test_recipetool_create_git_http(self): + self._test_recipetool_create_git('http://git.yoctoproject.org/git/matchbox-keyboard') + + def test_recipetool_create_git_srcuri_master(self): + self._test_recipetool_create_git('git://git.yoctoproject.org/matchbox-keyboard;branch=master;protocol=https') + + def test_recipetool_create_git_srcuri_branch(self): + self._test_recipetool_create_git('git://git.yoctoproject.org/matchbox-keyboard;branch=matchbox-keyboard-0-1;protocol=https') + + def test_recipetool_create_git_srcbranch(self): + self._test_recipetool_create_git('git://git.yoctoproject.org/matchbox-keyboard;protocol=https', 'matchbox-keyboard-0-1') + + def _go_urifiy(self, url, version, modulepath = None, pathmajor = None, subdir = None): + modulepath = ",path='%s'" % modulepath if len(modulepath) else '' + pathmajor = ",pathmajor='%s'" % pathmajor if len(pathmajor) else '' + subdir = ",subdir='%s'" % subdir if len(subdir) else '' + return "${@go_src_uri('%s','%s'%s%s%s)}" % (url, version, modulepath, pathmajor, subdir) + + def test_recipetool_create_go(self): + # Basic test to check go recipe generation + temprecipe = os.path.join(self.tempdir, 'recipe') + os.makedirs(temprecipe) + + recipefile = os.path.join(temprecipe, 'edgex-go_git.bb') + deps_require_file = os.path.join(temprecipe, 'edgex-go', 'edgex-go-modules.inc') + lics_require_file = os.path.join(temprecipe, 'edgex-go', 'edgex-go-licenses.inc') + modules_txt_file = os.path.join(temprecipe, 'edgex-go', 'modules.txt') + + srcuri = 'https://github.com/edgexfoundry/edgex-go.git' + srcrev = "v3.0.0" + srcbranch = "main" + + result = runCmd('recipetool create -o %s %s -S %s -B %s' % (temprecipe, srcuri, srcrev, srcbranch)) + + self.maxDiff = None + inherits = ['go-vendor'] + + checkvars = {} + checkvars['GO_IMPORT'] = "github.com/edgexfoundry/edgex-go" + checkvars['SRC_URI'] = {'git://${GO_IMPORT};destsuffix=git/src/${GO_IMPORT};nobranch=1;name=${BPN};protocol=https', + 'file://modules.txt'} + checkvars['LIC_FILES_CHKSUM'] = {'file://src/${GO_IMPORT}/LICENSE;md5=8f8bc924cf73f6a32381e5fd4c58d603'} + self.assertTrue(os.path.isfile(recipefile)) + self._test_recipe_contents(recipefile, checkvars, inherits) + + checkvars = {} + checkvars['VENDORED_LIC_FILES_CHKSUM'] = set( + ['file://src/${GO_IMPORT}/vendor/github.com/Microsoft/go-winio/LICENSE;md5=69205ff73858f2c22b2ca135b557e8ef', + 'file://src/${GO_IMPORT}/vendor/github.com/armon/go-metrics/LICENSE;md5=d2d77030c0183e3d1e66d26dc1f243be', + 'file://src/${GO_IMPORT}/vendor/github.com/cenkalti/backoff/LICENSE;md5=1571d94433e3f3aa05267efd4dbea68b', + 'file://src/${GO_IMPORT}/vendor/github.com/davecgh/go-spew/LICENSE;md5=c06795ed54b2a35ebeeb543cd3a73e56', + 'file://src/${GO_IMPORT}/vendor/github.com/eclipse/paho.mqtt.golang/LICENSE;md5=dcdb33474b60c38efd27356d8f2edec7', + 'file://src/${GO_IMPORT}/vendor/github.com/eclipse/paho.mqtt.golang/edl-v10;md5=3adfcc70f5aeb7a44f3f9b495aa1fbf3', + 'file://src/${GO_IMPORT}/vendor/github.com/edgexfoundry/go-mod-bootstrap/v3/LICENSE;md5=0d6dae39976133b2851fba4c1e1275ff', + 'file://src/${GO_IMPORT}/vendor/github.com/edgexfoundry/go-mod-configuration/v3/LICENSE;md5=0d6dae39976133b2851fba4c1e1275ff', + 'file://src/${GO_IMPORT}/vendor/github.com/edgexfoundry/go-mod-core-contracts/v3/LICENSE;md5=0d6dae39976133b2851fba4c1e1275ff', + 'file://src/${GO_IMPORT}/vendor/github.com/edgexfoundry/go-mod-messaging/v3/LICENSE;md5=0d6dae39976133b2851fba4c1e1275ff', + 'file://src/${GO_IMPORT}/vendor/github.com/edgexfoundry/go-mod-registry/v3/LICENSE;md5=0d6dae39976133b2851fba4c1e1275ff', + 'file://src/${GO_IMPORT}/vendor/github.com/edgexfoundry/go-mod-secrets/v3/LICENSE;md5=f9fa2f4f8e0ef8cc7b5dd150963eb457', + 'file://src/${GO_IMPORT}/vendor/github.com/fatih/color/LICENSE.md;md5=316e6d590bdcde7993fb175662c0dd5a', + 'file://src/${GO_IMPORT}/vendor/github.com/fxamacker/cbor/v2/LICENSE;md5=827f5a2fa861382d35a3943adf9ebb86', + 'file://src/${GO_IMPORT}/vendor/github.com/go-jose/go-jose/v3/LICENSE;md5=3b83ef96387f14655fc854ddc3c6bd57', + 'file://src/${GO_IMPORT}/vendor/github.com/go-jose/go-jose/v3/json/LICENSE;md5=591778525c869cdde0ab5a1bf283cd81', + 'file://src/${GO_IMPORT}/vendor/github.com/go-kit/log/LICENSE;md5=5b7c15ad5fffe2ff6e9d58a6c161f082', + 'file://src/${GO_IMPORT}/vendor/github.com/go-logfmt/logfmt/LICENSE;md5=98e39517c38127f969de33057067091e', + 'file://src/${GO_IMPORT}/vendor/github.com/go-playground/locales/LICENSE;md5=3ccbda375ee345400ad1da85ba522301', + 'file://src/${GO_IMPORT}/vendor/github.com/go-playground/universal-translator/LICENSE;md5=2e2b21ef8f61057977d27c727c84bef1', + 'file://src/${GO_IMPORT}/vendor/github.com/go-playground/validator/v10/LICENSE;md5=a718a0f318d76f7c5d510cbae84f0b60', + 'file://src/${GO_IMPORT}/vendor/github.com/go-redis/redis/v7/LICENSE;md5=58103aa5ea1ee9b7a369c9c4a95ef9b5', + 'file://src/${GO_IMPORT}/vendor/github.com/golang/protobuf/LICENSE;md5=939cce1ec101726fa754e698ac871622', + 'file://src/${GO_IMPORT}/vendor/github.com/gomodule/redigo/LICENSE;md5=2ee41112a44fe7014dce33e26468ba93', + 'file://src/${GO_IMPORT}/vendor/github.com/google/uuid/LICENSE;md5=88073b6dd8ec00fe09da59e0b6dfded1', + 'file://src/${GO_IMPORT}/vendor/github.com/gorilla/mux/LICENSE;md5=33fa1116c45f9e8de714033f99edde13', + 'file://src/${GO_IMPORT}/vendor/github.com/gorilla/websocket/LICENSE;md5=c007b54a1743d596f46b2748d9f8c044', + 'file://src/${GO_IMPORT}/vendor/github.com/hashicorp/consul/api/LICENSE;md5=b8a277a612171b7526e9be072f405ef4', + 'file://src/${GO_IMPORT}/vendor/github.com/hashicorp/errwrap/LICENSE;md5=b278a92d2c1509760384428817710378', + 'file://src/${GO_IMPORT}/vendor/github.com/hashicorp/go-cleanhttp/LICENSE;md5=65d26fcc2f35ea6a181ac777e42db1ea', + 'file://src/${GO_IMPORT}/vendor/github.com/hashicorp/go-hclog/LICENSE;md5=ec7f605b74b9ad03347d0a93a5cc7eb8', + 'file://src/${GO_IMPORT}/vendor/github.com/hashicorp/go-immutable-radix/LICENSE;md5=65d26fcc2f35ea6a181ac777e42db1ea', + 'file://src/${GO_IMPORT}/vendor/github.com/hashicorp/go-multierror/LICENSE;md5=d44fdeb607e2d2614db9464dbedd4094', + 'file://src/${GO_IMPORT}/vendor/github.com/hashicorp/go-rootcerts/LICENSE;md5=65d26fcc2f35ea6a181ac777e42db1ea', + 'file://src/${GO_IMPORT}/vendor/github.com/hashicorp/golang-lru/LICENSE;md5=f27a50d2e878867827842f2c60e30bfc', + 'file://src/${GO_IMPORT}/vendor/github.com/hashicorp/serf/LICENSE;md5=b278a92d2c1509760384428817710378', + 'file://src/${GO_IMPORT}/vendor/github.com/leodido/go-urn/LICENSE;md5=8f50db5538ec1148a9b3d14ed96c3418', + 'file://src/${GO_IMPORT}/vendor/github.com/mattn/go-colorable/LICENSE;md5=24ce168f90aec2456a73de1839037245', + 'file://src/${GO_IMPORT}/vendor/github.com/mattn/go-isatty/LICENSE;md5=f509beadd5a11227c27b5d2ad6c9f2c6', + 'file://src/${GO_IMPORT}/vendor/github.com/mitchellh/consulstructure/LICENSE;md5=96ada10a9e51c98c4656f2cede08c673', + 'file://src/${GO_IMPORT}/vendor/github.com/mitchellh/copystructure/LICENSE;md5=56da355a12d4821cda57b8f23ec34bc4', + 'file://src/${GO_IMPORT}/vendor/github.com/mitchellh/go-homedir/LICENSE;md5=3f7765c3d4f58e1f84c4313cecf0f5bd', + 'file://src/${GO_IMPORT}/vendor/github.com/mitchellh/mapstructure/LICENSE;md5=3f7765c3d4f58e1f84c4313cecf0f5bd', + 'file://src/${GO_IMPORT}/vendor/github.com/mitchellh/reflectwalk/LICENSE;md5=3f7765c3d4f58e1f84c4313cecf0f5bd', + 'file://src/${GO_IMPORT}/vendor/github.com/nats-io/nats.go/LICENSE;md5=86d3f3a95c324c9479bd8986968f4327', + 'file://src/${GO_IMPORT}/vendor/github.com/nats-io/nkeys/LICENSE;md5=86d3f3a95c324c9479bd8986968f4327', + 'file://src/${GO_IMPORT}/vendor/github.com/nats-io/nuid/LICENSE;md5=86d3f3a95c324c9479bd8986968f4327', + 'file://src/${GO_IMPORT}/vendor/github.com/pmezard/go-difflib/LICENSE;md5=e9a2ebb8de779a07500ddecca806145e', + 'file://src/${GO_IMPORT}/vendor/github.com/rcrowley/go-metrics/LICENSE;md5=1bdf5d819f50f141366dabce3be1460f', + 'file://src/${GO_IMPORT}/vendor/github.com/spiffe/go-spiffe/v2/LICENSE;md5=86d3f3a95c324c9479bd8986968f4327', + 'file://src/${GO_IMPORT}/vendor/github.com/stretchr/objx/LICENSE;md5=d023fd31d3ca39ec61eec65a91732735', + 'file://src/${GO_IMPORT}/vendor/github.com/stretchr/testify/LICENSE;md5=188f01994659f3c0d310612333d2a26f', + 'file://src/${GO_IMPORT}/vendor/github.com/x448/float16/LICENSE;md5=de8f8e025d57fe7ee0b67f30d571323b', + 'file://src/${GO_IMPORT}/vendor/github.com/zeebo/errs/LICENSE;md5=84914ab36fc0eb48edbaa53e66e8d326', + 'file://src/${GO_IMPORT}/vendor/golang.org/x/crypto/LICENSE;md5=5d4950ecb7b26d2c5e4e7b4e0dd74707', + 'file://src/${GO_IMPORT}/vendor/golang.org/x/mod/LICENSE;md5=5d4950ecb7b26d2c5e4e7b4e0dd74707', + 'file://src/${GO_IMPORT}/vendor/golang.org/x/net/LICENSE;md5=5d4950ecb7b26d2c5e4e7b4e0dd74707', + 'file://src/${GO_IMPORT}/vendor/golang.org/x/sync/LICENSE;md5=5d4950ecb7b26d2c5e4e7b4e0dd74707', + 'file://src/${GO_IMPORT}/vendor/golang.org/x/sys/LICENSE;md5=5d4950ecb7b26d2c5e4e7b4e0dd74707', + 'file://src/${GO_IMPORT}/vendor/golang.org/x/text/LICENSE;md5=5d4950ecb7b26d2c5e4e7b4e0dd74707', + 'file://src/${GO_IMPORT}/vendor/golang.org/x/tools/LICENSE;md5=5d4950ecb7b26d2c5e4e7b4e0dd74707', + 'file://src/${GO_IMPORT}/vendor/google.golang.org/genproto/LICENSE;md5=3b83ef96387f14655fc854ddc3c6bd57', + 'file://src/${GO_IMPORT}/vendor/google.golang.org/grpc/LICENSE;md5=3b83ef96387f14655fc854ddc3c6bd57', + 'file://src/${GO_IMPORT}/vendor/google.golang.org/protobuf/LICENSE;md5=02d4002e9171d41a8fad93aa7faf3956', + 'file://src/${GO_IMPORT}/vendor/gopkg.in/eapache/queue.v1/LICENSE;md5=1bfd4408d3de090ef6b908b0cc45a316', + 'file://src/${GO_IMPORT}/vendor/gopkg.in/yaml.v3/LICENSE;md5=3c91c17266710e16afdbb2b6d15c761c']) + + self.assertTrue(os.path.isfile(lics_require_file)) + self._test_recipe_contents(lics_require_file, checkvars, []) + + dependencies = \ + [ ('github.com/eclipse/paho.mqtt.golang','v1.4.2', '', '', ''), + ('github.com/edgexfoundry/go-mod-bootstrap','v3.0.1','github.com/edgexfoundry/go-mod-bootstrap/v3','/v3', ''), + ('github.com/edgexfoundry/go-mod-configuration','v3.0.0','github.com/edgexfoundry/go-mod-configuration/v3','/v3', ''), + ('github.com/edgexfoundry/go-mod-core-contracts','v3.0.0','github.com/edgexfoundry/go-mod-core-contracts/v3','/v3', ''), + ('github.com/edgexfoundry/go-mod-messaging','v3.0.0','github.com/edgexfoundry/go-mod-messaging/v3','/v3', ''), + ('github.com/edgexfoundry/go-mod-secrets','v3.0.1','github.com/edgexfoundry/go-mod-secrets/v3','/v3', ''), + ('github.com/fxamacker/cbor','v2.4.0','github.com/fxamacker/cbor/v2','/v2', ''), + ('github.com/gomodule/redigo','v1.8.9', '', '', ''), + ('github.com/google/uuid','v1.3.0', '', '', ''), + ('github.com/gorilla/mux','v1.8.0', '', '', ''), + ('github.com/rcrowley/go-metrics','v0.0.0-20201227073835-cf1acfcdf475', '', '', ''), + ('github.com/spiffe/go-spiffe','v2.1.4','github.com/spiffe/go-spiffe/v2','/v2', ''), + ('github.com/stretchr/testify','v1.8.2', '', '', ''), + ('go.googlesource.com/crypto','v0.8.0','golang.org/x/crypto', '', ''), + ('gopkg.in/eapache/queue.v1','v1.1.0', '', '', ''), + ('gopkg.in/yaml.v3','v3.0.1', '', '', ''), + ('github.com/microsoft/go-winio','v0.6.0','github.com/Microsoft/go-winio', '', ''), + ('github.com/hashicorp/go-metrics','v0.3.10','github.com/armon/go-metrics', '', ''), + ('github.com/cenkalti/backoff','v2.2.1+incompatible', '', '', ''), + ('github.com/davecgh/go-spew','v1.1.1', '', '', ''), + ('github.com/edgexfoundry/go-mod-registry','v3.0.0','github.com/edgexfoundry/go-mod-registry/v3','/v3', ''), + ('github.com/fatih/color','v1.9.0', '', '', ''), + ('github.com/go-jose/go-jose','v3.0.0','github.com/go-jose/go-jose/v3','/v3', ''), + ('github.com/go-kit/log','v0.2.1', '', '', ''), + ('github.com/go-logfmt/logfmt','v0.5.1', '', '', ''), + ('github.com/go-playground/locales','v0.14.1', '', '', ''), + ('github.com/go-playground/universal-translator','v0.18.1', '', '', ''), + ('github.com/go-playground/validator','v10.13.0','github.com/go-playground/validator/v10','/v10', ''), + ('github.com/go-redis/redis','v7.3.0','github.com/go-redis/redis/v7','/v7', ''), + ('github.com/golang/protobuf','v1.5.2', '', '', ''), + ('github.com/gorilla/websocket','v1.4.2', '', '', ''), + ('github.com/hashicorp/consul','v1.20.0','github.com/hashicorp/consul/api', '', 'api'), + ('github.com/hashicorp/errwrap','v1.0.0', '', '', ''), + ('github.com/hashicorp/go-cleanhttp','v0.5.1', '', '', ''), + ('github.com/hashicorp/go-hclog','v0.14.1', '', '', ''), + ('github.com/hashicorp/go-immutable-radix','v1.3.0', '', '', ''), + ('github.com/hashicorp/go-multierror','v1.1.1', '', '', ''), + ('github.com/hashicorp/go-rootcerts','v1.0.2', '', '', ''), + ('github.com/hashicorp/golang-lru','v0.5.4', '', '', ''), + ('github.com/hashicorp/serf','v0.10.1', '', '', ''), + ('github.com/leodido/go-urn','v1.2.3', '', '', ''), + ('github.com/mattn/go-colorable','v0.1.12', '', '', ''), + ('github.com/mattn/go-isatty','v0.0.14', '', '', ''), + ('github.com/mitchellh/consulstructure','v0.0.0-20190329231841-56fdc4d2da54', '', '', ''), + ('github.com/mitchellh/copystructure','v1.2.0', '', '', ''), + ('github.com/mitchellh/go-homedir','v1.1.0', '', '', ''), + ('github.com/mitchellh/mapstructure','v1.5.0', '', '', ''), + ('github.com/mitchellh/reflectwalk','v1.0.2', '', '', ''), + ('github.com/nats-io/nats.go','v1.25.0', '', '', ''), + ('github.com/nats-io/nkeys','v0.4.4', '', '', ''), + ('github.com/nats-io/nuid','v1.0.1', '', '', ''), + ('github.com/pmezard/go-difflib','v1.0.0', '', '', ''), + ('github.com/stretchr/objx','v0.5.0', '', '', ''), + ('github.com/x448/float16','v0.8.4', '', '', ''), + ('github.com/zeebo/errs','v1.3.0', '', '', ''), + ('go.googlesource.com/mod','v0.8.0','golang.org/x/mod', '', ''), + ('go.googlesource.com/net','v0.9.0','golang.org/x/net', '', ''), + ('go.googlesource.com/sync','v0.1.0','golang.org/x/sync', '', ''), + ('go.googlesource.com/sys','v0.7.0','golang.org/x/sys', '', ''), + ('go.googlesource.com/text','v0.9.0','golang.org/x/text', '', ''), + ('go.googlesource.com/tools','v0.6.0','golang.org/x/tools', '', ''), + ('github.com/googleapis/go-genproto','v0.0.0-20230223222841-637eb2293923','google.golang.org/genproto', '', ''), + ('github.com/grpc/grpc-go','v1.53.0','google.golang.org/grpc', '', ''), + ('go.googlesource.com/protobuf','v1.28.1','google.golang.org/protobuf', '', ''), + ] + + src_uri = set() + for d in dependencies: + src_uri.add(self._go_urifiy(*d)) + + checkvars = {} + checkvars['GO_DEPENDENCIES_SRC_URI'] = src_uri + + self.assertTrue(os.path.isfile(deps_require_file)) + self._test_recipe_contents(deps_require_file, checkvars, []) + + def test_recipetool_create_go_replace_modules(self): + # Check handling of replaced modules + temprecipe = os.path.join(self.tempdir, 'recipe') + os.makedirs(temprecipe) + + recipefile = os.path.join(temprecipe, 'openapi-generator_git.bb') + deps_require_file = os.path.join(temprecipe, 'openapi-generator', 'go-modules.inc') + lics_require_file = os.path.join(temprecipe, 'openapi-generator', 'go-licenses.inc') + modules_txt_file = os.path.join(temprecipe, 'openapi-generator', 'modules.txt') + + srcuri = 'https://github.com/OpenAPITools/openapi-generator.git' + srcrev = "v7.2.0" + srcbranch = "master" + srcsubdir = "samples/openapi3/client/petstore/go" + + result = runCmd('recipetool create -o %s %s -S %s -B %s --src-subdir %s' % (temprecipe, srcuri, srcrev, srcbranch, srcsubdir)) + + self.maxDiff = None + inherits = ['go-vendor'] + checkvars = {} - checkvars['LICENSE'] = set(['GPLv2']) - checkvars['SRC_URI'] = 'git://git.yoctoproject.org/git/matchbox-terminal;protocol=http' - inherits = ['pkgconfig', 'autotools'] + checkvars['GO_IMPORT'] = "github.com/OpenAPITools/openapi-generator/samples/openapi3/client/petstore/go" + checkvars['SRC_URI'] = {'git://${GO_IMPORT};destsuffix=git/src/${GO_IMPORT};nobranch=1;name=${BPN};protocol=https', + 'file://modules.txt'} + + self.assertNotIn('Traceback', result.output) + self.assertIn('No license file was detected for the main module', result.output) + self.assertTrue(os.path.isfile(recipefile)) self._test_recipe_contents(recipefile, checkvars, inherits) + # make sure that dependencies don't mention local directory ./go-petstore + dependencies = \ + [ ('github.com/stretchr/testify','v1.8.4', '', '', ''), + ('go.googlesource.com/oauth2','v0.10.0','golang.org/x/oauth2', '', ''), + ('github.com/davecgh/go-spew','v1.1.1', '', '', ''), + ('github.com/golang/protobuf','v1.5.3', '', '', ''), + ('github.com/kr/pretty','v0.3.0', '', '', ''), + ('github.com/pmezard/go-difflib','v1.0.0', '', '', ''), + ('github.com/rogpeppe/go-internal','v1.9.0', '', '', ''), + ('go.googlesource.com/net','v0.12.0','golang.org/x/net', '', ''), + ('github.com/golang/appengine','v1.6.7','google.golang.org/appengine', '', ''), + ('go.googlesource.com/protobuf','v1.31.0','google.golang.org/protobuf', '', ''), + ('gopkg.in/check.v1','v1.0.0-20201130134442-10cb98267c6c', '', '', ''), + ('gopkg.in/yaml.v3','v3.0.1', '', '', ''), + ] + + src_uri = set() + for d in dependencies: + src_uri.add(self._go_urifiy(*d)) + + checkvars = {} + checkvars['GO_DEPENDENCIES_SRC_URI'] = src_uri + + self.assertTrue(os.path.isfile(deps_require_file)) + self._test_recipe_contents(deps_require_file, checkvars, []) + +class RecipetoolTests(RecipetoolBase): + + @classmethod + def setUpClass(cls): + import sys + + super(RecipetoolTests, cls).setUpClass() + bb_vars = get_bb_vars(['BBPATH']) + cls.bbpath = bb_vars['BBPATH'] + libpath = os.path.join(get_bb_var('COREBASE'), 'scripts', 'lib', 'recipetool') + sys.path.insert(0, libpath) + def _copy_file_with_cleanup(self, srcfile, basedstdir, *paths): dstdir = basedstdir self.assertTrue(os.path.exists(dstdir)) for p in paths: dstdir = os.path.join(dstdir, p) if not os.path.exists(dstdir): - os.makedirs(dstdir) - self.track_for_cleanup(dstdir) + try: + os.makedirs(dstdir) + except PermissionError: + return False + except OSError as e: + if e.errno == errno.EROFS: + return False + else: + raise e + if p == "lib": + # Can race with other tests + self.add_command_to_tearDown('rmdir --ignore-fail-on-non-empty %s' % dstdir) + else: + self.track_for_cleanup(dstdir) dstfile = os.path.join(dstdir, os.path.basename(srcfile)) if srcfile != dstfile: - shutil.copy(srcfile, dstfile) + try: + shutil.copy(srcfile, dstfile) + except PermissionError: + return False self.track_for_cleanup(dstfile) + return True def test_recipetool_load_plugin(self): """Test that recipetool loads only the first found plugin in BBPATH.""" @@ -527,20 +1041,148 @@ class RecipetoolTests(RecipetoolBase): plugincontent = fh.readlines() try: self.assertIn('meta-selftest', srcfile, 'wrong bbpath plugin found') - for path in searchpath: - self._copy_file_with_cleanup(srcfile, path, 'lib', 'recipetool') + searchpath = [ + path for path in searchpath + if self._copy_file_with_cleanup(srcfile, path, 'lib', 'recipetool') + ] result = runCmd("recipetool --quiet count") self.assertEqual(result.output, '1') result = runCmd("recipetool --quiet multiloaded") self.assertEqual(result.output, "no") for path in searchpath: result = runCmd("recipetool --quiet bbdir") - self.assertEqual(result.output, path) + self.assertEqual(os.path.realpath(result.output), os.path.realpath(path)) os.unlink(os.path.join(result.output, 'lib', 'recipetool', 'bbpath.py')) finally: with open(srcfile, 'w') as fh: fh.writelines(plugincontent) + def test_recipetool_handle_license_vars(self): + from create import handle_license_vars + from unittest.mock import Mock + + commonlicdir = get_bb_var('COMMON_LICENSE_DIR') + + class DataConnectorCopy(bb.tinfoil.TinfoilDataStoreConnector): + pass + + d = DataConnectorCopy + d.getVar = Mock(return_value=commonlicdir) + d.expand = Mock(side_effect=lambda x: x) + + srctree = tempfile.mkdtemp(prefix='recipetoolqa') + self.track_for_cleanup(srctree) + + # Multiple licenses + licenses = ['MIT', 'ISC', 'BSD-3-Clause', 'Apache-2.0'] + for licence in licenses: + shutil.copy(os.path.join(commonlicdir, licence), os.path.join(srctree, 'LICENSE.' + licence)) + # Duplicate license + shutil.copy(os.path.join(commonlicdir, 'MIT'), os.path.join(srctree, 'LICENSE')) + + extravalues = { + # Duplicate and missing licenses + 'LICENSE': 'Zlib & BSD-2-Clause & Zlib', + 'LIC_FILES_CHKSUM': [ + 'file://README.md;md5=0123456789abcdef0123456789abcd' + ] + } + lines_before = [] + handled = [] + licvalues = handle_license_vars(srctree, lines_before, handled, extravalues, d) + expected_lines_before = [ + '# WARNING: the following LICENSE and LIC_FILES_CHKSUM values are best guesses - it is', + '# your responsibility to verify that the values are complete and correct.', + '# NOTE: Original package / source metadata indicates license is: BSD-2-Clause & Zlib', + '#', + '# NOTE: multiple licenses have been detected; they have been separated with &', + '# in the LICENSE value for now since it is a reasonable assumption that all', + '# of the licenses apply. If instead there is a choice between the multiple', + '# licenses then you should change the value to separate the licenses with |', + '# instead of &. If there is any doubt, check the accompanying documentation', + '# to determine which situation is applicable.', + 'LICENSE = "Apache-2.0 & BSD-2-Clause & BSD-3-Clause & ISC & MIT & Zlib"', + 'LIC_FILES_CHKSUM = "file://LICENSE;md5=0835ade698e0bcf8506ecda2f7b4f302 \\\n' + ' file://LICENSE.Apache-2.0;md5=89aea4e17d99a7cacdbeed46a0096b10 \\\n' + ' file://LICENSE.BSD-3-Clause;md5=550794465ba0ec5312d6919e203a55f9 \\\n' + ' file://LICENSE.ISC;md5=f3b90e78ea0cffb20bf5cca7947a896d \\\n' + ' file://LICENSE.MIT;md5=0835ade698e0bcf8506ecda2f7b4f302 \\\n' + ' file://README.md;md5=0123456789abcdef0123456789abcd"', + '' + ] + self.assertEqual(lines_before, expected_lines_before) + expected_licvalues = [ + ('MIT', 'LICENSE', '0835ade698e0bcf8506ecda2f7b4f302'), + ('Apache-2.0', 'LICENSE.Apache-2.0', '89aea4e17d99a7cacdbeed46a0096b10'), + ('BSD-3-Clause', 'LICENSE.BSD-3-Clause', '550794465ba0ec5312d6919e203a55f9'), + ('ISC', 'LICENSE.ISC', 'f3b90e78ea0cffb20bf5cca7947a896d'), + ('MIT', 'LICENSE.MIT', '0835ade698e0bcf8506ecda2f7b4f302') + ] + self.assertEqual(handled, [('license', expected_licvalues)]) + self.assertEqual(extravalues, {}) + self.assertEqual(licvalues, expected_licvalues) + + + def test_recipetool_split_pkg_licenses(self): + from create import split_pkg_licenses + licvalues = [ + # Duplicate licenses + ('BSD-2-Clause', 'x/COPYING', None), + ('BSD-2-Clause', 'x/LICENSE', None), + # Multiple licenses + ('MIT', 'x/a/LICENSE.MIT', None), + ('ISC', 'x/a/LICENSE.ISC', None), + # Alternative licenses + ('(MIT | ISC)', 'x/b/LICENSE', None), + # Alternative licenses without brackets + ('MIT | BSD-2-Clause', 'x/c/LICENSE', None), + # Multi licenses with alternatives + ('MIT', 'x/d/COPYING', None), + ('MIT | BSD-2-Clause', 'x/d/LICENSE', None), + # Multi licenses with alternatives and brackets + ('Apache-2.0 & ((MIT | ISC) & BSD-3-Clause)', 'x/e/LICENSE', None) + ] + packages = { + '${PN}': '', + 'a': 'x/a', + 'b': 'x/b', + 'c': 'x/c', + 'd': 'x/d', + 'e': 'x/e', + 'f': 'x/f', + 'g': 'x/g', + } + fallback_licenses = { + # Ignored + 'a': 'BSD-3-Clause', + # Used + 'f': 'BSD-3-Clause' + } + outlines = [] + outlicenses = split_pkg_licenses(licvalues, packages, outlines, fallback_licenses) + expected_outlicenses = { + '${PN}': ['BSD-2-Clause'], + 'a': ['ISC', 'MIT'], + 'b': ['(ISC | MIT)'], + 'c': ['(BSD-2-Clause | MIT)'], + 'd': ['(BSD-2-Clause | MIT)', 'MIT'], + 'e': ['(ISC | MIT)', 'Apache-2.0', 'BSD-3-Clause'], + 'f': ['BSD-3-Clause'], + 'g': ['Unknown'] + } + self.assertEqual(outlicenses, expected_outlicenses) + expected_outlines = [ + 'LICENSE:${PN} = "BSD-2-Clause"', + 'LICENSE:a = "ISC & MIT"', + 'LICENSE:b = "(ISC | MIT)"', + 'LICENSE:c = "(BSD-2-Clause | MIT)"', + 'LICENSE:d = "(BSD-2-Clause | MIT) & MIT"', + 'LICENSE:e = "(ISC | MIT) & Apache-2.0 & BSD-3-Clause"', + 'LICENSE:f = "BSD-3-Clause"', + 'LICENSE:g = "Unknown"' + ] + self.assertEqual(outlines, expected_outlines) + class RecipetoolAppendsrcBase(RecipetoolBase): def _try_recipetool_appendsrcfile(self, testrecipe, newfile, destfile, options, expectedlines, expectedfiles): @@ -573,9 +1215,9 @@ class RecipetoolAppendsrcBase(RecipetoolBase): for uri in src_uri: p = urllib.parse.urlparse(uri) if p.scheme == 'file': - return p.netloc + p.path + return p.netloc + p.path, uri - def _test_appendsrcfile(self, testrecipe, filename=None, destdir=None, has_src_uri=True, srcdir=None, newfile=None, options=''): + def _test_appendsrcfile(self, testrecipe, filename=None, destdir=None, has_src_uri=True, srcdir=None, newfile=None, remove=None, machine=None , options=''): if newfile is None: newfile = self.testfile @@ -600,14 +1242,42 @@ class RecipetoolAppendsrcBase(RecipetoolBase): else: destpath = '.' + os.sep - expectedlines = ['FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:"\n', + expectedlines = ['FILESEXTRAPATHS:prepend := "${THISDIR}/${PN}:"\n', '\n'] + + override = "" + if machine: + options += ' -m %s' % machine + override = ':append:%s' % machine + expectedlines.extend(['PACKAGE_ARCH = "${MACHINE_ARCH}"\n', + '\n']) + + if remove: + for entry in remove: + if machine: + entry_remove_line = 'SRC_URI:remove:%s = " %s"\n' % (machine, entry) + else: + entry_remove_line = 'SRC_URI:remove = "%s"\n' % entry + + expectedlines.extend([entry_remove_line, + '\n']) + if has_src_uri: uri = 'file://%s' % filename if expected_subdir: uri += ';subdir=%s' % expected_subdir - expectedlines[0:0] = ['SRC_URI += "%s"\n' % uri, - '\n'] + if machine: + src_uri_line = 'SRC_URI%s = " %s"\n' % (override, uri) + else: + src_uri_line = 'SRC_URI += "%s"\n' % uri + + expectedlines.extend([src_uri_line, '\n']) + + with open("/tmp/tmp.txt", "w") as file: + print(expectedlines, file=file) + + if machine: + filename = '%s/%s' % (machine, filename) return self._try_recipetool_appendsrcfile(testrecipe, newfile, destpath, options, expectedlines, [filename]) @@ -662,18 +1332,46 @@ class RecipetoolAppendsrcTests(RecipetoolAppendsrcBase): def test_recipetool_appendsrcfile_existing_in_src_uri(self): testrecipe = 'base-files' - filepath = self._get_first_file_uri(testrecipe) + filepath,_ = self._get_first_file_uri(testrecipe) self.assertTrue(filepath, 'Unable to test, no file:// uri found in SRC_URI for %s' % testrecipe) self._test_appendsrcfile(testrecipe, filepath, has_src_uri=False) - def test_recipetool_appendsrcfile_existing_in_src_uri_diff_params(self): + def test_recipetool_appendsrcfile_existing_in_src_uri_diff_params(self, machine=None): testrecipe = 'base-files' subdir = 'tmp' - filepath = self._get_first_file_uri(testrecipe) + filepath, srcuri_entry = self._get_first_file_uri(testrecipe) self.assertTrue(filepath, 'Unable to test, no file:// uri found in SRC_URI for %s' % testrecipe) - output = self._test_appendsrcfile(testrecipe, filepath, subdir, has_src_uri=False) - self.assertTrue(any('with different parameters' in l for l in output)) + self._test_appendsrcfile(testrecipe, filepath, subdir, machine=machine, remove=[srcuri_entry]) + + def test_recipetool_appendsrcfile_machine(self): + # A very basic test + self._test_appendsrcfile('base-files', 'a-file', machine='mymachine') + + # Force cleaning the output of previous test + self.tearDownLocal() + + # A more complex test: existing entry in src_uri with different param + self.test_recipetool_appendsrcfile_existing_in_src_uri_diff_params(machine='mymachine') + + def test_recipetool_appendsrcfile_update_recipe_basic(self): + testrecipe = "mtd-utils-selftest" + recipefile = get_bb_var('FILE', testrecipe) + self.assertIn('meta-selftest', recipefile, 'This test expect %s recipe to be in meta-selftest') + cmd = 'recipetool appendsrcfile -W -u meta-selftest %s %s' % (testrecipe, self.testfile) + result = runCmd(cmd) + self.assertNotIn('Traceback', result.output) + self.add_command_to_tearDown('cd %s; rm -f %s/%s; git checkout .' % (os.path.dirname(recipefile), testrecipe, os.path.basename(self.testfile))) + + expected_status = [(' M', '.*/%s$' % os.path.basename(recipefile)), + ('??', '.*/%s/%s$' % (testrecipe, os.path.basename(self.testfile)))] + self._check_repo_status(os.path.dirname(recipefile), expected_status) + result = runCmd('git diff %s' % os.path.basename(recipefile), cwd=os.path.dirname(recipefile)) + removelines = [] + addlines = [ + 'file://%s \\\\' % os.path.basename(self.testfile), + ] + self._check_diff(result.output, addlines, removelines) def test_recipetool_appendsrcfile_replace_file_srcdir(self): testrecipe = 'bash' diff --git a/meta/lib/oeqa/selftest/cases/recipeutils.py b/meta/lib/oeqa/selftest/cases/recipeutils.py index 747870383b..9949737172 100644 --- a/meta/lib/oeqa/selftest/cases/recipeutils.py +++ b/meta/lib/oeqa/selftest/cases/recipeutils.py @@ -1,15 +1,13 @@ # +# Copyright OpenEmbedded Contributors +# # SPDX-License-Identifier: MIT # -import os -import re -import time -import logging import bb.tinfoil from oeqa.selftest.case import OESelftestTestCase -from oeqa.utils.commands import runCmd, get_test_layer +from oeqa.utils.commands import get_test_layer def setUpModule(): @@ -40,7 +38,7 @@ class RecipeUtilsTests(OESelftestTestCase): SUMMARY = "Python framework to process interdependent tasks in a pool of workers" HOMEPAGE = "http://github.com/gitpython-developers/async" SECTION = "devel/python" --LICENSE = "BSD" +-LICENSE = "BSD-3-Clause" +LICENSE = "something" LIC_FILES_CHKSUM = "file://PKG-INFO;beginline=8;endline=8;md5=88df8e78b9edfd744953862179f2d14e" @@ -52,7 +50,7 @@ class RecipeUtilsTests(OESelftestTestCase): +SRC_URI[md5sum] = "aaaaaa" SRC_URI[sha256sum] = "ac6894d876e45878faae493b0cf61d0e28ec417334448ac0a6ea2229d8343051" - RDEPENDS_${PN} += "${PYTHON_PN}-threading" + RDEPENDS:${PN} += "python3-threading" """ patchlines = [] for f in patches: @@ -74,13 +72,13 @@ class RecipeUtilsTests(OESelftestTestCase): expected_patch = """ --- a/recipes-test/recipeutils/recipeutils-test_1.2.bb +++ b/recipes-test/recipeutils/recipeutils-test_1.2.bb -@@ -8,6 +8,4 @@ +@@ -11,6 +11,4 @@ BBCLASSEXTEND = "native nativesdk" -SRC_URI += "file://somefile" - - SRC_URI_append = " file://anotherfile" + SRC_URI:append = " file://anotherfile" """ patchlines = [] for f in patches: @@ -99,13 +97,13 @@ class RecipeUtilsTests(OESelftestTestCase): expected_patch = """ --- a/recipes-test/recipeutils/recipeutils-test_1.2.bb +++ b/recipes-test/recipeutils/recipeutils-test_1.2.bb -@@ -8,6 +8,3 @@ +@@ -11,6 +11,3 @@ BBCLASSEXTEND = "native nativesdk" -SRC_URI += "file://somefile" - --SRC_URI_append = " file://anotherfile" +-SRC_URI:append = " file://anotherfile" """ patchlines = [] for f in patches: diff --git a/meta/lib/oeqa/selftest/cases/reproducible.py b/meta/lib/oeqa/selftest/cases/reproducible.py index a9110565a9..3d3f30eebc 100644 --- a/meta/lib/oeqa/selftest/cases/reproducible.py +++ b/meta/lib/oeqa/selftest/cases/reproducible.py @@ -1,7 +1,7 @@ # # SPDX-License-Identifier: MIT # -# Copyright 2019 by Garmin Ltd. or its subsidiaries +# Copyright 2019-2020 by Garmin Ltd. or its subsidiaries from oeqa.selftest.case import OESelftestTestCase from oeqa.utils.commands import runCmd, bitbake, get_bb_var, get_bb_vars @@ -9,12 +9,21 @@ import bb.utils import functools import multiprocessing import textwrap -import json -import unittest import tempfile import shutil import stat import os +import datetime + +exclude_packages = [ + ] + +def is_excluded(package): + package_name = os.path.basename(package) + for i in exclude_packages: + if package_name.startswith(i): + return i + return None MISSING = 'MISSING' DIFFERENT = 'DIFFERENT' @@ -34,18 +43,26 @@ class CompareResult(object): return (self.status, self.test) < (other.status, other.test) class PackageCompareResults(object): - def __init__(self): + def __init__(self, exclusions): self.total = [] self.missing = [] self.different = [] + self.different_excluded = [] self.same = [] + self.active_exclusions = set() + exclude_packages.extend((exclusions or "").split()) def add_result(self, r): self.total.append(r) if r.status == MISSING: self.missing.append(r) elif r.status == DIFFERENT: - self.different.append(r) + exclusion = is_excluded(r.reference) + if exclusion: + self.different_excluded.append(r) + self.active_exclusions.add(exclusion) + else: + self.different.append(r) else: self.same.append(r) @@ -53,10 +70,14 @@ class PackageCompareResults(object): self.total.sort() self.missing.sort() self.different.sort() + self.different_excluded.sort() self.same.sort() def __str__(self): - return 'same=%i different=%i missing=%i total=%i' % (len(self.same), len(self.different), len(self.missing), len(self.total)) + return 'same=%i different=%i different_excluded=%i missing=%i total=%i\nunused_exclusions=%s' % (len(self.same), len(self.different), len(self.different_excluded), len(self.missing), len(self.total), self.unused_exclusions()) + + def unused_exclusions(self): + return sorted(set(exclude_packages) - self.active_exclusions) def compare_file(reference, test, diffutils_sysroot): result = CompareResult() @@ -67,7 +88,7 @@ def compare_file(reference, test, diffutils_sysroot): result.status = MISSING return result - r = runCmd(['cmp', '--quiet', reference, test], native_sysroot=diffutils_sysroot, ignore_status=True) + r = runCmd(['cmp', '--quiet', reference, test], native_sysroot=diffutils_sysroot, ignore_status=True, sync=False) if r.status: result.status = DIFFERENT @@ -76,27 +97,94 @@ def compare_file(reference, test, diffutils_sysroot): result.status = SAME return result +def run_diffoscope(a_dir, b_dir, html_dir, max_report_size=0, **kwargs): + return runCmd(['diffoscope', '--no-default-limits', '--max-report-size', str(max_report_size), + '--exclude-directory-metadata', 'yes', '--html-dir', html_dir, a_dir, b_dir], + **kwargs) + +class DiffoscopeTests(OESelftestTestCase): + diffoscope_test_files = os.path.join(os.path.dirname(os.path.abspath(__file__)), "diffoscope") + + def test_diffoscope(self): + bitbake("diffoscope-native -c addto_recipe_sysroot") + diffoscope_sysroot = get_bb_var("RECIPE_SYSROOT_NATIVE", "diffoscope-native") + + # Check that diffoscope doesn't return an error when the files compare + # the same (a general check that diffoscope is working) + with tempfile.TemporaryDirectory() as tmpdir: + run_diffoscope('A', 'A', tmpdir, + native_sysroot=diffoscope_sysroot, cwd=self.diffoscope_test_files) + + # Check that diffoscope generates an index.html file when the files are + # different + with tempfile.TemporaryDirectory() as tmpdir: + r = run_diffoscope('A', 'B', tmpdir, + native_sysroot=diffoscope_sysroot, ignore_status=True, cwd=self.diffoscope_test_files) + + self.assertNotEqual(r.status, 0, msg="diffoscope was successful when an error was expected") + self.assertTrue(os.path.exists(os.path.join(tmpdir, 'index.html')), "HTML index not found!") + class ReproducibleTests(OESelftestTestCase): - package_classes = ['deb', 'ipk'] - images = ['core-image-minimal'] + # Test the reproducibility of whatever is built between sstate_targets and targets + + package_classes = ['deb', 'ipk', 'rpm'] + + # Maximum report size, in bytes + max_report_size = 250 * 1024 * 1024 + + # targets are the things we want to test the reproducibility of + # Have to add the virtual targets manually for now as builds may or may not include them as they're exclude from world + targets = ['core-image-minimal', 'core-image-sato', 'core-image-full-cmdline', 'core-image-weston', 'world', 'virtual/librpc', 'virtual/libsdl2', 'virtual/crypt'] + + # sstate targets are things to pull from sstate to potentially cut build/debugging time + sstate_targets = [] + save_results = False + if 'OEQA_DEBUGGING_SAVED_OUTPUT' in os.environ: + save_results = os.environ['OEQA_DEBUGGING_SAVED_OUTPUT'] + + # This variable controls if one of the test builds is allowed to pull from + # an sstate cache/mirror. The other build is always done clean as a point of + # comparison. + # If you know that your sstate archives are reproducible, enabling this + # will test that and also make the test run faster. If your sstate is not + # reproducible, disable this in your derived test class + build_from_sstate = True def setUpLocal(self): super().setUpLocal() - needed_vars = ['TOPDIR', 'TARGET_PREFIX', 'BB_NUMBER_THREADS'] + needed_vars = [ + 'TOPDIR', + 'TARGET_PREFIX', + 'BB_NUMBER_THREADS', + 'BB_HASHSERVE', + 'OEQA_REPRODUCIBLE_TEST_PACKAGE', + 'OEQA_REPRODUCIBLE_TEST_TARGET', + 'OEQA_REPRODUCIBLE_TEST_SSTATE_TARGETS', + 'OEQA_REPRODUCIBLE_EXCLUDED_PACKAGES', + ] bb_vars = get_bb_vars(needed_vars) for v in needed_vars: setattr(self, v.lower(), bb_vars[v]) - self.extrasresults = {} - self.extrasresults.setdefault('reproducible.rawlogs', {})['log'] = '' - self.extrasresults.setdefault('reproducible', {}).setdefault('files', {}) + if bb_vars['OEQA_REPRODUCIBLE_TEST_PACKAGE']: + self.package_classes = bb_vars['OEQA_REPRODUCIBLE_TEST_PACKAGE'].split() + + if bb_vars['OEQA_REPRODUCIBLE_TEST_TARGET']: + self.targets = bb_vars['OEQA_REPRODUCIBLE_TEST_TARGET'].split() + + if bb_vars['OEQA_REPRODUCIBLE_TEST_SSTATE_TARGETS']: + self.sstate_targets = bb_vars['OEQA_REPRODUCIBLE_TEST_SSTATE_TARGETS'].split() + + self.extraresults = {} + self.extraresults.setdefault('reproducible.rawlogs', {})['log'] = '' + self.extraresults.setdefault('reproducible', {}).setdefault('files', {}) def append_to_log(self, msg): - self.extrasresults['reproducible.rawlogs']['log'] += msg + self.extraresults['reproducible.rawlogs']['log'] += msg def compare_packages(self, reference_dir, test_dir, diffutils_sysroot): - result = PackageCompareResults() + result = PackageCompareResults(self.oeqa_reproducible_excluded_packages) old_cwd = os.getcwd() try: @@ -120,60 +208,85 @@ class ReproducibleTests(OESelftestTestCase): return result def write_package_list(self, package_class, name, packages): - self.extrasresults['reproducible']['files'].setdefault(package_class, {})[name] = [ + self.extraresults['reproducible']['files'].setdefault(package_class, {})[name] = [ {'reference': p.reference, 'test': p.test} for p in packages] def copy_file(self, source, dest): bb.utils.mkdirhier(os.path.dirname(dest)) shutil.copyfile(source, dest) - def test_reproducible_builds(self): + def do_test_build(self, name, use_sstate): capture_vars = ['DEPLOY_DIR_' + c.upper() for c in self.package_classes] - if self.save_results: - save_dir = tempfile.mkdtemp(prefix='oe-reproducible-') - os.chmod(save_dir, stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH) - self.logger.info('Non-reproducible packages will be copied to %s', save_dir) + tmpdir = os.path.join(self.topdir, name, 'tmp') + if os.path.exists(tmpdir): + bb.utils.remove(tmpdir, recurse=True) + + config = textwrap.dedent('''\ + PACKAGE_CLASSES = "{package_classes}" + TMPDIR = "{tmpdir}" + LICENSE_FLAGS_ACCEPTED = "commercial" + DISTRO_FEATURES:append = ' pam' + USERADDEXTENSION = "useradd-staticids" + USERADD_ERROR_DYNAMIC = "skip" + USERADD_UID_TABLES += "files/static-passwd" + USERADD_GID_TABLES += "files/static-group" + ''').format(package_classes=' '.join('package_%s' % c for c in self.package_classes), + tmpdir=tmpdir) + + if not use_sstate: + if self.sstate_targets: + self.logger.info("Building prebuild for %s (sstate allowed)..." % (name)) + self.write_config(config) + bitbake(' '.join(self.sstate_targets)) + + # This config fragment will disable using shared and the sstate + # mirror, forcing a complete build from scratch + config += textwrap.dedent('''\ + SSTATE_DIR = "${TMPDIR}/sstate" + SSTATE_MIRRORS = "file://.*/.*-native.* http://sstate.yoctoproject.org/all/PATH;downloadfilename=PATH file://.*/.*-cross.* http://sstate.yoctoproject.org/all/PATH;downloadfilename=PATH" + ''') + + self.logger.info("Building %s (sstate%s allowed)..." % (name, '' if use_sstate else ' NOT')) + self.write_config(config) + d = get_bb_vars(capture_vars) + # targets used to be called images + bitbake(' '.join(getattr(self, 'images', self.targets))) + return d + + def test_reproducible_builds(self): + def strip_topdir(s): + if s.startswith(self.topdir): + return s[len(self.topdir):] + return s # Build native utilities self.write_config('') - bitbake("diffutils-native -c addto_recipe_sysroot") + bitbake("diffoscope-native diffutils-native jquery-native -c addto_recipe_sysroot") diffutils_sysroot = get_bb_var("RECIPE_SYSROOT_NATIVE", "diffutils-native") + diffoscope_sysroot = get_bb_var("RECIPE_SYSROOT_NATIVE", "diffoscope-native") + jquery_sysroot = get_bb_var("RECIPE_SYSROOT_NATIVE", "jquery-native") + + if self.save_results: + os.makedirs(self.save_results, exist_ok=True) + datestr = datetime.datetime.now().strftime('%Y%m%d') + save_dir = tempfile.mkdtemp(prefix='oe-reproducible-%s-' % datestr, dir=self.save_results) + os.chmod(save_dir, stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH) + self.logger.info('Non-reproducible packages will be copied to %s', save_dir) + + # The below bug shows that a few reproducible issues are depends on build dir path length. + # https://bugzilla.yoctoproject.org/show_bug.cgi?id=15554 + # So, the reproducibleA & reproducibleB directories are changed to reproducibleA & reproducibleB-extended to have different size. - # Reproducible builds should not pull from sstate or mirrors, but - # sharing DL_DIR is fine - common_config = textwrap.dedent('''\ - INHERIT += "reproducible_build" - PACKAGE_CLASSES = "%s" - SSTATE_DIR = "${TMPDIR}/sstate" - ''') % (' '.join('package_%s' % c for c in self.package_classes)) - - # Perform a build. - reproducibleA_tmp = os.path.join(self.topdir, 'reproducibleA', 'tmp') - if os.path.exists(reproducibleA_tmp): - bb.utils.remove(reproducibleA_tmp, recurse=True) - - self.write_config((textwrap.dedent('''\ - TMPDIR = "%s" - ''') % reproducibleA_tmp) + common_config) - vars_A = get_bb_vars(capture_vars) - bitbake(' '.join(self.images)) - - # Perform another build. - reproducibleB_tmp = os.path.join(self.topdir, 'reproducibleB', 'tmp') - if os.path.exists(reproducibleB_tmp): - bb.utils.remove(reproducibleB_tmp, recurse=True) - - self.write_config((textwrap.dedent('''\ - SSTATE_MIRROR = "" - TMPDIR = "%s" - ''') % reproducibleB_tmp) + common_config) - vars_B = get_bb_vars(capture_vars) - bitbake(' '.join(self.images)) + vars_A = self.do_test_build('reproducibleA', self.build_from_sstate) + + vars_B = self.do_test_build('reproducibleB-extended', False) # NOTE: The temp directories from the reproducible build are purposely # kept after the build so it can be diffed for debugging. + fails = [] + for c in self.package_classes: with self.subTest(package_class=c): package_class = 'package_' + c @@ -181,6 +294,7 @@ class ReproducibleTests(OESelftestTestCase): deploy_A = vars_A['DEPLOY_DIR_' + c.upper()] deploy_B = vars_B['DEPLOY_DIR_' + c.upper()] + self.logger.info('Checking %s packages for differences...' % c) result = self.compare_packages(deploy_A, deploy_B, diffutils_sysroot) self.logger.info('Reproducibility summary for %s: %s' % (c, result)) @@ -189,14 +303,41 @@ class ReproducibleTests(OESelftestTestCase): self.write_package_list(package_class, 'missing', result.missing) self.write_package_list(package_class, 'different', result.different) + self.write_package_list(package_class, 'different_excluded', result.different_excluded) self.write_package_list(package_class, 'same', result.same) if self.save_results: for d in result.different: - self.copy_file(d.reference, '/'.join([save_dir, d.reference])) - self.copy_file(d.test, '/'.join([save_dir, d.test])) + self.copy_file(d.reference, '/'.join([save_dir, 'packages', strip_topdir(d.reference)])) + self.copy_file(d.test, '/'.join([save_dir, 'packages', strip_topdir(d.test)])) + + for d in result.different_excluded: + self.copy_file(d.reference, '/'.join([save_dir, 'packages-excluded', strip_topdir(d.reference)])) + self.copy_file(d.test, '/'.join([save_dir, 'packages-excluded', strip_topdir(d.test)])) + + if result.different: + fails.append("The following %s packages are different and not in exclusion list:\n%s" % + (c, '\n'.join(r.test for r in (result.different)))) + + if result.missing and len(self.sstate_targets) == 0: + fails.append("The following %s packages are missing and not in exclusion list:\n%s" % + (c, '\n'.join(r.test for r in (result.missing)))) + + # Clean up empty directories + if self.save_results: + if not os.listdir(save_dir): + os.rmdir(save_dir) + else: + self.logger.info('Running diffoscope') + package_dir = os.path.join(save_dir, 'packages') + package_html_dir = os.path.join(package_dir, 'diff-html') + + # Copy jquery to improve the diffoscope output usability + self.copy_file(os.path.join(jquery_sysroot, 'usr/share/javascript/jquery/jquery.min.js'), os.path.join(package_html_dir, 'jquery.js')) + + run_diffoscope('reproducibleA', 'reproducibleB-extended', package_html_dir, max_report_size=self.max_report_size, + native_sysroot=diffoscope_sysroot, ignore_status=True, cwd=package_dir) - if result.missing or result.different: - self.fail("The following %s packages are missing or different: %s" % - (c, ' '.join(r.test for r in (result.missing + result.different)))) + if fails: + self.fail('\n'.join(fails)) diff --git a/meta/lib/oeqa/selftest/cases/resulttooltests.py b/meta/lib/oeqa/selftest/cases/resulttooltests.py index dac5c46801..c3303f3fbb 100644 --- a/meta/lib/oeqa/selftest/cases/resulttooltests.py +++ b/meta/lib/oeqa/selftest/cases/resulttooltests.py @@ -1,4 +1,6 @@ # +# Copyright OpenEmbedded Contributors +# # SPDX-License-Identifier: MIT # @@ -69,7 +71,7 @@ class ResultToolTests(OESelftestTestCase): self.assertTrue('target_result1' in results['runtime/mydistro/qemux86/image'], msg="Pair not correct:%s" % results) self.assertTrue('target_result3' in results['runtime/mydistro/qemux86-64/image'], msg="Pair not correct:%s" % results) - def test_regrresion_can_get_regression_result(self): + def test_regression_can_get_regression_result(self): base_result_data = {'result': {'test1': {'status': 'PASSED'}, 'test2': {'status': 'PASSED'}, 'test3': {'status': 'FAILED'}, @@ -96,3 +98,278 @@ class ResultToolTests(OESelftestTestCase): resultutils.append_resultsdata(results, ResultToolTests.target_results_data, configmap=resultutils.flatten_map) self.assertEqual(len(results[''].keys()), 5, msg="Flattened results not correct %s" % str(results)) + def test_results_without_metadata_can_be_compared(self): + base_configuration = {"configuration": { + "TEST_TYPE": "oeselftest", + "TESTSERIES": "series1", + "IMAGE_BASENAME": "image", + "IMAGE_PKGTYPE": "ipk", + "DISTRO": "mydistro", + "MACHINE": "qemux86", + "STARTTIME": 1672527600 + }, "result": {}} + target_configuration = {"configuration": { + "TEST_TYPE": "oeselftest", + "TESTSERIES": "series1", + "IMAGE_BASENAME": "image", + "IMAGE_PKGTYPE": "ipk", + "DISTRO": "mydistro", + "MACHINE": "qemux86", + "STARTTIME": 1672527600 + }, "result": {}} + self.assertTrue(regression.can_be_compared(self.logger, base_configuration, target_configuration), + msg="incorrect metadata filtering, tests without metadata should be compared") + + def test_target_result_with_missing_metadata_can_not_be_compared(self): + base_configuration = {"configuration": { + "TEST_TYPE": "oeselftest", + "TESTSERIES": "series1", + "IMAGE_BASENAME": "image", + "IMAGE_PKGTYPE": "ipk", + "DISTRO": "mydistro", + "MACHINE": "qemux86", + "OESELFTEST_METADATA": { + "run_all_tests": True, + "run_tests": None, + "skips": None, + "machine": None, + "select_tags": ["toolchain-user", "toolchain-system"], + "exclude_tags": None + }}, "result": {}} + target_configuration = {"configuration": {"TEST_TYPE": "oeselftest", + "TESTSERIES": "series1", + "IMAGE_BASENAME": "image", + "IMAGE_PKGTYPE": "ipk", + "DISTRO": "mydistro", + "MACHINE": "qemux86", + "STARTTIME": 1672527600 + }, "result": {}} + self.assertFalse(regression.can_be_compared(self.logger, base_configuration, target_configuration), + msg="incorrect metadata filtering, tests should not be compared") + + def test_results_with_matching_metadata_can_be_compared(self): + base_configuration = {"configuration": { + "TEST_TYPE": "oeselftest", + "TESTSERIES": "series1", + "IMAGE_BASENAME": "image", + "IMAGE_PKGTYPE": "ipk", + "DISTRO": "mydistro", + "MACHINE": "qemux86", + "STARTTIME": 1672527600, + "OESELFTEST_METADATA": {"run_all_tests": True, + "run_tests": None, + "skips": None, + "machine": None, + "select_tags": ["toolchain-user", "toolchain-system"], + "exclude_tags": None} + }, "result": {}} + target_configuration = {"configuration": { + "TEST_TYPE": "oeselftest", + "TESTSERIES": "series1", + "IMAGE_BASENAME": "image", + "IMAGE_PKGTYPE": "ipk", + "DISTRO": "mydistro", + "MACHINE": "qemux86", + "STARTTIME": 1672527600, + "OESELFTEST_METADATA": {"run_all_tests": True, + "run_tests": None, + "skips": None, + "machine": None, + "select_tags": ["toolchain-user", "toolchain-system"], + "exclude_tags": None} + }, "result": {}} + self.assertTrue(regression.can_be_compared(self.logger, base_configuration, target_configuration), + msg="incorrect metadata filtering, tests with matching metadata should be compared") + + def test_results_with_mismatching_metadata_can_not_be_compared(self): + base_configuration = {"configuration": { + "TEST_TYPE": "oeselftest", + "TESTSERIES": "series1", + "IMAGE_BASENAME": "image", + "IMAGE_PKGTYPE": "ipk", + "DISTRO": "mydistro", + "MACHINE": "qemux86", + "STARTTIME": 1672527600, + "OESELFTEST_METADATA": {"run_all_tests": True, + "run_tests": None, + "skips": None, + "machine": None, + "select_tags": ["toolchain-user", "toolchain-system"], + "exclude_tags": None} + }, "result": {}} + target_configuration = {"configuration": { + "TEST_TYPE": "oeselftest", + "TESTSERIES": "series1", + "IMAGE_BASENAME": "image", + "IMAGE_PKGTYPE": "ipk", + "DISTRO": "mydistro", + "MACHINE": "qemux86", + "STARTTIME": 1672527600, + "OESELFTEST_METADATA": {"run_all_tests": True, + "run_tests": None, + "skips": None, + "machine": None, + "select_tags": ["machine"], + "exclude_tags": None} + }, "result": {}} + self.assertFalse(regression.can_be_compared(self.logger, base_configuration, target_configuration), + msg="incorrect metadata filtering, tests with mismatching metadata should not be compared") + + def test_metadata_matching_is_only_checked_for_relevant_test_type(self): + base_configuration = {"configuration": {"TEST_TYPE": "runtime", + "TESTSERIES": "series1", + "IMAGE_BASENAME": "image", + "IMAGE_PKGTYPE": "ipk", + "DISTRO": "mydistro", + "MACHINE": "qemux86", + "STARTTIME": 1672527600, + "OESELFTEST_METADATA": {"run_all_tests": True, + "run_tests": None, + "skips": None, + "machine": None, + "select_tags": ["toolchain-user", "toolchain-system"], + "exclude_tags": None}}, "result": {}} + target_configuration = {"configuration": {"TEST_TYPE": "runtime", + "TESTSERIES": "series1", + "IMAGE_BASENAME": "image", + "IMAGE_PKGTYPE": "ipk", + "DISTRO": "mydistro", + "MACHINE": "qemux86", + "STARTTIME": 1672527600, + "OESELFTEST_METADATA": {"run_all_tests": True, + "run_tests": None, + "skips": None, + "machine": None, + "select_tags": ["machine"], + "exclude_tags": None}}, "result": {}} + self.assertTrue(regression.can_be_compared(self.logger, base_configuration, target_configuration), + msg="incorrect metadata filtering, %s tests should be compared" % base_configuration['configuration']['TEST_TYPE']) + + def test_machine_matches(self): + base_configuration = {"configuration": { + "TEST_TYPE": "runtime", + "MACHINE": "qemux86"}, "result": {}} + target_configuration = {"configuration": { + "TEST_TYPE": "runtime", + "MACHINE": "qemux86" + }, "result": {}} + self.assertTrue(regression.can_be_compared(self.logger, base_configuration, target_configuration), + msg="incorrect machine filtering, identical machine tests should be compared") + + def test_machine_mismatches(self): + base_configuration = {"configuration": { + "TEST_TYPE": "runtime", + "MACHINE": "qemux86" + }, "result": {}} + target_configuration = {"configuration": { + "TEST_TYPE": "runtime", + "MACHINE": "qemux86_64" + }, "result": {}} + self.assertFalse(regression.can_be_compared(self.logger, base_configuration, target_configuration), + msg="incorrect machine filtering, mismatching machine tests should not be compared") + + def test_can_not_compare_non_ltp_tests(self): + base_configuration = {"configuration": { + "TEST_TYPE": "runtime", + "MACHINE": "qemux86" + }, "result": { + "ltpresult_foo": { + "status": "PASSED" + }}} + target_configuration = {"configuration": { + "TEST_TYPE": "runtime", + "MACHINE": "qemux86_64" + }, "result": { + "bar": { + "status": "PASSED" + }}} + self.assertFalse(regression.can_be_compared(self.logger, base_configuration, target_configuration), + msg="incorrect ltpresult filtering, mismatching ltpresult content should not be compared") + + def test_can_compare_ltp_tests(self): + base_configuration = {"configuration": { + "TEST_TYPE": "runtime", + "MACHINE": "qemux86" + }, "result": { + "ltpresult_foo": { + "status": "PASSED" + }}} + target_configuration = {"configuration": { + "TEST_TYPE": "runtime", + "MACHINE": "qemux86" + }, "result": { + "ltpresult_foo": { + "status": "PASSED" + }}} + self.assertTrue(regression.can_be_compared(self.logger, base_configuration, target_configuration), + msg="incorrect ltpresult filtering, matching ltpresult content should be compared") + + def test_can_match_non_static_ptest_names(self): + base_configuration = {"a": { + "conf_X": { + "configuration": { + "TEST_TYPE": "runtime", + "MACHINE": "qemux86" + }, "result": { + "ptestresult.lttng-tools.foo_-_bar_-_moo": { + "status": "PASSED" + }, + "ptestresult.babeltrace.bar_-_moo_-_foo": { + "status": "PASSED" + }, + "ptestresult.babeltrace2.moo_-_foo_-_bar": { + "status": "PASSED" + }, + "ptestresult.curl.test_0000__foo_out_of_bar": { + "status": "PASSED" + }, + "ptestresult.dbus.test_0000__foo_out_of_bar,_remaining:_00:02,_took_0.032s,_duration:_03:32_": { + "status": "PASSED" + }, + "ptestresult.binutils-ld.in testcase /foo/build-st-bar/moo/ctf.exp": { + "status": "PASSED" + }, + "ptestresult.gcc-libstdc++-v3.Couldn't create remote directory /tmp/runtest.30975 on target": { + "status": "PASSED" + }, + "ptestresult.gcc-libgomp.Couldn't create remote directory /tmp/runtest.3657621 on": { + "status": "PASSED" + } + }}}} + target_configuration = {"a": { + "conf_Y": { + "configuration": { + "TEST_TYPE": "runtime", + "MACHINE": "qemux86" + }, "result": { + "ptestresult.lttng-tools.foo_-_yyy_-_zzz": { + "status": "PASSED" + }, + "ptestresult.babeltrace.bar_-_zzz_-_xxx": { + "status": "PASSED" + }, + "ptestresult.babeltrace2.moo_-_xxx_-_yyy": { + "status": "PASSED" + }, + "ptestresult.curl.test_0000__xxx_out_of_yyy": { + "status": "PASSED" + }, + "ptestresult.dbus.test_0000__yyy_out_of_zzz,_remaining:_00:03,_took_0.034s,_duration:_03:30_": { + "status": "PASSED" + }, + "ptestresult.binutils-ld.in testcase /xxx/build-st-yyy/zzz/ctf.exp": { + "status": "PASSED" + }, + "ptestresult.gcc-libstdc++-v3.Couldn't create remote directory /tmp/runtest.45678 on target": { + "status": "PASSED" + }, + "ptestresult.gcc-libgomp.Couldn't create remote directory /tmp/runtest.3657621 on": { + "status": "PASSED" + } + }}}} + regression.fixup_ptest_names(base_configuration, self.logger) + regression.fixup_ptest_names(target_configuration, self.logger) + result, resultstring = regression.compare_result( + self.logger, "A", "B", base_configuration["a"]["conf_X"], target_configuration["a"]["conf_Y"]) + self.assertDictEqual( + result, {}, msg=f"ptests should be compared: {resultstring}") diff --git a/meta/lib/oeqa/selftest/cases/retain.py b/meta/lib/oeqa/selftest/cases/retain.py new file mode 100644 index 0000000000..892be45857 --- /dev/null +++ b/meta/lib/oeqa/selftest/cases/retain.py @@ -0,0 +1,241 @@ +# Tests for retain.bbclass +# +# Copyright OpenEmbedded Contributors +# +# SPDX-License-Identifier: MIT +# + +import os +import glob +import fnmatch +import oe.path +import shutil +import tarfile +from oeqa.utils.commands import bitbake, get_bb_vars +from oeqa.selftest.case import OESelftestTestCase + +class Retain(OESelftestTestCase): + + def test_retain_always(self): + """ + Summary: Test retain class with RETAIN_DIRS_ALWAYS + Expected: Archive written to RETAIN_OUTDIR when build of test recipe completes + Product: oe-core + Author: Paul Eggleton <paul.eggleton@microsoft.com> + """ + + test_recipe = 'quilt-native' + + features = 'INHERIT += "retain"\n' + features += 'RETAIN_DIRS_ALWAYS = "${T}"\n' + self.write_config(features) + + bitbake('-c clean %s' % test_recipe) + + bb_vars = get_bb_vars(['RETAIN_OUTDIR', 'TMPDIR']) + retain_outdir = bb_vars['RETAIN_OUTDIR'] or '' + tmpdir = bb_vars['TMPDIR'] + if len(retain_outdir) < 5: + self.fail('RETAIN_OUTDIR value "%s" is invalid' % retain_outdir) + if not oe.path.is_path_parent(tmpdir, retain_outdir): + self.fail('RETAIN_OUTDIR (%s) is not underneath TMPDIR (%s)' % (retain_outdir, tmpdir)) + try: + shutil.rmtree(retain_outdir) + except FileNotFoundError: + pass + + bitbake(test_recipe) + if not glob.glob(os.path.join(retain_outdir, '%s_temp_*.tar.gz' % test_recipe)): + self.fail('No output archive for %s created' % test_recipe) + + + def test_retain_failure(self): + """ + Summary: Test retain class default behaviour + Expected: Archive written to RETAIN_OUTDIR only when build of test + recipe fails, and archive contents are as expected + Product: oe-core + Author: Paul Eggleton <paul.eggleton@microsoft.com> + """ + + test_recipe_fail = 'error' + + features = 'INHERIT += "retain"\n' + self.write_config(features) + + bb_vars = get_bb_vars(['RETAIN_OUTDIR', 'TMPDIR', 'RETAIN_DIRS_ALWAYS', 'RETAIN_DIRS_GLOBAL_ALWAYS']) + if bb_vars['RETAIN_DIRS_ALWAYS']: + self.fail('RETAIN_DIRS_ALWAYS is set, this interferes with the test') + if bb_vars['RETAIN_DIRS_GLOBAL_ALWAYS']: + self.fail('RETAIN_DIRS_GLOBAL_ALWAYS is set, this interferes with the test') + retain_outdir = bb_vars['RETAIN_OUTDIR'] or '' + tmpdir = bb_vars['TMPDIR'] + if len(retain_outdir) < 5: + self.fail('RETAIN_OUTDIR value "%s" is invalid' % retain_outdir) + if not oe.path.is_path_parent(tmpdir, retain_outdir): + self.fail('RETAIN_OUTDIR (%s) is not underneath TMPDIR (%s)' % (retain_outdir, tmpdir)) + + try: + shutil.rmtree(retain_outdir) + except FileNotFoundError: + pass + + bitbake('-c clean %s' % test_recipe_fail) + + if os.path.exists(retain_outdir): + retain_dirlist = os.listdir(retain_outdir) + if retain_dirlist: + self.fail('RETAIN_OUTDIR should be empty without failure, contents:\n%s' % '\n'.join(retain_dirlist)) + + result = bitbake('-c compile %s' % test_recipe_fail, ignore_status=True) + if result.status == 0: + self.fail('Build of %s did not fail as expected' % test_recipe_fail) + + archives = glob.glob(os.path.join(retain_outdir, '%s_*.tar.gz' % test_recipe_fail)) + if not archives: + self.fail('No output archive for %s created' % test_recipe_fail) + if len(archives) > 1: + self.fail('More than one archive for %s created' % test_recipe_fail) + for archive in archives: + found = False + archive_prefix = os.path.basename(archive).split('.tar')[0] + expected_prefix_start = '%s_workdir' % test_recipe_fail + if not archive_prefix.startswith(expected_prefix_start): + self.fail('Archive %s name does not start with expected prefix "%s"' % (os.path.basename(archive), expected_prefix_start)) + with tarfile.open(archive) as tf: + for ti in tf: + if not fnmatch.fnmatch(ti.name, '%s/*' % archive_prefix): + self.fail('File without tarball-named subdirectory within tarball %s: %s' % (os.path.basename(archive), ti.name)) + if ti.name.endswith('/temp/log.do_compile'): + found = True + if not found: + self.fail('Did not find log.do_compile in output archive %s' % os.path.basename(archive)) + + + def test_retain_global(self): + """ + Summary: Test retain class RETAIN_DIRS_GLOBAL_* behaviour + Expected: Ensure RETAIN_DIRS_GLOBAL_ALWAYS always causes an + archive to be created, and RETAIN_DIRS_GLOBAL_FAILURE + only causes an archive to be created on failure. + Also test archive naming (with : character) as an + added bonus. + Product: oe-core + Author: Paul Eggleton <paul.eggleton@microsoft.com> + """ + + test_recipe = 'quilt-native' + test_recipe_fail = 'error' + + features = 'INHERIT += "retain"\n' + features += 'RETAIN_DIRS_GLOBAL_ALWAYS = "${LOG_DIR};prefix=buildlogs"\n' + features += 'RETAIN_DIRS_GLOBAL_FAILURE = "${STAMPS_DIR}"\n' + self.write_config(features) + + bitbake('-c clean %s' % test_recipe) + + bb_vars = get_bb_vars(['RETAIN_OUTDIR', 'TMPDIR', 'STAMPS_DIR']) + retain_outdir = bb_vars['RETAIN_OUTDIR'] or '' + tmpdir = bb_vars['TMPDIR'] + if len(retain_outdir) < 5: + self.fail('RETAIN_OUTDIR value "%s" is invalid' % retain_outdir) + if not oe.path.is_path_parent(tmpdir, retain_outdir): + self.fail('RETAIN_OUTDIR (%s) is not underneath TMPDIR (%s)' % (retain_outdir, tmpdir)) + try: + shutil.rmtree(retain_outdir) + except FileNotFoundError: + pass + + # Test success case + bitbake(test_recipe) + if not glob.glob(os.path.join(retain_outdir, 'buildlogs_*.tar.gz')): + self.fail('No output archive for LOG_DIR created') + stamps_dir = bb_vars['STAMPS_DIR'] + if glob.glob(os.path.join(retain_outdir, '%s_*.tar.gz' % os.path.basename(stamps_dir))): + self.fail('Output archive for STAMPS_DIR created when it should not have been') + + # Test failure case + result = bitbake('-c compile %s' % test_recipe_fail, ignore_status=True) + if result.status == 0: + self.fail('Build of %s did not fail as expected' % test_recipe_fail) + if not glob.glob(os.path.join(retain_outdir, '%s_*.tar.gz' % os.path.basename(stamps_dir))): + self.fail('Output archive for STAMPS_DIR not created') + if len(glob.glob(os.path.join(retain_outdir, 'buildlogs_*.tar.gz'))) != 2: + self.fail('Should be exactly two buildlogs archives in output dir') + + + def test_retain_misc(self): + """ + Summary: Test retain class with RETAIN_ENABLED and RETAIN_TARBALL_SUFFIX + Expected: Archive written to RETAIN_OUTDIR only when RETAIN_ENABLED is set + and archive contents are as expected. Also test archive naming + (with : character) as an added bonus. + Product: oe-core + Author: Paul Eggleton <paul.eggleton@microsoft.com> + """ + + test_recipe_fail = 'error' + + features = 'INHERIT += "retain"\n' + features += 'RETAIN_DIRS_ALWAYS = "${T}"\n' + features += 'RETAIN_ENABLED = "0"\n' + self.write_config(features) + + bb_vars = get_bb_vars(['RETAIN_OUTDIR', 'TMPDIR']) + retain_outdir = bb_vars['RETAIN_OUTDIR'] or '' + tmpdir = bb_vars['TMPDIR'] + if len(retain_outdir) < 5: + self.fail('RETAIN_OUTDIR value "%s" is invalid' % retain_outdir) + if not oe.path.is_path_parent(tmpdir, retain_outdir): + self.fail('RETAIN_OUTDIR (%s) is not underneath TMPDIR (%s)' % (retain_outdir, tmpdir)) + + try: + shutil.rmtree(retain_outdir) + except FileNotFoundError: + pass + + bitbake('-c clean %s' % test_recipe_fail) + result = bitbake('-c compile %s' % test_recipe_fail, ignore_status=True) + if result.status == 0: + self.fail('Build of %s did not fail as expected' % test_recipe_fail) + + if os.path.exists(retain_outdir) and os.listdir(retain_outdir): + self.fail('RETAIN_OUTDIR should be empty with RETAIN_ENABLED = "0"') + + features = 'INHERIT += "retain"\n' + features += 'RETAIN_DIRS_ALWAYS = "${T};prefix=recipelogs"\n' + features += 'RETAIN_TARBALL_SUFFIX = "${DATETIME}-testsuffix.tar.bz2"\n' + features += 'RETAIN_ENABLED = "1"\n' + self.write_config(features) + + result = bitbake('-c compile %s' % test_recipe_fail, ignore_status=True) + if result.status == 0: + self.fail('Build of %s did not fail as expected' % test_recipe_fail) + + archives = glob.glob(os.path.join(retain_outdir, '%s_*-testsuffix.tar.bz2' % test_recipe_fail)) + if not archives: + self.fail('No output archive for %s created' % test_recipe_fail) + if len(archives) != 2: + self.fail('Two archives for %s expected, but %d exist' % (test_recipe_fail, len(archives))) + recipelogs_found = False + workdir_found = False + for archive in archives: + contents_found = False + archive_prefix = os.path.basename(archive).split('.tar')[0] + if archive_prefix.startswith('%s_recipelogs' % test_recipe_fail): + recipelogs_found = True + if archive_prefix.startswith('%s_workdir' % test_recipe_fail): + workdir_found = True + with tarfile.open(archive, 'r:bz2') as tf: + for ti in tf: + if not fnmatch.fnmatch(ti.name, '%s/*' % (archive_prefix)): + self.fail('File without tarball-named subdirectory within tarball %s: %s' % (os.path.basename(archive), ti.name)) + if ti.name.endswith('/log.do_compile'): + contents_found = True + if not contents_found: + # Both archives should contain this file + self.fail('Did not find log.do_compile in output archive %s' % os.path.basename(archive)) + if not recipelogs_found: + self.fail('No archive with expected "recipelogs" prefix found') + if not workdir_found: + self.fail('No archive with expected "workdir" prefix found') diff --git a/meta/lib/oeqa/selftest/cases/rootfspostcommandstests.py b/meta/lib/oeqa/selftest/cases/rootfspostcommandstests.py new file mode 100644 index 0000000000..44e2c09a6f --- /dev/null +++ b/meta/lib/oeqa/selftest/cases/rootfspostcommandstests.py @@ -0,0 +1,97 @@ +# SPDX-FileCopyrightText: Huawei Inc. +# +# SPDX-License-Identifier: MIT + +import os +import oe +import unittest +from oeqa.selftest.case import OESelftestTestCase +from oeqa.utils.commands import bitbake, get_bb_vars + +class ShadowUtilsTidyFiles(OESelftestTestCase): + """ + Check if shadow image rootfs files are tidy. + + The tests are focused on testing the functionality provided by the + 'tidy_shadowutils_files' rootfs postprocess command (via + SORT_PASSWD_POSTPROCESS_COMMAND). + """ + + def sysconf_build(self): + """ + Verify if shadow tidy files tests are to be run and if yes, build a + test image and return its sysconf rootfs path. + """ + + test_image = "core-image-minimal" + + config = 'IMAGE_CLASSES += "extrausers"\n' + config += 'EXTRA_USERS_PARAMS = "groupadd -g 1000 oeqatester; "\n' + config += 'EXTRA_USERS_PARAMS += "useradd -p \'\' -u 1000 -N -g 1000 oeqatester; "\n' + self.write_config(config) + + vars = get_bb_vars(("IMAGE_ROOTFS", "SORT_PASSWD_POSTPROCESS_COMMAND", "sysconfdir"), + test_image) + passwd_postprocess_cmd = vars["SORT_PASSWD_POSTPROCESS_COMMAND"] + self.assertIsNotNone(passwd_postprocess_cmd) + if (passwd_postprocess_cmd.strip() != 'tidy_shadowutils_files;'): + raise unittest.SkipTest("Testcase skipped as 'tidy_shadowutils_files' " + "rootfs post process command is not the set SORT_PASSWD_POSTPROCESS_COMMAND.") + + rootfs = vars["IMAGE_ROOTFS"] + self.assertIsNotNone(rootfs) + sysconfdir = vars["sysconfdir"] + bitbake(test_image) + self.assertIsNotNone(sysconfdir) + + return oe.path.join(rootfs, sysconfdir) + + def test_shadowutils_backup_files(self): + """ + Test that the rootfs doesn't include any known shadow backup files. + """ + + backup_files = ( + 'group-', + 'gshadow-', + 'passwd-', + 'shadow-', + 'subgid-', + 'subuid-', + ) + + rootfs_sysconfdir = self.sysconf_build() + found = [] + for backup_file in backup_files: + backup_filepath = oe.path.join(rootfs_sysconfdir, backup_file) + if os.path.exists(backup_filepath): + found.append(backup_file) + if (found): + raise Exception('The following shadow backup files were found in ' + 'the rootfs: %s' % found) + + def test_shadowutils_sorted_files(self): + """ + Test that the 'passwd' and the 'group' shadow utils files are ordered + by ID. + """ + + files = ( + 'passwd', + 'group', + ) + + rootfs_sysconfdir = self.sysconf_build() + unsorted = [] + for file in files: + filepath = oe.path.join(rootfs_sysconfdir, file) + with open(filepath, 'rb') as f: + ids = [] + lines = f.readlines() + for line in lines: + entries = line.split(b':') + ids.append(int(entries[2])) + if (ids != sorted(ids)): + unsorted.append(file) + if (unsorted): + raise Exception("The following files were not sorted by ID as expected: %s" % unsorted) diff --git a/meta/lib/oeqa/selftest/cases/rpmtests.py b/meta/lib/oeqa/selftest/cases/rpmtests.py new file mode 100644 index 0000000000..902d7dca3d --- /dev/null +++ b/meta/lib/oeqa/selftest/cases/rpmtests.py @@ -0,0 +1,14 @@ +# +# Copyright OpenEmbedded Contributors +# +# SPDX-License-Identifier: MIT +# + +from oeqa.selftest.case import OESelftestTestCase +from oeqa.utils.commands import bitbake + +class BitbakeTests(OESelftestTestCase): + + def test_rpm_filenames(self): + test_recipe = "testrpm" + bitbake(test_recipe) diff --git a/meta/lib/oeqa/selftest/cases/runcmd.py b/meta/lib/oeqa/selftest/cases/runcmd.py index 3755764ee7..70047ca0ca 100644 --- a/meta/lib/oeqa/selftest/cases/runcmd.py +++ b/meta/lib/oeqa/selftest/cases/runcmd.py @@ -1,4 +1,6 @@ # +# Copyright OpenEmbedded Contributors +# # SPDX-License-Identifier: MIT # @@ -27,8 +29,8 @@ class RunCmdTests(OESelftestTestCase): # The delta is intentionally smaller than the timeout, to detect cases where # we incorrectly apply the timeout more than once. - TIMEOUT = 5 - DELTA = 3 + TIMEOUT = 10 + DELTA = 8 def test_result_okay(self): result = runCmd("true") @@ -56,20 +58,20 @@ class RunCmdTests(OESelftestTestCase): self.assertEqual(result.status, 0) def test_result_assertion(self): - self.assertRaisesRegexp(AssertionError, "Command 'echo .* false' returned non-zero exit status 1:\nfoobar", + self.assertRaisesRegex(AssertionError, "Command 'echo .* false' returned non-zero exit status 1:\nfoobar", runCmd, "echo foobar >&2; false", shell=True) def test_result_exception(self): - self.assertRaisesRegexp(CommandError, "Command 'echo .* false' returned non-zero exit status 1 with output: foobar", + self.assertRaisesRegex(CommandError, "Command 'echo .* false' returned non-zero exit status 1 with output: foobar", runCmd, "echo foobar >&2; false", shell=True, assert_error=False) def test_output(self): - result = runCmd("echo stdout; echo stderr >&2", shell=True) + result = runCmd("echo stdout; echo stderr >&2", shell=True, sync=False) self.assertEqual("stdout\nstderr", result.output) self.assertEqual("", result.error) def test_output_split(self): - result = runCmd("echo stdout; echo stderr >&2", shell=True, stderr=subprocess.PIPE) + result = runCmd("echo stdout; echo stderr >&2", shell=True, stderr=subprocess.PIPE, sync=False) self.assertEqual("stdout", result.output) self.assertEqual("stderr", result.error) @@ -77,45 +79,46 @@ class RunCmdTests(OESelftestTestCase): numthreads = threading.active_count() start = time.time() # Killing a hanging process only works when not using a shell?! - result = runCmd(['sleep', '60'], timeout=self.TIMEOUT, ignore_status=True) + result = runCmd(['sleep', '60'], timeout=self.TIMEOUT, ignore_status=True, sync=False) self.assertEqual(result.status, -signal.SIGTERM) end = time.time() self.assertLess(end - start, self.TIMEOUT + self.DELTA) - self.assertEqual(numthreads, threading.active_count()) + self.assertEqual(numthreads, threading.active_count(), msg="Thread counts were not equal before (%s) and after (%s), active threads: %s" % (numthreads, threading.active_count(), threading.enumerate())) def test_timeout_split(self): numthreads = threading.active_count() start = time.time() # Killing a hanging process only works when not using a shell?! - result = runCmd(['sleep', '60'], timeout=self.TIMEOUT, ignore_status=True, stderr=subprocess.PIPE) + result = runCmd(['sleep', '60'], timeout=self.TIMEOUT, ignore_status=True, stderr=subprocess.PIPE, sync=False) self.assertEqual(result.status, -signal.SIGTERM) end = time.time() self.assertLess(end - start, self.TIMEOUT + self.DELTA) - self.assertEqual(numthreads, threading.active_count()) + self.assertEqual(numthreads, threading.active_count(), msg="Thread counts were not equal before (%s) and after (%s), active threads: %s" % (numthreads, threading.active_count(), threading.enumerate())) def test_stdin(self): numthreads = threading.active_count() - result = runCmd("cat", data=b"hello world", timeout=self.TIMEOUT) + result = runCmd("cat", data=b"hello world", timeout=self.TIMEOUT, sync=False) self.assertEqual("hello world", result.output) - self.assertEqual(numthreads, threading.active_count()) + self.assertEqual(numthreads, threading.active_count(), msg="Thread counts were not equal before (%s) and after (%s), active threads: %s" % (numthreads, threading.active_count(), threading.enumerate())) + self.assertEqual(numthreads, 1) def test_stdin_timeout(self): numthreads = threading.active_count() start = time.time() - result = runCmd(['sleep', '60'], data=b"hello world", timeout=self.TIMEOUT, ignore_status=True) + result = runCmd(['sleep', '60'], data=b"hello world", timeout=self.TIMEOUT, ignore_status=True, sync=False) self.assertEqual(result.status, -signal.SIGTERM) end = time.time() self.assertLess(end - start, self.TIMEOUT + self.DELTA) - self.assertEqual(numthreads, threading.active_count()) + self.assertEqual(numthreads, threading.active_count(), msg="Thread counts were not equal before (%s) and after (%s), active threads: %s" % (numthreads, threading.active_count(), threading.enumerate())) def test_log(self): log = MemLogger() - result = runCmd("echo stdout; echo stderr >&2", shell=True, output_log=log) + result = runCmd("echo stdout; echo stderr >&2", shell=True, output_log=log, sync=False) self.assertEqual(["Running: echo stdout; echo stderr >&2", "stdout", "stderr"], log.info_msgs) self.assertEqual([], log.error_msgs) def test_log_split(self): log = MemLogger() - result = runCmd("echo stdout; echo stderr >&2", shell=True, output_log=log, stderr=subprocess.PIPE) + result = runCmd("echo stdout; echo stderr >&2", shell=True, output_log=log, stderr=subprocess.PIPE, sync=False) self.assertEqual(["Running: echo stdout; echo stderr >&2", "stdout"], log.info_msgs) self.assertEqual(["stderr"], log.error_msgs) diff --git a/meta/lib/oeqa/selftest/cases/runqemu.py b/meta/lib/oeqa/selftest/cases/runqemu.py index 7e676bcb41..f01e1eec66 100644 --- a/meta/lib/oeqa/selftest/cases/runqemu.py +++ b/meta/lib/oeqa/selftest/cases/runqemu.py @@ -4,14 +4,17 @@ # SPDX-License-Identifier: MIT # +import os import re -import tempfile import time import oe.types from oeqa.core.decorator import OETestTag +from oeqa.core.decorator.data import skipIfNotArch, skipIfNotMachine from oeqa.selftest.case import OESelftestTestCase -from oeqa.utils.commands import bitbake, runqemu, get_bb_var, runCmd +from oeqa.utils.commands import bitbake, runqemu, get_bb_var + +@OETestTag("runqemu") class RunqemuTests(OESelftestTestCase): """Runqemu test class""" @@ -21,23 +24,26 @@ class RunqemuTests(OESelftestTestCase): def setUpLocal(self): super(RunqemuTests, self).setUpLocal() self.recipe = 'core-image-minimal' - self.machine = 'qemux86-64' - self.fstypes = "ext4 iso hddimg wic.vmdk wic.qcow2 wic.vdi" - self.cmd_common = "runqemu nographic" + self.machine = self.td['MACHINE'] + self.image_link_name = get_bb_var('IMAGE_LINK_NAME', self.recipe) - kvm = oe.types.qemu_use_kvm(get_bb_var('QEMU_USE_KVM'), 'x86_64') + self.fstypes = "ext4" + if self.td["HOST_ARCH"] in ('i586', 'i686', 'x86_64'): + self.fstypes += " iso hddimg" + if self.machine == "qemux86-64": + self.fstypes += " wic.vmdk wic.qcow2 wic.vdi" + + self.cmd_common = "runqemu nographic" + kvm = oe.types.qemu_use_kvm(get_bb_var('QEMU_USE_KVM'), self.td["TARGET_ARCH"]) if kvm: self.cmd_common += " kvm" self.write_config( """ -MACHINE = "%s" IMAGE_FSTYPES = "%s" # 10 means 1 second SYSLINUX_TIMEOUT = "10" -""" -% (self.machine, self.fstypes) - ) +""" % self.fstypes) if not RunqemuTests.image_is_ready: RunqemuTests.deploy_dir_image = get_bb_var('DEPLOY_DIR_IMAGE') @@ -56,14 +62,17 @@ SYSLINUX_TIMEOUT = "10" cmd = "%s %s ext4" % (self.cmd_common, self.machine) with runqemu(self.recipe, ssh=False, launch_cmd=cmd) as qemu: with open(qemu.qemurunnerlog) as f: - self.assertIn('rootfs.ext4', f.read(), "Failed: %s" % cmd) + regexp = r'\nROOTFS: .*\.ext4]\n' + self.assertRegex(f.read(), regexp, "Failed to find '%s' in '%s' after running '%s'" % (regexp, qemu.qemurunnerlog, cmd)) + @skipIfNotArch(['i586', 'i686', 'x86_64']) def test_boot_machine_iso(self): """Test runqemu machine iso""" cmd = "%s %s iso" % (self.cmd_common, self.machine) with runqemu(self.recipe, ssh=False, launch_cmd=cmd) as qemu: with open(qemu.qemurunnerlog) as f: - self.assertIn('media=cdrom', f.read(), "Failed: %s" % cmd) + text_in = 'media=cdrom' + self.assertIn(text_in, f.read(), "Failed to find '%s' in '%s' after running '%s'" % (text_in, qemu.qemurunnerlog, cmd)) def test_boot_recipe_image(self): """Test runqemu recipe-image""" @@ -72,20 +81,24 @@ SYSLINUX_TIMEOUT = "10" with open(qemu.qemurunnerlog) as f: self.assertTrue(qemu.runner.logged, "Failed: %s, %s" % (cmd, f.read())) - + # https://bugzilla.yoctoproject.org/show_bug.cgi?id=14963 + @skipIfNotMachine("qemux86-64", "tests are qemux86-64 specific currently") def test_boot_recipe_image_vmdk(self): """Test runqemu recipe-image vmdk""" cmd = "%s %s wic.vmdk" % (self.cmd_common, self.recipe) with runqemu(self.recipe, ssh=False, launch_cmd=cmd) as qemu: with open(qemu.qemurunnerlog) as f: - self.assertIn('format=vmdk', f.read(), "Failed: %s" % cmd) + text_in = 'format=vmdk' + self.assertIn(text_in, f.read(), "Failed to find '%s' in '%s' after running '%s'" % (text_in, qemu.qemurunnerlog, cmd)) + @skipIfNotMachine("qemux86-64", "tests are qemux86-64 specific currently") def test_boot_recipe_image_vdi(self): """Test runqemu recipe-image vdi""" cmd = "%s %s wic.vdi" % (self.cmd_common, self.recipe) with runqemu(self.recipe, ssh=False, launch_cmd=cmd) as qemu: with open(qemu.qemurunnerlog) as f: - self.assertIn('format=vdi', f.read(), "Failed: %s" % cmd) + text_in = 'format=vdi' + self.assertIn(text_in, f.read(), "Failed to find '%s' in '%s' after running '%s'" % (text_in, qemu.qemurunnerlog, cmd)) def test_boot_deploy(self): """Test runqemu deploy_dir_image""" @@ -94,7 +107,7 @@ SYSLINUX_TIMEOUT = "10" with open(qemu.qemurunnerlog) as f: self.assertTrue(qemu.runner.logged, "Failed: %s, %s" % (cmd, f.read())) - + @skipIfNotArch(['i586', 'i686', 'x86_64']) def test_boot_deploy_hddimg(self): """Test runqemu deploy_dir_image hddimg""" cmd = "%s %s hddimg" % (self.cmd_common, self.deploy_dir_image) @@ -109,6 +122,7 @@ SYSLINUX_TIMEOUT = "10" with open(qemu.qemurunnerlog) as f: self.assertIn(' -netdev user', f.read(), "Failed: %s" % cmd) + @skipIfNotMachine("qemux86-64", "tests are qemux86-64 specific currently") def test_boot_machine_slirp_qcow2(self): """Test runqemu machine slirp qcow2""" cmd = "%s slirp wic.qcow2 %s" % (self.cmd_common, self.machine) @@ -118,7 +132,7 @@ SYSLINUX_TIMEOUT = "10" def test_boot_qemu_boot(self): """Test runqemu /path/to/image.qemuboot.conf""" - qemuboot_conf = "%s-%s.qemuboot.conf" % (self.recipe, self.machine) + qemuboot_conf = "%s.qemuboot.conf" % (self.image_link_name) qemuboot_conf = os.path.join(self.deploy_dir_image, qemuboot_conf) if not os.path.exists(qemuboot_conf): self.skipTest("%s not found" % qemuboot_conf) @@ -129,7 +143,7 @@ SYSLINUX_TIMEOUT = "10" def test_boot_rootfs(self): """Test runqemu /path/to/rootfs.ext4""" - rootfs = "%s-%s.ext4" % (self.recipe, self.machine) + rootfs = "%s.ext4" % (self.image_link_name) rootfs = os.path.join(self.deploy_dir_image, rootfs) if not os.path.exists(rootfs): self.skipTest("%s not found" % rootfs) @@ -149,26 +163,27 @@ SYSLINUX_TIMEOUT = "10" # bootup various filesystem types, including live image(iso and hddimg) # where live image was not supported on all qemu architecture. @OETestTag("machine") +@OETestTag("runqemu") class QemuTest(OESelftestTestCase): @classmethod def setUpClass(cls): super(QemuTest, cls).setUpClass() cls.recipe = 'core-image-minimal' - cls.machine = get_bb_var('MACHINE') - cls.deploy_dir_image = get_bb_var('DEPLOY_DIR_IMAGE') + cls.machine = get_bb_var('MACHINE') + cls.deploy_dir_image = get_bb_var('DEPLOY_DIR_IMAGE') + cls.image_link_name = get_bb_var('IMAGE_LINK_NAME', cls.recipe) cls.cmd_common = "runqemu nographic" - cls.qemuboot_conf = "%s-%s.qemuboot.conf" % (cls.recipe, cls.machine) + cls.qemuboot_conf = "%s.qemuboot.conf" % (cls.image_link_name) cls.qemuboot_conf = os.path.join(cls.deploy_dir_image, cls.qemuboot_conf) bitbake(cls.recipe) def _start_qemu_shutdown_check_if_shutdown_succeeded(self, qemu, timeout): + # Allow the runner's LoggingThread instance to exit without errors + # (such as the exception "Console connection closed unexpectedly") + # as qemu will disappear when we shut it down + qemu.runner.allowexit() qemu.run_serial("shutdown -h now") - # Stop thread will stop the LoggingThread instance used for logging - # qemu through serial console, stop thread will prevent this code - # from facing exception (Console connection closed unexpectedly) - # when qemu was shutdown by the above shutdown command - qemu.runner.stop_thread() time_track = 0 try: while True: @@ -190,22 +205,12 @@ class QemuTest(OESelftestTestCase): qemu_shutdown_succeeded = self._start_qemu_shutdown_check_if_shutdown_succeeded(qemu, shutdown_timeout) self.assertTrue(qemu_shutdown_succeeded, 'Failed: %s does not shutdown within timeout(%s)' % (self.machine, shutdown_timeout)) - # Need to have portmap/rpcbind running to allow this test to work and - # current autobuilder setup does not have this. - def disabled_test_qemu_can_boot_nfs_and_shutdown(self): - self.assertExists(self.qemuboot_conf) - bitbake('meta-ide-support') - rootfs_tar = "%s-%s.tar.bz2" % (self.recipe, self.machine) + def test_qemu_can_boot_nfs_and_shutdown(self): + rootfs_tar = "%s.tar.bz2" % (self.image_link_name) rootfs_tar = os.path.join(self.deploy_dir_image, rootfs_tar) self.assertExists(rootfs_tar) - tmpdir = tempfile.mkdtemp(prefix='qemu_nfs') - tmpdir_nfs = os.path.join(tmpdir, 'nfs') - cmd_extract_nfs = 'runqemu-extract-sdk %s %s' % (rootfs_tar, tmpdir_nfs) - result = runCmd(cmd_extract_nfs) - self.assertEqual(0, result.status, "runqemu-extract-sdk didn't run as expected. %s" % result.output) - cmd = "%s nfs %s %s" % (self.cmd_common, self.qemuboot_conf, tmpdir_nfs) + cmd = "%s %s" % (self.cmd_common, rootfs_tar) shutdown_timeout = 120 with runqemu(self.recipe, ssh=False, launch_cmd=cmd) as qemu: qemu_shutdown_succeeded = self._start_qemu_shutdown_check_if_shutdown_succeeded(qemu, shutdown_timeout) self.assertTrue(qemu_shutdown_succeeded, 'Failed: %s does not shutdown within timeout(%s)' % (self.machine, shutdown_timeout)) - runCmd('rm -rf %s' % tmpdir) diff --git a/meta/lib/oeqa/selftest/cases/runtime_test.py b/meta/lib/oeqa/selftest/cases/runtime_test.py index 60cb2e01a6..27090ae5cd 100644 --- a/meta/lib/oeqa/selftest/cases/runtime_test.py +++ b/meta/lib/oeqa/selftest/cases/runtime_test.py @@ -1,24 +1,20 @@ # +# Copyright OpenEmbedded Contributors +# # SPDX-License-Identifier: MIT # from oeqa.selftest.case import OESelftestTestCase from oeqa.utils.commands import runCmd, bitbake, get_bb_var, get_bb_vars, runqemu -from oeqa.utils.sshcontrol import SSHControl +from oeqa.core.decorator import OETestTag import os -import re import tempfile -import shutil import oe.lsb -from oeqa.core.decorator.data import skipIfNotQemu +from oeqa.core.decorator.data import skipIfNotQemu, skipIfNotMachine class TestExport(OESelftestTestCase): - @classmethod - def tearDownClass(cls): - runCmd("rm -rf /tmp/sdk") - super(TestExport, cls).tearDownClass() - + @OETestTag("runqemu") def test_testexport_basic(self): """ Summary: Check basic testexport functionality with only ping test enabled. @@ -29,7 +25,7 @@ class TestExport(OESelftestTestCase): Author: Mariano Lopez <mariano.lopez@intel.com> """ - features = 'INHERIT += "testexport"\n' + features = 'IMAGE_CLASSES += "testexport"\n' # These aren't the actual IP addresses but testexport class needs something defined features += 'TEST_SERVER_IP = "192.168.7.1"\n' features += 'TEST_TARGET_IP = "192.168.7.1"\n' @@ -70,7 +66,7 @@ class TestExport(OESelftestTestCase): Author: Mariano Lopez <mariano.lopez@intel.com> """ - features = 'INHERIT += "testexport"\n' + features = 'IMAGE_CLASSES += "testexport"\n' # These aren't the actual IP addresses but testexport class needs something defined features += 'TEST_SERVER_IP = "192.168.7.1"\n' features += 'TEST_TARGET_IP = "192.168.7.1"\n' @@ -95,21 +91,23 @@ class TestExport(OESelftestTestCase): msg = "Couldn't find SDK tarball: %s" % tarball_path self.assertEqual(os.path.isfile(tarball_path), True, msg) - # Extract SDK and run tar from SDK - result = runCmd("%s -y -d /tmp/sdk" % tarball_path) - self.assertEqual(0, result.status, "Couldn't extract SDK") + with tempfile.TemporaryDirectory() as tmpdirname: + # Extract SDK and run tar from SDK + result = runCmd("%s -y -d %s" % (tarball_path, tmpdirname)) + self.assertEqual(0, result.status, "Couldn't extract SDK") - env_script = result.output.split()[-1] - result = runCmd(". %s; which tar" % env_script, shell=True) - self.assertEqual(0, result.status, "Couldn't setup SDK environment") - is_sdk_tar = True if "/tmp/sdk" in result.output else False - self.assertTrue(is_sdk_tar, "Couldn't setup SDK environment") + env_script = result.output.split()[-1] + result = runCmd(". %s; which tar" % env_script, shell=True) + self.assertEqual(0, result.status, "Couldn't setup SDK environment") + is_sdk_tar = True if tmpdirname in result.output else False + self.assertTrue(is_sdk_tar, "Couldn't setup SDK environment") - tar_sdk = result.output - result = runCmd("%s --version" % tar_sdk) - self.assertEqual(0, result.status, "Couldn't run tar from SDK") + tar_sdk = result.output + result = runCmd("%s --version" % tar_sdk) + self.assertEqual(0, result.status, "Couldn't run tar from SDK") +@OETestTag("runqemu") class TestImage(OESelftestTestCase): def test_testimage_install(self): @@ -123,15 +121,30 @@ class TestImage(OESelftestTestCase): if get_bb_var('DISTRO') == 'poky-tiny': self.skipTest('core-image-full-cmdline not buildable for poky-tiny') - features = 'INHERIT += "testimage"\n' - features += 'IMAGE_INSTALL_append = " libssl"\n' + features = 'IMAGE_CLASSES += "testimage"\n' + features += 'IMAGE_INSTALL:append = " libssl"\n' features += 'TEST_SUITES = "ping ssh selftest"\n' self.write_config(features) - # Build core-image-sato and testimage bitbake('core-image-full-cmdline socat') bitbake('-c testimage core-image-full-cmdline') + def test_testimage_slirp(self): + """ + Summary: Check basic testimage functionality with qemu and slirp networking. + """ + + features = ''' +IMAGE_CLASSES:append = " testimage" +IMAGE_FEATURES:append = " ssh-server-dropbear" +IMAGE_ROOTFS_EXTRA_SPACE:append = "${@bb.utils.contains("IMAGE_CLASSES", "testimage", " + 5120", "", d)}" +TEST_RUNQEMUPARAMS += " slirp" +''' + self.write_config(features) + + bitbake('core-image-minimal') + bitbake('-c testimage core-image-minimal') + def test_testimage_dnf(self): """ Summary: Check package feeds functionality for dnf @@ -142,7 +155,7 @@ class TestImage(OESelftestTestCase): if get_bb_var('DISTRO') == 'poky-tiny': self.skipTest('core-image-full-cmdline not buildable for poky-tiny') - features = 'INHERIT += "testimage"\n' + features = 'IMAGE_CLASSES += "testimage"\n' features += 'TEST_SUITES = "ping ssh dnf_runtime dnf.DnfBasicTest.test_dnf_help"\n' # We don't yet know what the server ip and port will be - they will be patched # in at the start of the on-image test @@ -156,17 +169,57 @@ class TestImage(OESelftestTestCase): self.gpg_home = tempfile.mkdtemp(prefix="oeqa-feed-sign-") self.track_for_cleanup(self.gpg_home) signing_key_dir = os.path.join(self.testlayer_path, 'files', 'signing') - runCmd('gpg --batch --homedir %s --import %s' % (self.gpg_home, os.path.join(signing_key_dir, 'key.secret')), native_sysroot=get_bb_var("RECIPE_SYSROOT_NATIVE", "gnupg-native")) + runCmd('gpgconf --list-dirs --homedir %s; gpg -v --batch --homedir %s --import %s' % (self.gpg_home, self.gpg_home, os.path.join(signing_key_dir, 'key.secret')), native_sysroot=get_bb_var("RECIPE_SYSROOT_NATIVE", "gnupg-native"), shell=True) + features += 'INHERIT += "sign_package_feed"\n' + features += 'PACKAGE_FEED_GPG_NAME = "testuser"\n' + features += 'PACKAGE_FEED_GPG_PASSPHRASE_FILE = "%s"\n' % os.path.join(signing_key_dir, 'key.passphrase') + features += 'GPG_PATH = "%s"\n' % self.gpg_home + features += 'PSEUDO_IGNORE_PATHS .= ",%s"\n' % self.gpg_home + self.write_config(features) + + bitbake('core-image-full-cmdline socat') + bitbake('-c testimage core-image-full-cmdline') + + def test_testimage_apt(self): + """ + Summary: Check package feeds functionality for apt + Expected: 1. Check that remote package feeds can be accessed + Product: oe-core + Author: Ferry Toth <fntoth@gmail.com> + """ + if get_bb_var('DISTRO') == 'poky-tiny': + self.skipTest('core-image-full-cmdline not buildable for poky-tiny') + + features = 'IMAGE_CLASSES += "testimage"\n' + features += 'TEST_SUITES = "ping ssh apt.AptRepoTest.test_apt_install_from_repo"\n' + # We don't yet know what the server ip and port will be - they will be patched + # in at the start of the on-image test + features += 'PACKAGE_FEED_URIS = "http://bogus_ip:bogus_port"\n' + features += 'EXTRA_IMAGE_FEATURES += "package-management"\n' + features += 'PACKAGE_CLASSES = "package_deb"\n' + # We need gnupg on the target to install keys + features += 'IMAGE_INSTALL:append:pn-core-image-full-cmdline = " gnupg"\n' + + bitbake('gnupg-native -c addto_recipe_sysroot') + + # Enable package feed signing + self.gpg_home = tempfile.mkdtemp(prefix="oeqa-feed-sign-") + self.track_for_cleanup(self.gpg_home) + signing_key_dir = os.path.join(self.testlayer_path, 'files', 'signing') + runCmd('gpgconf --list-dirs --homedir %s; gpg -v --batch --homedir %s --import %s' % (self.gpg_home, self.gpg_home, os.path.join(signing_key_dir, 'key.secret')), native_sysroot=get_bb_var("RECIPE_SYSROOT_NATIVE", "gnupg-native"), shell=True) features += 'INHERIT += "sign_package_feed"\n' features += 'PACKAGE_FEED_GPG_NAME = "testuser"\n' features += 'PACKAGE_FEED_GPG_PASSPHRASE_FILE = "%s"\n' % os.path.join(signing_key_dir, 'key.passphrase') features += 'GPG_PATH = "%s"\n' % self.gpg_home + features += 'PSEUDO_IGNORE_PATHS .= ",%s"\n' % self.gpg_home self.write_config(features) # Build core-image-sato and testimage bitbake('core-image-full-cmdline socat') bitbake('-c testimage core-image-full-cmdline') + # https://bugzilla.yoctoproject.org/show_bug.cgi?id=14966 + @skipIfNotMachine("qemux86-64", "test needs qemux86-64") def test_testimage_virgl_gtk_sdl(self): """ Summary: Check host-assisted accelerate OpenGL functionality in qemu with gtk and SDL frontends @@ -180,36 +233,35 @@ class TestImage(OESelftestTestCase): distro = oe.lsb.distro_identifier() if distro and distro == 'debian-8': self.skipTest('virgl isn\'t working with Debian 8') + if distro and distro == 'debian-9': + self.skipTest('virgl isn\'t working with Debian 9') if distro and distro == 'centos-7': self.skipTest('virgl isn\'t working with Centos 7') if distro and distro == 'opensuseleap-15.0': self.skipTest('virgl isn\'t working with Opensuse 15.0') qemu_packageconfig = get_bb_var('PACKAGECONFIG', 'qemu-system-native') - sdl_packageconfig = get_bb_var('PACKAGECONFIG', 'libsdl2-native') - features = 'INHERIT += "testimage"\n' + qemu_distrofeatures = get_bb_var('DISTRO_FEATURES', 'qemu-system-native') + features = 'IMAGE_CLASSES += "testimage"\n' if 'gtk+' not in qemu_packageconfig: - features += 'PACKAGECONFIG_append_pn-qemu-system-native = " gtk+"\n' + features += 'PACKAGECONFIG:append:pn-qemu-system-native = " gtk+"\n' if 'sdl' not in qemu_packageconfig: - features += 'PACKAGECONFIG_append_pn-qemu-system-native = " sdl"\n' - if 'virglrenderer' not in qemu_packageconfig: - features += 'PACKAGECONFIG_append_pn-qemu-system-native = " virglrenderer"\n' - if 'glx' not in qemu_packageconfig: - features += 'PACKAGECONFIG_append_pn-qemu-system-native = " glx"\n' - if 'opengl' not in sdl_packageconfig: - features += 'PACKAGECONFIG_append_pn-libsdl2-native = " opengl"\n' + features += 'PACKAGECONFIG:append:pn-qemu-system-native = " sdl"\n' + if 'opengl' not in qemu_distrofeatures: + features += 'DISTRO_FEATURES:append = " opengl"\n' features += 'TEST_SUITES = "ping ssh virgl"\n' - features += 'IMAGE_FEATURES_append = " ssh-server-dropbear"\n' - features += 'IMAGE_INSTALL_append = " kmscube"\n' - features_gtk = features + 'TEST_RUNQEMUPARAMS = "gtk gl"\n' + features += 'IMAGE_FEATURES:append = " ssh-server-dropbear"\n' + features += 'IMAGE_INSTALL:append = " kmscube"\n' + features_gtk = features + 'TEST_RUNQEMUPARAMS += " gtk gl"\n' self.write_config(features_gtk) bitbake('core-image-minimal') bitbake('-c testimage core-image-minimal') - features_sdl = features + 'TEST_RUNQEMUPARAMS = "sdl gl"\n' + features_sdl = features + 'TEST_RUNQEMUPARAMS += " sdl gl"\n' self.write_config(features_sdl) bitbake('core-image-minimal') bitbake('-c testimage core-image-minimal') + @skipIfNotMachine("qemux86-64", "test needs qemux86-64") def test_testimage_virgl_headless(self): """ Summary: Check host-assisted accelerate OpenGL functionality in qemu with egl-headless frontend @@ -219,30 +271,25 @@ class TestImage(OESelftestTestCase): Author: Alexander Kanavin <alex.kanavin@gmail.com> """ import subprocess, os - try: - content = os.listdir("/dev/dri") - if len([i for i in content if i.startswith('render')]) == 0: - self.skipTest("No render nodes found in /dev/dri: %s" %(content)) - except FileNotFoundError: - self.skipTest("/dev/dri directory does not exist; no render nodes available on this machine.") - try: - dripath = subprocess.check_output("pkg-config --variable=dridriverdir dri", shell=True) - except subprocess.CalledProcessError as e: - self.skipTest("Could not determine the path to dri drivers on the host via pkg-config.\nPlease install Mesa development files (particularly, dri.pc) on the host machine.") - qemu_packageconfig = get_bb_var('PACKAGECONFIG', 'qemu-system-native') - features = 'INHERIT += "testimage"\n' - if 'virglrenderer' not in qemu_packageconfig: - features += 'PACKAGECONFIG_append_pn-qemu-system-native = " virglrenderer"\n' - if 'glx' not in qemu_packageconfig: - features += 'PACKAGECONFIG_append_pn-qemu-system-native = " glx"\n' + + distro = oe.lsb.distro_identifier() + if distro and (distro in ['debian-9', 'debian-10', 'centos-7', 'centos-8', 'centos-9', 'ubuntu-16.04', 'ubuntu-18.04'] or + distro.startswith('almalinux') or distro.startswith('rocky')): + self.skipTest('virgl headless cannot be tested with %s' %(distro)) + + qemu_distrofeatures = get_bb_var('DISTRO_FEATURES', 'qemu-system-native') + features = 'IMAGE_CLASSES += "testimage"\n' + if 'opengl' not in qemu_distrofeatures: + features += 'DISTRO_FEATURES:append = " opengl"\n' features += 'TEST_SUITES = "ping ssh virgl"\n' - features += 'IMAGE_FEATURES_append = " ssh-server-dropbear"\n' - features += 'IMAGE_INSTALL_append = " kmscube"\n' - features += 'TEST_RUNQEMUPARAMS = "egl-headless"\n' + features += 'IMAGE_FEATURES:append = " ssh-server-dropbear"\n' + features += 'IMAGE_INSTALL:append = " kmscube"\n' + features += 'TEST_RUNQEMUPARAMS += " egl-headless"\n' self.write_config(features) bitbake('core-image-minimal') bitbake('-c testimage core-image-minimal') +@OETestTag("runqemu") class Postinst(OESelftestTestCase): def init_manager_loop(self, init_manager): @@ -263,10 +310,7 @@ class Postinst(OESelftestTestCase): features += 'IMAGE_FEATURES += "package-management empty-root-password"\n' features += 'PACKAGE_CLASSES = "%s"\n' % classes if init_manager == "systemd": - features += 'DISTRO_FEATURES_append = " systemd"\n' - features += 'VIRTUAL-RUNTIME_init_manager = "systemd"\n' - features += 'DISTRO_FEATURES_BACKFILL_CONSIDERED = "sysvinit"\n' - features += 'VIRTUAL-RUNTIME_initscripts = ""\n' + features += 'INIT_MANAGER = "systemd"\n' self.write_config(features) bitbake('core-image-minimal') @@ -279,11 +323,11 @@ class Postinst(OESelftestTestCase): # run_serial()'s status code is useless.' for filename in ("rootfs", "delayed-a", "delayed-b"): status, output = qemu.run_serial("test -f %s && echo found" % os.path.join(targettestdir, filename)) - self.assertEqual(output, "found", "%s was not present on boot" % filename) + self.assertIn("found", output, "%s was not present on boot" % filename) - @skipIfNotQemu('qemuall', 'Test only runs in qemu') + @skipIfNotQemu() def test_postinst_rootfs_and_boot_sysvinit(self): """ Summary: The purpose of this test case is to verify Post-installation @@ -304,7 +348,7 @@ class Postinst(OESelftestTestCase): self.init_manager_loop("sysvinit") - @skipIfNotQemu('qemuall', 'Test only runs in qemu') + @skipIfNotQemu() def test_postinst_rootfs_and_boot_systemd(self): """ Summary: The purpose of this test case is to verify Post-installation @@ -360,6 +404,7 @@ class Postinst(OESelftestTestCase): self.assertFalse(os.path.isfile(os.path.join(hosttestdir, "rootfs-after-failure")), "rootfs-after-failure file was created") +@OETestTag("runqemu") class SystemTap(OESelftestTestCase): """ Summary: The purpose of this test case is to verify native crosstap @@ -380,14 +425,14 @@ TEST_SERVER_IP = "192.168.7.1" TEST_TARGET_IP = "192.168.7.2" EXTRA_IMAGE_FEATURES += "tools-profile dbg-pkgs" -IMAGE_FEATURES_append = " ssh-server-dropbear" +IMAGE_FEATURES:append = " ssh-server-dropbear" # enables kernel debug symbols -KERNEL_EXTRA_FEATURES_append = " features/debug/debug-kernel.scc" -KERNEL_EXTRA_FEATURES_append = " features/systemtap/systemtap.scc" +KERNEL_EXTRA_FEATURES:append = " features/debug/debug-kernel.scc" +KERNEL_EXTRA_FEATURES:append = " features/systemtap/systemtap.scc" # add systemtap run-time into target image if it is not there yet -IMAGE_INSTALL_append = " systemtap" +IMAGE_INSTALL:append = " systemtap-runtime" """ def test_crosstap_helloworld(self): @@ -436,4 +481,3 @@ IMAGE_INSTALL_append = " systemtap" cmd = "crosstap -r root@192.168.7.2 -s %s/process/ syscalls_by_pid.stp" % systemtap_examples result = runCmd(cmd) self.assertEqual(0, result.status, 'crosstap syscalls_by_pid returned a non 0 status:%s' % result.output) - diff --git a/meta/lib/oeqa/selftest/cases/rust.py b/meta/lib/oeqa/selftest/cases/rust.py new file mode 100644 index 0000000000..cbe6366f75 --- /dev/null +++ b/meta/lib/oeqa/selftest/cases/rust.py @@ -0,0 +1,143 @@ +# SPDX-License-Identifier: MIT +import os +import subprocess +import time +from oeqa.core.decorator import OETestTag +from oeqa.core.case import OEPTestResultTestCase +from oeqa.selftest.case import OESelftestTestCase +from oeqa.utils.commands import runCmd, bitbake, get_bb_var, get_bb_vars, runqemu, Command +from oeqa.utils.sshcontrol import SSHControl + +def parse_results(filename): + tests = {} + with open(filename, "r") as f: + lines = f.readlines() + for line in lines: + if "..." in line and "test [" in line: + test = line.split("test ")[1].split(" ... ")[0] + if "] " in test: + test = test.split("] ", 1)[1] + result = line.split(" ... ")[1].strip() + if result == "ok": + result = "PASS" + elif result == "failed": + result = "FAIL" + elif "ignored" in result: + result = "SKIPPED" + if test in tests: + if tests[test] != result: + print("Duplicate and mismatching result %s for %s" % (result, test)) + else: + print("Duplicate result %s for %s" % (result, test)) + else: + tests[test] = result + return tests + +# Total time taken for testing is of about 2hr 20min, with PARALLEL_MAKE set to 40 number of jobs. +@OETestTag("toolchain-system") +@OETestTag("toolchain-user") +@OETestTag("runqemu") +class RustSelfTestSystemEmulated(OESelftestTestCase, OEPTestResultTestCase): + def test_rust(self, *args, **kwargs): + # Disable Rust Oe-selftest + #self.skipTest("The Rust Oe-selftest is disabled.") + + # Skip mips32 target since it is unstable with rust tests + machine = get_bb_var('MACHINE') + if machine == "qemumips": + self.skipTest("The mips32 target is skipped for Rust Oe-selftest.") + + # build remote-test-server before image build + recipe = "rust" + start_time = time.time() + bitbake("{} -c test_compile".format(recipe)) + builddir = get_bb_var("RUSTSRC", "rust") + # build core-image-minimal with required packages + default_installed_packages = ["libgcc", "libstdc++", "libatomic", "libgomp"] + features = [] + features.append('IMAGE_FEATURES += "ssh-server-dropbear"') + features.append('CORE_IMAGE_EXTRA_INSTALL += "{0}"'.format(" ".join(default_installed_packages))) + self.write_config("\n".join(features)) + bitbake("core-image-minimal") + + # Exclude the test folders that error out while building + # TODO: Fix the errors and include them for testing + # no-fail-fast: Run all tests regardless of failure. + # bless: First runs rustfmt to format the codebase, + # then runs tidy checks. + exclude_list = [ + 'src/bootstrap', + 'src/doc/rustc', + 'src/doc/rustdoc', + 'src/doc/unstable-book', + 'src/librustdoc', + 'src/rustdoc-json-types', + 'src/tools/compiletest/src/common.rs', + 'src/tools/jsondoclint', + 'src/tools/lint-docs', + 'src/tools/replace-version-placeholder', + 'src/tools/rust-analyzer', + 'src/tools/rustdoc-themes', + 'src/tools/rust-installer', + 'src/tools/suggest-tests', + 'src/tools/tidy/src/', + 'tests/assembly/asm/aarch64-outline-atomics.rs', + 'tests/codegen/abi-main-signature-32bit-c-int.rs', + 'tests/codegen/i128-x86-align.rs', + 'tests/codegen/issues/issue-122805.rs', + 'tests/codegen/thread-local.rs', + 'tests/mir-opt/', + 'tests/run-make', + 'tests/run-make-fulldeps', + 'tests/rustdoc', + 'tests/rustdoc-json', + 'tests/rustdoc-js-std', + 'tests/ui/abi/stack-probes-lto.rs', + 'tests/ui/abi/stack-probes.rs', + 'tests/ui/codegen/mismatched-data-layouts.rs', + 'tests/ui/debuginfo/debuginfo-emit-llvm-ir-and-split-debuginfo.rs', + 'tests/ui-fulldeps/', + 'tests/ui/process/nofile-limit.rs', + 'tests/ui/structs-enums/multiple-reprs.rs', + 'tidyselftest' + ] + + exclude_fail_tests = " ".join([" --exclude " + item for item in exclude_list]) + # Add exclude_fail_tests with other test arguments + testargs = exclude_fail_tests + " --no-fail-fast --bless" + + # wrap the execution with a qemu instance. + # Tests are run with 512 tasks in parallel to execute all tests very quickly + with runqemu("core-image-minimal", runqemuparams = "nographic", qemuparams = "-m 512") as qemu: + # Copy remote-test-server to image through scp + host_sys = get_bb_var("RUST_BUILD_SYS", "rust") + ssh = SSHControl(ip=qemu.ip, logfile=qemu.sshlog, user="root") + ssh.copy_to(builddir + "/build/" + host_sys + "/stage1-tools-bin/remote-test-server","~/") + # Execute remote-test-server on image through background ssh + command = '~/remote-test-server --bind 0.0.0.0:12345 -v' + sshrun=subprocess.Popen(("ssh", '-o', 'UserKnownHostsFile=/dev/null', '-o', 'StrictHostKeyChecking=no', '-f', "root@%s" % qemu.ip, command), shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + # Get the values of variables. + tcpath = get_bb_var("TARGET_SYS", "rust") + targetsys = get_bb_var("RUST_TARGET_SYS", "rust") + rustlibpath = get_bb_var("WORKDIR", "rust") + tmpdir = get_bb_var("TMPDIR", "rust") + + # Set path for target-poky-linux-gcc, RUST_TARGET_PATH and hosttools. + cmd = "export TARGET_VENDOR=\"-poky\";" + cmd = cmd + " export PATH=%s/recipe-sysroot-native/usr/bin/python3-native:%s/recipe-sysroot-native/usr/bin:%s/recipe-sysroot-native/usr/bin/%s:%s/hosttools:$PATH;" % (rustlibpath, rustlibpath, rustlibpath, tcpath, tmpdir) + cmd = cmd + " export RUST_TARGET_PATH=%s/rust-targets;" % rustlibpath + # Trigger testing. + cmd = cmd + " export TEST_DEVICE_ADDR=\"%s:12345\";" % qemu.ip + cmd = cmd + " cd %s; python3 src/bootstrap/bootstrap.py test %s --target %s" % (builddir, testargs, targetsys) + retval = runCmd(cmd) + end_time = time.time() + + resultlog = rustlibpath + "/results-log.txt" + with open(resultlog, "w") as f: + f.write(retval.output) + + ptestsuite = "rust" + self.ptest_section(ptestsuite, duration = int(end_time - start_time), logfile=resultlog) + test_results = parse_results(resultlog) + for test in test_results: + self.ptest_result(ptestsuite, test, test_results[test]) diff --git a/meta/lib/oeqa/selftest/cases/selftest.py b/meta/lib/oeqa/selftest/cases/selftest.py index af080dcf03..a80a8651a5 100644 --- a/meta/lib/oeqa/selftest/cases/selftest.py +++ b/meta/lib/oeqa/selftest/cases/selftest.py @@ -1,9 +1,10 @@ # +# Copyright OpenEmbedded Contributors +# # SPDX-License-Identifier: MIT # import importlib -from oeqa.utils.commands import runCmd import oeqa.selftest from oeqa.selftest.case import OESelftestTestCase diff --git a/meta/lib/oeqa/selftest/cases/signing.py b/meta/lib/oeqa/selftest/cases/signing.py index 93b15ae681..18cce0ba25 100644 --- a/meta/lib/oeqa/selftest/cases/signing.py +++ b/meta/lib/oeqa/selftest/cases/signing.py @@ -1,4 +1,6 @@ # +# Copyright OpenEmbedded Contributors +# # SPDX-License-Identifier: MIT # @@ -44,7 +46,9 @@ class Signing(OESelftestTestCase): origenv = os.environ.copy() for e in os.environ: - if builddir in os.environ[e]: + if builddir + "/" in os.environ[e]: + os.environ[e] = os.environ[e].replace(builddir + "/", newbuilddir + "/") + if os.environ[e].endswith(builddir): os.environ[e] = os.environ[e].replace(builddir, newbuilddir) os.chdir(newbuilddir) @@ -143,7 +147,7 @@ class Signing(OESelftestTestCase): feature += 'GPG_PATH = "%s"\n' % self.gpg_dir feature += 'SSTATE_DIR = "%s"\n' % sstatedir # Any mirror might have partial sstate without .sig files, triggering failures - feature += 'SSTATE_MIRRORS_forcevariable = ""\n' + feature += 'SSTATE_MIRRORS:forcevariable = ""\n' self.write_config(feature) @@ -157,13 +161,13 @@ class Signing(OESelftestTestCase): bitbake('-c clean %s' % test_recipe) bitbake('-c populate_lic %s' % test_recipe) - recipe_sig = glob.glob(sstatedir + '/*/*:ed:*_populate_lic.tgz.sig') - recipe_tgz = glob.glob(sstatedir + '/*/*:ed:*_populate_lic.tgz') + recipe_sig = glob.glob(sstatedir + '/*/*/*:ed:*_populate_lic.tar.zst.sig') + recipe_archive = glob.glob(sstatedir + '/*/*/*:ed:*_populate_lic.tar.zst') self.assertEqual(len(recipe_sig), 1, 'Failed to find .sig file.') - self.assertEqual(len(recipe_tgz), 1, 'Failed to find .tgz file.') + self.assertEqual(len(recipe_archive), 1, 'Failed to find .tar.zst file.') - ret = runCmd('gpg --homedir %s --verify %s %s' % (self.gpg_dir, recipe_sig[0], recipe_tgz[0])) + ret = runCmd('gpg --homedir %s --verify %s %s' % (self.gpg_dir, recipe_sig[0], recipe_archive[0])) # gpg: Signature made Thu 22 Oct 2015 01:45:09 PM EEST using RSA key ID 61EEFB30 # gpg: Good signature from "testuser (nocomment) <testuser@email.com>" self.assertIn('gpg: Good signature from', ret.output, 'Package signed incorrectly.') @@ -187,7 +191,7 @@ class LockedSignatures(OESelftestTestCase): bitbake(test_recipe) # Generate locked sigs include file - bitbake('-S none %s' % test_recipe) + bitbake('-S lockedsigs %s' % test_recipe) feature = 'require %s\n' % locked_sigs_file feature += 'SIGGEN_LOCKEDSIGS_TASKSIG_CHECK = "warn"\n' @@ -204,7 +208,7 @@ class LockedSignatures(OESelftestTestCase): # Use uuid so hash equivalance server isn't triggered recipe_append_file = test_recipe + '_' + get_bb_var('PV', test_recipe) + '.bbappend' recipe_append_path = os.path.join(templayerdir, 'recipes-test', test_recipe, recipe_append_file) - feature = 'SUMMARY_${PN} = "test locked signature%s"\n' % uuid.uuid4() + feature = 'SUMMARY:${PN} = "test locked signature%s"\n' % uuid.uuid4() os.mkdir(os.path.join(templayerdir, 'recipes-test')) os.mkdir(os.path.join(templayerdir, 'recipes-test', test_recipe)) diff --git a/meta/lib/oeqa/selftest/cases/spdx.py b/meta/lib/oeqa/selftest/cases/spdx.py new file mode 100644 index 0000000000..be595babb3 --- /dev/null +++ b/meta/lib/oeqa/selftest/cases/spdx.py @@ -0,0 +1,170 @@ +# +# Copyright OpenEmbedded Contributors +# +# SPDX-License-Identifier: MIT +# + +import json +import os +import textwrap +from pathlib import Path +from oeqa.selftest.case import OESelftestTestCase +from oeqa.utils.commands import bitbake, get_bb_var, get_bb_vars, runCmd + + +class SPDX22Check(OESelftestTestCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + bitbake("python3-spdx-tools-native") + bitbake("-c addto_recipe_sysroot python3-spdx-tools-native") + + def check_recipe_spdx(self, high_level_dir, spdx_file, target_name): + config = textwrap.dedent( + """\ + INHERIT:remove = "create-spdx" + INHERIT += "create-spdx-2.2" + """ + ) + self.write_config(config) + + deploy_dir = get_bb_var("DEPLOY_DIR") + machine_var = get_bb_var("MACHINE") + spdx_version = get_bb_var("SPDX_VERSION") + # qemux86-64 creates the directory qemux86_64 + machine_dir = machine_var.replace("-", "_") + + full_file_path = os.path.join( + deploy_dir, "spdx", spdx_version, machine_dir, high_level_dir, spdx_file + ) + + try: + os.remove(full_file_path) + except FileNotFoundError: + pass + + bitbake("%s -c create_spdx" % target_name) + + def check_spdx_json(filename): + with open(filename) as f: + report = json.load(f) + self.assertNotEqual(report, None) + self.assertNotEqual(report["SPDXID"], None) + + python = os.path.join( + get_bb_var("STAGING_BINDIR", "python3-spdx-tools-native"), + "nativepython3", + ) + validator = os.path.join( + get_bb_var("STAGING_BINDIR", "python3-spdx-tools-native"), "pyspdxtools" + ) + result = runCmd("{} {} -i {}".format(python, validator, filename)) + + self.assertExists(full_file_path) + result = check_spdx_json(full_file_path) + + def test_spdx_base_files(self): + self.check_recipe_spdx("packages", "base-files.spdx.json", "base-files") + + +class SPDX3CheckBase(object): + """ + Base class for checking SPDX 3 based tests + """ + + def check_spdx_file(self, filename): + import oe.spdx30 + + self.assertExists(filename) + + # Read the file + objset = oe.spdx30.SHACLObjectSet() + with open(filename, "r") as f: + d = oe.spdx30.JSONLDDeserializer() + d.read(f, objset) + + return objset + + def check_recipe_spdx(self, target_name, spdx_path, *, task=None, extraconf=""): + config = textwrap.dedent( + f"""\ + INHERIT:remove = "create-spdx" + INHERIT += "{self.SPDX_CLASS}" + {extraconf} + """ + ) + self.write_config(config) + + if task: + bitbake(f"-c {task} {target_name}") + else: + bitbake(target_name) + + filename = spdx_path.format( + **get_bb_vars( + [ + "DEPLOY_DIR_IMAGE", + "DEPLOY_DIR_SPDX", + "MACHINE", + "MACHINE_ARCH", + "SDKMACHINE", + "SDK_DEPLOY", + "SPDX_VERSION", + "TOOLCHAIN_OUTPUTNAME", + ], + target_name, + ) + ) + + return self.check_spdx_file(filename) + + def check_objset_missing_ids(self, objset): + if objset.missing_ids: + self.assertTrue( + False, + "The following SPDXIDs are unresolved:\n " + + "\n ".join(objset.missing_ids), + ) + + +class SPDX30Check(SPDX3CheckBase, OESelftestTestCase): + SPDX_CLASS = "create-spdx-3.0" + + def test_base_files(self): + self.check_recipe_spdx( + "base-files", + "{DEPLOY_DIR_SPDX}/{MACHINE_ARCH}/packages/base-files.spdx.json", + ) + + def test_core_image_minimal(self): + objset = self.check_recipe_spdx( + "core-image-minimal", + "{DEPLOY_DIR_IMAGE}/core-image-minimal-{MACHINE}.rootfs.spdx.json", + ) + + # Document should be fully linked + self.check_objset_missing_ids(objset) + + def test_core_image_minimal_sdk(self): + objset = self.check_recipe_spdx( + "core-image-minimal", + "{SDK_DEPLOY}/{TOOLCHAIN_OUTPUTNAME}.spdx.json", + task="populate_sdk", + ) + + # Document should be fully linked + self.check_objset_missing_ids(objset) + + def test_baremetal_helloworld(self): + objset = self.check_recipe_spdx( + "baremetal-helloworld", + "{DEPLOY_DIR_IMAGE}/baremetal-helloworld-image-{MACHINE}.spdx.json", + extraconf=textwrap.dedent( + """\ + TCLIBC = "baremetal" + """ + ), + ) + + # Document should be fully linked + self.check_objset_missing_ids(objset) diff --git a/meta/lib/oeqa/selftest/cases/sstate.py b/meta/lib/oeqa/selftest/cases/sstate.py deleted file mode 100644 index 410dec64fc..0000000000 --- a/meta/lib/oeqa/selftest/cases/sstate.py +++ /dev/null @@ -1,67 +0,0 @@ -# -# SPDX-License-Identifier: MIT -# - -import datetime -import unittest -import os -import re -import shutil - -import oeqa.utils.ftools as ftools -from oeqa.selftest.case import OESelftestTestCase -from oeqa.utils.commands import runCmd, bitbake, get_bb_vars, get_test_layer - - -class SStateBase(OESelftestTestCase): - - def setUpLocal(self): - super(SStateBase, self).setUpLocal() - self.temp_sstate_location = None - needed_vars = ['SSTATE_DIR', 'NATIVELSBSTRING', 'TCLIBC', 'TUNE_ARCH', - 'TOPDIR', 'TARGET_VENDOR', 'TARGET_OS'] - bb_vars = get_bb_vars(needed_vars) - self.sstate_path = bb_vars['SSTATE_DIR'] - self.hostdistro = bb_vars['NATIVELSBSTRING'] - self.tclibc = bb_vars['TCLIBC'] - self.tune_arch = bb_vars['TUNE_ARCH'] - self.topdir = bb_vars['TOPDIR'] - self.target_vendor = bb_vars['TARGET_VENDOR'] - self.target_os = bb_vars['TARGET_OS'] - self.distro_specific_sstate = os.path.join(self.sstate_path, self.hostdistro) - - # Creates a special sstate configuration with the option to add sstate mirrors - def config_sstate(self, temp_sstate_location=False, add_local_mirrors=[]): - self.temp_sstate_location = temp_sstate_location - - if self.temp_sstate_location: - temp_sstate_path = os.path.join(self.builddir, "temp_sstate_%s" % datetime.datetime.now().strftime('%Y%m%d%H%M%S')) - config_temp_sstate = "SSTATE_DIR = \"%s\"" % temp_sstate_path - self.append_config(config_temp_sstate) - self.track_for_cleanup(temp_sstate_path) - bb_vars = get_bb_vars(['SSTATE_DIR', 'NATIVELSBSTRING']) - self.sstate_path = bb_vars['SSTATE_DIR'] - self.hostdistro = bb_vars['NATIVELSBSTRING'] - self.distro_specific_sstate = os.path.join(self.sstate_path, self.hostdistro) - - if add_local_mirrors: - config_set_sstate_if_not_set = 'SSTATE_MIRRORS ?= ""' - self.append_config(config_set_sstate_if_not_set) - for local_mirror in add_local_mirrors: - self.assertFalse(os.path.join(local_mirror) == os.path.join(self.sstate_path), msg='Cannot add the current sstate path as a sstate mirror') - config_sstate_mirror = "SSTATE_MIRRORS += \"file://.* file:///%s/PATH\"" % local_mirror - self.append_config(config_sstate_mirror) - - # Returns a list containing sstate files - def search_sstate(self, filename_regex, distro_specific=True, distro_nonspecific=True): - result = [] - for root, dirs, files in os.walk(self.sstate_path): - if distro_specific and re.search("%s/[a-z0-9]{2}$" % self.hostdistro, root): - for f in files: - if re.search(filename_regex, f): - result.append(f) - if distro_nonspecific and re.search("%s/[a-z0-9]{2}$" % self.sstate_path, root): - for f in files: - if re.search(filename_regex, f): - result.append(f) - return result diff --git a/meta/lib/oeqa/selftest/cases/sstatetests.py b/meta/lib/oeqa/selftest/cases/sstatetests.py index 6757a0ec68..ae295bef5f 100644 --- a/meta/lib/oeqa/selftest/cases/sstatetests.py +++ b/meta/lib/oeqa/selftest/cases/sstatetests.py @@ -1,4 +1,6 @@ # +# Copyright OpenEmbedded Contributors +# # SPDX-License-Identifier: MIT # @@ -7,51 +9,77 @@ import shutil import glob import subprocess import tempfile +import datetime +import re +from oeqa.utils.commands import runCmd, bitbake, get_bb_var, create_temp_layer, get_bb_vars from oeqa.selftest.case import OESelftestTestCase -from oeqa.utils.commands import runCmd, bitbake, get_bb_var, get_test_layer, create_temp_layer -from oeqa.selftest.cases.sstate import SStateBase +from oeqa.core.decorator import OETestTag +import oe import bb.siggen -class SStateTests(SStateBase): - def test_autorev_sstate_works(self): - # Test that a git repository which changes is correctly handled by SRCREV = ${AUTOREV} - # when PV does not contain SRCPV - - tempdir = tempfile.mkdtemp(prefix='oeqa') - self.track_for_cleanup(tempdir) - create_temp_layer(tempdir, 'selftestrecipetool') - self.add_command_to_tearDown('bitbake-layers remove-layer %s' % tempdir) - runCmd('bitbake-layers add-layer %s' % tempdir) - - # Use dbus-wait as a local git repo we can add a commit between two builds in - pn = 'dbus-wait' - srcrev = '6cc6077a36fe2648a5f993fe7c16c9632f946517' - url = 'git://git.yoctoproject.org/dbus-wait' - result = runCmd('git clone %s noname' % url, cwd=tempdir) - srcdir = os.path.join(tempdir, 'noname') - result = runCmd('git reset --hard %s' % srcrev, cwd=srcdir) - self.assertTrue(os.path.isfile(os.path.join(srcdir, 'configure.ac')), 'Unable to find configure script in source directory') - - recipefile = os.path.join(tempdir, "recipes-test", "dbus-wait-test", 'dbus-wait-test_git.bb') - os.makedirs(os.path.dirname(recipefile)) - srcuri = 'git://' + srcdir + ';protocol=file' - result = runCmd(['recipetool', 'create', '-o', recipefile, srcuri]) - self.assertTrue(os.path.isfile(recipefile), 'recipetool did not create recipe file; output:\n%s' % result.output) - - with open(recipefile, 'a') as f: - f.write('SRCREV = "${AUTOREV}"\n') - f.write('PV = "1.0"\n') - - bitbake("dbus-wait-test -c fetch") - with open(os.path.join(srcdir, "bar.txt"), "w") as f: - f.write("foo") - result = runCmd('git add bar.txt; git commit -asm "add bar"', cwd=srcdir) - bitbake("dbus-wait-test -c unpack") - - - # Test sstate files creation and their location +# Set to True to preserve stamp files after test execution for debugging failures +keep_temp_files = False + +class SStateBase(OESelftestTestCase): + + def setUpLocal(self): + super(SStateBase, self).setUpLocal() + self.temp_sstate_location = None + needed_vars = ['SSTATE_DIR', 'NATIVELSBSTRING', 'TCLIBC', 'TUNE_ARCH', + 'TOPDIR', 'TARGET_VENDOR', 'TARGET_OS'] + bb_vars = get_bb_vars(needed_vars) + self.sstate_path = bb_vars['SSTATE_DIR'] + self.hostdistro = bb_vars['NATIVELSBSTRING'] + self.tclibc = bb_vars['TCLIBC'] + self.tune_arch = bb_vars['TUNE_ARCH'] + self.topdir = bb_vars['TOPDIR'] + self.target_vendor = bb_vars['TARGET_VENDOR'] + self.target_os = bb_vars['TARGET_OS'] + self.distro_specific_sstate = os.path.join(self.sstate_path, self.hostdistro) + + def track_for_cleanup(self, path): + if not keep_temp_files: + super().track_for_cleanup(path) + + # Creates a special sstate configuration with the option to add sstate mirrors + def config_sstate(self, temp_sstate_location=False, add_local_mirrors=[]): + self.temp_sstate_location = temp_sstate_location + + if self.temp_sstate_location: + temp_sstate_path = os.path.join(self.builddir, "temp_sstate_%s" % datetime.datetime.now().strftime('%Y%m%d%H%M%S')) + config_temp_sstate = "SSTATE_DIR = \"%s\"" % temp_sstate_path + self.append_config(config_temp_sstate) + self.track_for_cleanup(temp_sstate_path) + bb_vars = get_bb_vars(['SSTATE_DIR', 'NATIVELSBSTRING']) + self.sstate_path = bb_vars['SSTATE_DIR'] + self.hostdistro = bb_vars['NATIVELSBSTRING'] + self.distro_specific_sstate = os.path.join(self.sstate_path, self.hostdistro) + + if add_local_mirrors: + config_set_sstate_if_not_set = 'SSTATE_MIRRORS ?= ""' + self.append_config(config_set_sstate_if_not_set) + for local_mirror in add_local_mirrors: + self.assertFalse(os.path.join(local_mirror) == os.path.join(self.sstate_path), msg='Cannot add the current sstate path as a sstate mirror') + config_sstate_mirror = "SSTATE_MIRRORS += \"file://.* file:///%s/PATH\"" % local_mirror + self.append_config(config_sstate_mirror) + + # Returns a list containing sstate files + def search_sstate(self, filename_regex, distro_specific=True, distro_nonspecific=True): + result = [] + for root, dirs, files in os.walk(self.sstate_path): + if distro_specific and re.search(r"%s/%s/[a-z0-9]{2}/[a-z0-9]{2}$" % (self.sstate_path, self.hostdistro), root): + for f in files: + if re.search(filename_regex, f): + result.append(f) + if distro_nonspecific and re.search(r"%s/[a-z0-9]{2}/[a-z0-9]{2}$" % self.sstate_path, root): + for f in files: + if re.search(filename_regex, f): + result.append(f) + return result + + # Test sstate files creation and their location and directory perms def run_test_sstate_creation(self, targets, distro_specific=True, distro_nonspecific=True, temp_sstate_location=True, should_pass=True): self.config_sstate(temp_sstate_location, [self.sstate_path]) @@ -60,12 +88,25 @@ class SStateTests(SStateBase): else: bitbake(['-ccleansstate'] + targets) + # We need to test that the env umask have does not effect sstate directory creation + # So, first, we'll get the current umask and set it to something we know incorrect + # See: sstate_task_postfunc for correct umask of os.umask(0o002) + import os + def current_umask(): + current_umask = os.umask(0) + os.umask(current_umask) + return current_umask + + orig_umask = current_umask() + # Set it to a umask we know will be 'wrong' + os.umask(0o022) + bitbake(targets) file_tracker = [] results = self.search_sstate('|'.join(map(str, targets)), distro_specific, distro_nonspecific) if distro_nonspecific: for r in results: - if r.endswith(("_populate_lic.tgz", "_populate_lic.tgz.siginfo", "_fetch.tgz.siginfo", "_unpack.tgz.siginfo", "_patch.tgz.siginfo")): + if r.endswith(("_populate_lic.tar.zst", "_populate_lic.tar.zst.siginfo", "_fetch.tar.zst.siginfo", "_unpack.tar.zst.siginfo", "_patch.tar.zst.siginfo")): continue file_tracker.append(r) else: @@ -76,17 +117,18 @@ class SStateTests(SStateBase): else: self.assertTrue(not file_tracker , msg="Found sstate files in the wrong place for: %s (found %s)" % (', '.join(map(str, targets)), str(file_tracker))) - def test_sstate_creation_distro_specific_pass(self): - self.run_test_sstate_creation(['binutils-cross-'+ self.tune_arch, 'binutils-native'], distro_specific=True, distro_nonspecific=False, temp_sstate_location=True) - - def test_sstate_creation_distro_specific_fail(self): - self.run_test_sstate_creation(['binutils-cross-'+ self.tune_arch, 'binutils-native'], distro_specific=False, distro_nonspecific=True, temp_sstate_location=True, should_pass=False) + # Now we'll walk the tree to check the mode and see if things are incorrect. + badperms = [] + for root, dirs, files in os.walk(self.sstate_path): + for directory in dirs: + if (os.stat(os.path.join(root, directory)).st_mode & 0o777) != 0o775: + badperms.append(os.path.join(root, directory)) - def test_sstate_creation_distro_nonspecific_pass(self): - self.run_test_sstate_creation(['linux-libc-headers'], distro_specific=False, distro_nonspecific=True, temp_sstate_location=True) + # Return to original umask + os.umask(orig_umask) - def test_sstate_creation_distro_nonspecific_fail(self): - self.run_test_sstate_creation(['linux-libc-headers'], distro_specific=True, distro_nonspecific=False, temp_sstate_location=True, should_pass=False) + if should_pass: + self.assertTrue(badperms , msg="Found sstate directories with the wrong permissions: %s (found %s)" % (', '.join(map(str, targets)), str(badperms))) # Test the sstate files deletion part of the do_cleansstate task def run_test_cleansstate_task(self, targets, distro_specific=True, distro_nonspecific=True, temp_sstate_location=True): @@ -95,29 +137,15 @@ class SStateTests(SStateBase): bitbake(['-ccleansstate'] + targets) bitbake(targets) - tgz_created = self.search_sstate('|'.join(map(str, [s + r'.*?\.tgz$' for s in targets])), distro_specific, distro_nonspecific) - self.assertTrue(tgz_created, msg="Could not find sstate .tgz files for: %s (%s)" % (', '.join(map(str, targets)), str(tgz_created))) + archives_created = self.search_sstate('|'.join(map(str, [s + r'.*?\.tar.zst$' for s in targets])), distro_specific, distro_nonspecific) + self.assertTrue(archives_created, msg="Could not find sstate .tar.zst files for: %s (%s)" % (', '.join(map(str, targets)), str(archives_created))) siginfo_created = self.search_sstate('|'.join(map(str, [s + r'.*?\.siginfo$' for s in targets])), distro_specific, distro_nonspecific) self.assertTrue(siginfo_created, msg="Could not find sstate .siginfo files for: %s (%s)" % (', '.join(map(str, targets)), str(siginfo_created))) bitbake(['-ccleansstate'] + targets) - tgz_removed = self.search_sstate('|'.join(map(str, [s + r'.*?\.tgz$' for s in targets])), distro_specific, distro_nonspecific) - self.assertTrue(not tgz_removed, msg="do_cleansstate didn't remove .tgz sstate files for: %s (%s)" % (', '.join(map(str, targets)), str(tgz_removed))) - - def test_cleansstate_task_distro_specific_nonspecific(self): - targets = ['binutils-cross-'+ self.tune_arch, 'binutils-native'] - targets.append('linux-libc-headers') - self.run_test_cleansstate_task(targets, distro_specific=True, distro_nonspecific=True, temp_sstate_location=True) - - def test_cleansstate_task_distro_nonspecific(self): - self.run_test_cleansstate_task(['linux-libc-headers'], distro_specific=False, distro_nonspecific=True, temp_sstate_location=True) - - def test_cleansstate_task_distro_specific(self): - targets = ['binutils-cross-'+ self.tune_arch, 'binutils-native'] - targets.append('linux-libc-headers') - self.run_test_cleansstate_task(targets, distro_specific=True, distro_nonspecific=False, temp_sstate_location=True) - + archives_removed = self.search_sstate('|'.join(map(str, [s + r'.*?\.tar.zst$' for s in targets])), distro_specific, distro_nonspecific) + self.assertTrue(not archives_removed, msg="do_cleansstate didn't remove .tar.zst sstate files for: %s (%s)" % (', '.join(map(str, targets)), str(archives_removed))) # Test rebuilding of distro-specific sstate files def run_test_rebuild_distro_specific_sstate(self, targets, temp_sstate_location=True): @@ -126,15 +154,15 @@ class SStateTests(SStateBase): bitbake(['-ccleansstate'] + targets) bitbake(targets) - results = self.search_sstate('|'.join(map(str, [s + r'.*?\.tgz$' for s in targets])), distro_specific=False, distro_nonspecific=True) + results = self.search_sstate('|'.join(map(str, [s + r'.*?\.tar.zst$' for s in targets])), distro_specific=False, distro_nonspecific=True) filtered_results = [] for r in results: - if r.endswith(("_populate_lic.tgz", "_populate_lic.tgz.siginfo")): + if r.endswith(("_populate_lic.tar.zst", "_populate_lic.tar.zst.siginfo")): continue filtered_results.append(r) self.assertTrue(filtered_results == [], msg="Found distro non-specific sstate for: %s (%s)" % (', '.join(map(str, targets)), str(filtered_results))) - file_tracker_1 = self.search_sstate('|'.join(map(str, [s + r'.*?\.tgz$' for s in targets])), distro_specific=True, distro_nonspecific=False) - self.assertTrue(len(file_tracker_1) >= len(targets), msg = "Not all sstate files ware created for: %s" % ', '.join(map(str, targets))) + file_tracker_1 = self.search_sstate('|'.join(map(str, [s + r'.*?\.tar.zst$' for s in targets])), distro_specific=True, distro_nonspecific=False) + self.assertTrue(len(file_tracker_1) >= len(targets), msg = "Not all sstate files were created for: %s" % ', '.join(map(str, targets))) self.track_for_cleanup(self.distro_specific_sstate + "_old") shutil.copytree(self.distro_specific_sstate, self.distro_specific_sstate + "_old") @@ -142,15 +170,114 @@ class SStateTests(SStateBase): bitbake(['-cclean'] + targets) bitbake(targets) - file_tracker_2 = self.search_sstate('|'.join(map(str, [s + r'.*?\.tgz$' for s in targets])), distro_specific=True, distro_nonspecific=False) - self.assertTrue(len(file_tracker_2) >= len(targets), msg = "Not all sstate files ware created for: %s" % ', '.join(map(str, targets))) + file_tracker_2 = self.search_sstate('|'.join(map(str, [s + r'.*?\.tar.zst$' for s in targets])), distro_specific=True, distro_nonspecific=False) + self.assertTrue(len(file_tracker_2) >= len(targets), msg = "Not all sstate files were created for: %s" % ', '.join(map(str, targets))) not_recreated = [x for x in file_tracker_1 if x not in file_tracker_2] - self.assertTrue(not_recreated == [], msg="The following sstate files ware not recreated: %s" % ', '.join(map(str, not_recreated))) + self.assertTrue(not_recreated == [], msg="The following sstate files were not recreated: %s" % ', '.join(map(str, not_recreated))) created_once = [x for x in file_tracker_2 if x not in file_tracker_1] - self.assertTrue(created_once == [], msg="The following sstate files ware created only in the second run: %s" % ', '.join(map(str, created_once))) + self.assertTrue(created_once == [], msg="The following sstate files were created only in the second run: %s" % ', '.join(map(str, created_once))) + + def sstate_common_samesigs(self, configA, configB, allarch=False): + + self.write_config(configA) + self.track_for_cleanup(self.topdir + "/tmp-sstatesamehash") + bitbake("world meta-toolchain -S none") + self.write_config(configB) + self.track_for_cleanup(self.topdir + "/tmp-sstatesamehash2") + bitbake("world meta-toolchain -S none") + + def get_files(d, result): + for root, dirs, files in os.walk(d): + for name in files: + if "meta-environment" in root or "cross-canadian" in root: + continue + if "do_build" not in name: + # 1.4.1+gitAUTOINC+302fca9f4c-r0.do_package_write_ipk.sigdata.f3a2a38697da743f0dbed8b56aafcf79 + (_, task, _, shash) = name.rsplit(".", 3) + result[os.path.join(os.path.basename(root), task)] = shash + + files1 = {} + files2 = {} + subdirs = sorted(glob.glob(self.topdir + "/tmp-sstatesamehash/stamps/*-nativesdk*-linux")) + if allarch: + subdirs.extend(sorted(glob.glob(self.topdir + "/tmp-sstatesamehash/stamps/all-*-linux"))) + + for subdir in subdirs: + nativesdkdir = os.path.basename(subdir) + get_files(self.topdir + "/tmp-sstatesamehash/stamps/" + nativesdkdir, files1) + get_files(self.topdir + "/tmp-sstatesamehash2/stamps/" + nativesdkdir, files2) + + self.maxDiff = None + self.assertEqual(files1, files2) + +class SStateTests(SStateBase): + def test_autorev_sstate_works(self): + # Test that a git repository which changes is correctly handled by SRCREV = ${AUTOREV} + + tempdir = tempfile.mkdtemp(prefix='sstate_autorev') + tempdldir = tempfile.mkdtemp(prefix='sstate_autorev_dldir') + self.track_for_cleanup(tempdir) + self.track_for_cleanup(tempdldir) + create_temp_layer(tempdir, 'selftestrecipetool') + self.add_command_to_tearDown('bitbake-layers remove-layer %s' % tempdir) + self.append_config("DL_DIR = \"%s\"" % tempdldir) + runCmd('bitbake-layers add-layer %s' % tempdir) + + # Use dbus-wait as a local git repo we can add a commit between two builds in + pn = 'dbus-wait' + srcrev = '6cc6077a36fe2648a5f993fe7c16c9632f946517' + url = 'git://git.yoctoproject.org/dbus-wait' + result = runCmd('git clone %s noname' % url, cwd=tempdir) + srcdir = os.path.join(tempdir, 'noname') + result = runCmd('git reset --hard %s' % srcrev, cwd=srcdir) + self.assertTrue(os.path.isfile(os.path.join(srcdir, 'configure.ac')), 'Unable to find configure script in source directory') + + recipefile = os.path.join(tempdir, "recipes-test", "dbus-wait-test", 'dbus-wait-test_git.bb') + os.makedirs(os.path.dirname(recipefile)) + srcuri = 'git://' + srcdir + ';protocol=file;branch=master' + result = runCmd(['recipetool', 'create', '-o', recipefile, srcuri]) + self.assertTrue(os.path.isfile(recipefile), 'recipetool did not create recipe file; output:\n%s' % result.output) + + with open(recipefile, 'a') as f: + f.write('SRCREV = "${AUTOREV}"\n') + f.write('PV = "1.0"\n') + + bitbake("dbus-wait-test -c fetch") + with open(os.path.join(srcdir, "bar.txt"), "w") as f: + f.write("foo") + result = runCmd('git add bar.txt; git commit -asm "add bar"', cwd=srcdir) + bitbake("dbus-wait-test -c unpack") + +class SStateCreation(SStateBase): + def test_sstate_creation_distro_specific_pass(self): + self.run_test_sstate_creation(['binutils-cross-'+ self.tune_arch, 'binutils-native'], distro_specific=True, distro_nonspecific=False, temp_sstate_location=True) + + def test_sstate_creation_distro_specific_fail(self): + self.run_test_sstate_creation(['binutils-cross-'+ self.tune_arch, 'binutils-native'], distro_specific=False, distro_nonspecific=True, temp_sstate_location=True, should_pass=False) + + def test_sstate_creation_distro_nonspecific_pass(self): + self.run_test_sstate_creation(['linux-libc-headers'], distro_specific=False, distro_nonspecific=True, temp_sstate_location=True) + + def test_sstate_creation_distro_nonspecific_fail(self): + self.run_test_sstate_creation(['linux-libc-headers'], distro_specific=True, distro_nonspecific=False, temp_sstate_location=True, should_pass=False) + +class SStateCleanup(SStateBase): + def test_cleansstate_task_distro_specific_nonspecific(self): + targets = ['binutils-cross-'+ self.tune_arch, 'binutils-native'] + targets.append('linux-libc-headers') + self.run_test_cleansstate_task(targets, distro_specific=True, distro_nonspecific=True, temp_sstate_location=True) + + def test_cleansstate_task_distro_nonspecific(self): + self.run_test_cleansstate_task(['linux-libc-headers'], distro_specific=False, distro_nonspecific=True, temp_sstate_location=True) + + def test_cleansstate_task_distro_specific(self): + targets = ['binutils-cross-'+ self.tune_arch, 'binutils-native'] + targets.append('linux-libc-headers') + self.run_test_cleansstate_task(targets, distro_specific=True, distro_nonspecific=False, temp_sstate_location=True) +class SStateDistroTests(SStateBase): def test_rebuild_distro_specific_sstate_cross_native_targets(self): self.run_test_rebuild_distro_specific_sstate(['binutils-cross-' + self.tune_arch, 'binutils-native'], temp_sstate_location=True) @@ -160,48 +287,48 @@ class SStateTests(SStateBase): def test_rebuild_distro_specific_sstate_native_target(self): self.run_test_rebuild_distro_specific_sstate(['binutils-native'], temp_sstate_location=True) - +class SStateCacheManagement(SStateBase): # Test the sstate-cache-management script. Each element in the global_config list is used with the corresponding element in the target_config list - # global_config elements are expected to not generate any sstate files that would be removed by sstate-cache-management.sh (such as changing the value of MACHINE) + # global_config elements are expected to not generate any sstate files that would be removed by sstate-cache-management.py (such as changing the value of MACHINE) def run_test_sstate_cache_management_script(self, target, global_config=[''], target_config=[''], ignore_patterns=[]): self.assertTrue(global_config) self.assertTrue(target_config) self.assertTrue(len(global_config) == len(target_config), msg='Lists global_config and target_config should have the same number of elements') - self.config_sstate(temp_sstate_location=True, add_local_mirrors=[self.sstate_path]) - # If buildhistory is enabled, we need to disable version-going-backwards - # QA checks for this test. It may report errors otherwise. - self.append_config('ERROR_QA_remove = "version-going-backwards"') + for idx in range(len(target_config)): + self.append_config(global_config[idx]) + self.append_recipeinc(target, target_config[idx]) + bitbake(target) + self.remove_config(global_config[idx]) + self.remove_recipeinc(target, target_config[idx]) + + self.config_sstate(temp_sstate_location=True, add_local_mirrors=[self.sstate_path]) - # For not this only checks if random sstate tasks are handled correctly as a group. + # For now this only checks if random sstate tasks are handled correctly as a group. # In the future we should add control over what tasks we check for. - sstate_archs_list = [] expected_remaining_sstate = [] for idx in range(len(target_config)): self.append_config(global_config[idx]) self.append_recipeinc(target, target_config[idx]) - sstate_arch = get_bb_var('SSTATE_PKGARCH', target) - if not sstate_arch in sstate_archs_list: - sstate_archs_list.append(sstate_arch) if target_config[idx] == target_config[-1]: - target_sstate_before_build = self.search_sstate(target + r'.*?\.tgz$') + target_sstate_before_build = self.search_sstate(target + r'.*?\.tar.zst$') bitbake("-cclean %s" % target) result = bitbake(target, ignore_status=True) if target_config[idx] == target_config[-1]: - target_sstate_after_build = self.search_sstate(target + r'.*?\.tgz$') + target_sstate_after_build = self.search_sstate(target + r'.*?\.tar.zst$') expected_remaining_sstate += [x for x in target_sstate_after_build if x not in target_sstate_before_build if not any(pattern in x for pattern in ignore_patterns)] self.remove_config(global_config[idx]) self.remove_recipeinc(target, target_config[idx]) self.assertEqual(result.status, 0, msg = "build of %s failed with %s" % (target, result.output)) - runCmd("sstate-cache-management.sh -y --cache-dir=%s --remove-duplicated --extra-archs=%s" % (self.sstate_path, ','.join(map(str, sstate_archs_list)))) - actual_remaining_sstate = [x for x in self.search_sstate(target + r'.*?\.tgz$') if not any(pattern in x for pattern in ignore_patterns)] + runCmd("sstate-cache-management.py -y --cache-dir=%s --remove-duplicated" % (self.sstate_path)) + actual_remaining_sstate = [x for x in self.search_sstate(target + r'.*?\.tar.zst$') if not any(pattern in x for pattern in ignore_patterns)] actual_not_expected = [x for x in actual_remaining_sstate if x not in expected_remaining_sstate] - self.assertFalse(actual_not_expected, msg="Files should have been removed but ware not: %s" % ', '.join(map(str, actual_not_expected))) + self.assertFalse(actual_not_expected, msg="Files should have been removed but were not: %s" % ', '.join(map(str, actual_not_expected))) expected_not_actual = [x for x in expected_remaining_sstate if x not in actual_remaining_sstate] - self.assertFalse(expected_not_actual, msg="Extra files ware removed: %s" ', '.join(map(str, expected_not_actual))) + self.assertFalse(expected_not_actual, msg="Extra files were removed: %s" ', '.join(map(str, expected_not_actual))) def test_sstate_cache_management_script_using_pr_1(self): global_config = [] @@ -239,6 +366,7 @@ class SStateTests(SStateBase): target_config.append('') self.run_test_sstate_cache_management_script('m4', global_config, target_config, ignore_patterns=['populate_lic']) +class SStateHashSameSigs(SStateBase): def test_sstate_32_64_same_hash(self): """ The sstate checksums for both native and target should not vary whether @@ -250,7 +378,6 @@ class SStateTests(SStateBase): self.write_config(""" MACHINE = "qemux86" TMPDIR = "${TOPDIR}/tmp-sstatesamehash" -TCLIBCAPPEND = "" BUILD_ARCH = "x86_64" BUILD_OS = "linux" SDKMACHINE = "x86_64" @@ -258,11 +385,10 @@ PACKAGE_CLASSES = "package_rpm package_ipk package_deb" BB_SIGNATURE_HANDLER = "OEBasicHash" """) self.track_for_cleanup(self.topdir + "/tmp-sstatesamehash") - bitbake("core-image-sato -S none") + bitbake("core-image-weston -S none") self.write_config(""" MACHINE = "qemux86" TMPDIR = "${TOPDIR}/tmp-sstatesamehash2" -TCLIBCAPPEND = "" BUILD_ARCH = "i686" BUILD_OS = "linux" SDKMACHINE = "i686" @@ -270,12 +396,12 @@ PACKAGE_CLASSES = "package_rpm package_ipk package_deb" BB_SIGNATURE_HANDLER = "OEBasicHash" """) self.track_for_cleanup(self.topdir + "/tmp-sstatesamehash2") - bitbake("core-image-sato -S none") + bitbake("core-image-weston -S none") def get_files(d): f = [] for root, dirs, files in os.walk(d): - if "core-image-sato" in root: + if "core-image-weston" in root: # SDKMACHINE changing will change # do_rootfs/do_testimage/do_build stamps of images which # is safe to ignore. @@ -298,20 +424,18 @@ BB_SIGNATURE_HANDLER = "OEBasicHash" self.write_config(""" TMPDIR = \"${TOPDIR}/tmp-sstatesamehash\" -TCLIBCAPPEND = \"\" NATIVELSBSTRING = \"DistroA\" BB_SIGNATURE_HANDLER = "OEBasicHash" """) self.track_for_cleanup(self.topdir + "/tmp-sstatesamehash") - bitbake("core-image-sato -S none") + bitbake("core-image-weston -S none") self.write_config(""" TMPDIR = \"${TOPDIR}/tmp-sstatesamehash2\" -TCLIBCAPPEND = \"\" NATIVELSBSTRING = \"DistroB\" BB_SIGNATURE_HANDLER = "OEBasicHash" """) self.track_for_cleanup(self.topdir + "/tmp-sstatesamehash2") - bitbake("core-image-sato -S none") + bitbake("core-image-weston -S none") def get_files(d): f = [] @@ -324,6 +448,7 @@ BB_SIGNATURE_HANDLER = "OEBasicHash" self.maxDiff = None self.assertCountEqual(files1, files2) +class SStateHashSameSigs2(SStateBase): def test_sstate_allarch_samesigs(self): """ The sstate checksums of allarch packages should be independent of whichever @@ -334,17 +459,19 @@ BB_SIGNATURE_HANDLER = "OEBasicHash" configA = """ TMPDIR = \"${TOPDIR}/tmp-sstatesamehash\" -TCLIBCAPPEND = \"\" MACHINE = \"qemux86-64\" BB_SIGNATURE_HANDLER = "OEBasicHash" """ + #OLDEST_KERNEL is arch specific so set to a different value here for testing configB = """ TMPDIR = \"${TOPDIR}/tmp-sstatesamehash2\" -TCLIBCAPPEND = \"\" MACHINE = \"qemuarm\" +OLDEST_KERNEL = \"3.3.0\" BB_SIGNATURE_HANDLER = "OEBasicHash" +ERROR_QA:append = " somenewoption" +WARN_QA:append = " someotheroption" """ - self.sstate_allarch_samesigs(configA, configB) + self.sstate_common_samesigs(configA, configB, allarch=True) def test_sstate_nativesdk_samesigs_multilib(self): """ @@ -353,51 +480,22 @@ BB_SIGNATURE_HANDLER = "OEBasicHash" configA = """ TMPDIR = \"${TOPDIR}/tmp-sstatesamehash\" -TCLIBCAPPEND = \"\" MACHINE = \"qemux86-64\" require conf/multilib.conf MULTILIBS = \"multilib:lib32\" -DEFAULTTUNE_virtclass-multilib-lib32 = \"x86\" +DEFAULTTUNE:virtclass-multilib-lib32 = \"x86\" BB_SIGNATURE_HANDLER = "OEBasicHash" """ configB = """ TMPDIR = \"${TOPDIR}/tmp-sstatesamehash2\" -TCLIBCAPPEND = \"\" MACHINE = \"qemuarm\" require conf/multilib.conf MULTILIBS = \"\" BB_SIGNATURE_HANDLER = "OEBasicHash" """ - self.sstate_allarch_samesigs(configA, configB) - - def sstate_allarch_samesigs(self, configA, configB): - - self.write_config(configA) - self.track_for_cleanup(self.topdir + "/tmp-sstatesamehash") - bitbake("world meta-toolchain -S none") - self.write_config(configB) - self.track_for_cleanup(self.topdir + "/tmp-sstatesamehash2") - bitbake("world meta-toolchain -S none") - - def get_files(d): - f = {} - for root, dirs, files in os.walk(d): - for name in files: - if "meta-environment" in root or "cross-canadian" in root: - continue - if "do_build" not in name: - # 1.4.1+gitAUTOINC+302fca9f4c-r0.do_package_write_ipk.sigdata.f3a2a38697da743f0dbed8b56aafcf79 - (_, task, _, shash) = name.rsplit(".", 3) - f[os.path.join(os.path.basename(root), task)] = shash - return f - - nativesdkdir = os.path.basename(glob.glob(self.topdir + "/tmp-sstatesamehash/stamps/*-nativesdk*-linux")[0]) - - files1 = get_files(self.topdir + "/tmp-sstatesamehash/stamps/" + nativesdkdir) - files2 = get_files(self.topdir + "/tmp-sstatesamehash2/stamps/" + nativesdkdir) - self.maxDiff = None - self.assertEqual(files1, files2) + self.sstate_common_samesigs(configA, configB) +class SStateHashSameSigs3(SStateBase): def test_sstate_sametune_samesigs(self): """ The sstate checksums of two identical machines (using the same tune) should be the @@ -407,22 +505,20 @@ BB_SIGNATURE_HANDLER = "OEBasicHash" self.write_config(""" TMPDIR = \"${TOPDIR}/tmp-sstatesamehash\" -TCLIBCAPPEND = \"\" MACHINE = \"qemux86\" require conf/multilib.conf MULTILIBS = "multilib:lib32" -DEFAULTTUNE_virtclass-multilib-lib32 = "x86" +DEFAULTTUNE:virtclass-multilib-lib32 = "x86" BB_SIGNATURE_HANDLER = "OEBasicHash" """) self.track_for_cleanup(self.topdir + "/tmp-sstatesamehash") bitbake("world meta-toolchain -S none") self.write_config(""" TMPDIR = \"${TOPDIR}/tmp-sstatesamehash2\" -TCLIBCAPPEND = \"\" MACHINE = \"qemux86copy\" require conf/multilib.conf MULTILIBS = "multilib:lib32" -DEFAULTTUNE_virtclass-multilib-lib32 = "x86" +DEFAULTTUNE:virtclass-multilib-lib32 = "x86" BB_SIGNATURE_HANDLER = "OEBasicHash" """) self.track_for_cleanup(self.topdir + "/tmp-sstatesamehash2") @@ -432,7 +528,7 @@ BB_SIGNATURE_HANDLER = "OEBasicHash" f = [] for root, dirs, files in os.walk(d): for name in files: - if "meta-environment" in root or "cross-canadian" in root: + if "meta-environment" in root or "cross-canadian" in root or 'meta-ide-support' in root: continue if "qemux86copy-" in root or "qemux86-" in root: continue @@ -446,6 +542,44 @@ BB_SIGNATURE_HANDLER = "OEBasicHash" self.assertCountEqual(files1, files2) + def test_sstate_multilib_or_not_native_samesigs(self): + """The sstate checksums of two native recipes (and their dependencies) + where the target is using multilib in one but not the other + should be the same. We use the qemux86copy machine to test + this. + """ + + self.write_config(""" +TMPDIR = \"${TOPDIR}/tmp-sstatesamehash\" +MACHINE = \"qemux86\" +require conf/multilib.conf +MULTILIBS = "multilib:lib32" +DEFAULTTUNE:virtclass-multilib-lib32 = "x86" +BB_SIGNATURE_HANDLER = "OEBasicHash" +""") + self.track_for_cleanup(self.topdir + "/tmp-sstatesamehash") + bitbake("binutils-native -S none") + self.write_config(""" +TMPDIR = \"${TOPDIR}/tmp-sstatesamehash2\" +MACHINE = \"qemux86copy\" +BB_SIGNATURE_HANDLER = "OEBasicHash" +""") + self.track_for_cleanup(self.topdir + "/tmp-sstatesamehash2") + bitbake("binutils-native -S none") + + def get_files(d): + f = [] + for root, dirs, files in os.walk(d): + for name in files: + f.append(os.path.join(root, name)) + return f + files1 = get_files(self.topdir + "/tmp-sstatesamehash/stamps") + files2 = get_files(self.topdir + "/tmp-sstatesamehash2/stamps") + files2 = [x.replace("tmp-sstatesamehash2", "tmp-sstatesamehash") for x in files2] + self.maxDiff = None + self.assertCountEqual(files1, files2) + +class SStateHashSameSigs4(SStateBase): def test_sstate_noop_samesigs(self): """ The sstate checksums of two builds with these variables changed or @@ -454,13 +588,12 @@ BB_SIGNATURE_HANDLER = "OEBasicHash" self.write_config(""" TMPDIR = "${TOPDIR}/tmp-sstatesamehash" -TCLIBCAPPEND = "" BB_NUMBER_THREADS = "${@oe.utils.cpu_count()}" PARALLEL_MAKE = "-j 1" DL_DIR = "${TOPDIR}/download1" TIME = "111111" DATE = "20161111" -INHERIT_remove = "buildstats-summary buildhistory uninative" +INHERIT:remove = "buildstats-summary buildhistory uninative" http_proxy = "" BB_SIGNATURE_HANDLER = "OEBasicHash" """) @@ -469,14 +602,13 @@ BB_SIGNATURE_HANDLER = "OEBasicHash" bitbake("world meta-toolchain -S none") self.write_config(""" TMPDIR = "${TOPDIR}/tmp-sstatesamehash2" -TCLIBCAPPEND = "" BB_NUMBER_THREADS = "${@oe.utils.cpu_count()+1}" PARALLEL_MAKE = "-j 2" DL_DIR = "${TOPDIR}/download2" TIME = "222222" DATE = "20161212" # Always remove uninative as we're changing proxies -INHERIT_remove = "uninative" +INHERIT:remove = "uninative" INHERIT += "buildstats-summary buildhistory" http_proxy = "http://example.com/" BB_SIGNATURE_HANDLER = "OEBasicHash" @@ -530,3 +662,334 @@ BB_SIGNATURE_HANDLER = "OEBasicHash" compare_sigfiles(rest, files1, files2, compare=False) self.fail("sstate hashes not identical.") + + def test_sstate_movelayer_samesigs(self): + """ + The sstate checksums of two builds with the same oe-core layer in two + different locations should be the same. + """ + core_layer = os.path.join( + self.tc.td["COREBASE"], 'meta') + copy_layer_1 = self.topdir + "/meta-copy1/meta" + copy_layer_2 = self.topdir + "/meta-copy2/meta" + + oe.path.copytree(core_layer, copy_layer_1) + os.symlink(os.path.dirname(core_layer) + "/scripts", self.topdir + "/meta-copy1/scripts") + self.write_config(""" +TMPDIR = "${TOPDIR}/tmp-sstatesamehash" +""") + bblayers_conf = 'BBLAYERS += "%s"\nBBLAYERS:remove = "%s"' % (copy_layer_1, core_layer) + self.write_bblayers_config(bblayers_conf) + self.track_for_cleanup(self.topdir + "/tmp-sstatesamehash") + bitbake("bash -S none") + + oe.path.copytree(core_layer, copy_layer_2) + os.symlink(os.path.dirname(core_layer) + "/scripts", self.topdir + "/meta-copy2/scripts") + self.write_config(""" +TMPDIR = "${TOPDIR}/tmp-sstatesamehash2" +""") + bblayers_conf = 'BBLAYERS += "%s"\nBBLAYERS:remove = "%s"' % (copy_layer_2, core_layer) + self.write_bblayers_config(bblayers_conf) + self.track_for_cleanup(self.topdir + "/tmp-sstatesamehash2") + bitbake("bash -S none") + + def get_files(d): + f = [] + for root, dirs, files in os.walk(d): + for name in files: + f.append(os.path.join(root, name)) + return f + files1 = get_files(self.topdir + "/tmp-sstatesamehash/stamps") + files2 = get_files(self.topdir + "/tmp-sstatesamehash2/stamps") + files2 = [x.replace("tmp-sstatesamehash2", "tmp-sstatesamehash") for x in files2] + self.maxDiff = None + self.assertCountEqual(files1, files2) + +class SStateFindSiginfo(SStateBase): + def test_sstate_compare_sigfiles_and_find_siginfo(self): + """ + Test the functionality of the find_siginfo: basic function and callback in compare_sigfiles + """ + self.write_config(""" +TMPDIR = \"${TOPDIR}/tmp-sstates-findsiginfo\" +MACHINE = \"qemux86-64\" +require conf/multilib.conf +MULTILIBS = "multilib:lib32" +DEFAULTTUNE:virtclass-multilib-lib32 = "x86" +BB_SIGNATURE_HANDLER = "OEBasicHash" +""") + self.track_for_cleanup(self.topdir + "/tmp-sstates-findsiginfo") + + pns = ["binutils", "binutils-native", "lib32-binutils"] + target_configs = [ +""" +TMPVAL1 = "tmpval1" +TMPVAL2 = "tmpval2" +do_tmptask1() { + echo ${TMPVAL1} +} +do_tmptask2() { + echo ${TMPVAL2} +} +addtask do_tmptask1 +addtask tmptask2 before do_tmptask1 +""", +""" +TMPVAL3 = "tmpval3" +TMPVAL4 = "tmpval4" +do_tmptask1() { + echo ${TMPVAL3} +} +do_tmptask2() { + echo ${TMPVAL4} +} +addtask do_tmptask1 +addtask tmptask2 before do_tmptask1 +""" + ] + + for target_config in target_configs: + self.write_recipeinc("binutils", target_config) + for pn in pns: + bitbake("%s -c do_tmptask1 -S none" % pn) + self.delete_recipeinc("binutils") + + with bb.tinfoil.Tinfoil() as tinfoil: + tinfoil.prepare(config_only=True) + + def find_siginfo(pn, taskname, sigs=None): + result = None + command_complete = False + tinfoil.set_event_mask(["bb.event.FindSigInfoResult", + "bb.command.CommandCompleted"]) + ret = tinfoil.run_command("findSigInfo", pn, taskname, sigs) + if ret: + while result is None or not command_complete: + event = tinfoil.wait_event(1) + if event: + if isinstance(event, bb.command.CommandCompleted): + command_complete = True + elif isinstance(event, bb.event.FindSigInfoResult): + result = event.result + return result + + def recursecb(key, hash1, hash2): + nonlocal recursecb_count + recursecb_count += 1 + hashes = [hash1, hash2] + hashfiles = find_siginfo(key, None, hashes) + self.assertCountEqual(hashes, hashfiles) + bb.siggen.compare_sigfiles(hashfiles[hash1]['path'], hashfiles[hash2]['path'], recursecb) + + for pn in pns: + recursecb_count = 0 + matches = find_siginfo(pn, "do_tmptask1") + self.assertGreaterEqual(len(matches), 2) + latesthashes = sorted(matches.keys(), key=lambda h: matches[h]['time'])[-2:] + bb.siggen.compare_sigfiles(matches[latesthashes[-2]]['path'], matches[latesthashes[-1]]['path'], recursecb) + self.assertEqual(recursecb_count,1) + +class SStatePrintdiff(SStateBase): + def run_test_printdiff_changerecipe(self, target, change_recipe, change_bbtask, change_content, expected_sametmp_output, expected_difftmp_output): + import time + self.write_config(""" +TMPDIR = "${{TOPDIR}}/tmp-sstateprintdiff-sametmp-{}" +""".format(time.time())) + # Use runall do_build to ensure any indirect sstate is created, e.g. tzcode-native on both x86 and + # aarch64 hosts since only allarch target recipes depend upon it and it may not be built otherwise. + # A bitbake -c cleansstate tzcode-native would cause some of these tests to error for example. + bitbake("--runall build --runall deploy_source_date_epoch {}".format(target)) + bitbake("-S none {}".format(target)) + bitbake(change_bbtask) + self.write_recipeinc(change_recipe, change_content) + result_sametmp = bitbake("-S printdiff {}".format(target)) + + self.write_config(""" +TMPDIR = "${{TOPDIR}}/tmp-sstateprintdiff-difftmp-{}" +""".format(time.time())) + result_difftmp = bitbake("-S printdiff {}".format(target)) + + self.delete_recipeinc(change_recipe) + for item in expected_sametmp_output: + self.assertIn(item, result_sametmp.output, msg = "Item {} not found in output:\n{}".format(item, result_sametmp.output)) + for item in expected_difftmp_output: + self.assertIn(item, result_difftmp.output, msg = "Item {} not found in output:\n{}".format(item, result_difftmp.output)) + + def run_test_printdiff_changeconfig(self, target, change_bbtasks, change_content, expected_sametmp_output, expected_difftmp_output): + import time + self.write_config(""" +TMPDIR = "${{TOPDIR}}/tmp-sstateprintdiff-sametmp-{}" +""".format(time.time())) + bitbake("--runall build --runall deploy_source_date_epoch {}".format(target)) + bitbake("-S none {}".format(target)) + bitbake(" ".join(change_bbtasks)) + self.append_config(change_content) + result_sametmp = bitbake("-S printdiff {}".format(target)) + + self.write_config(""" +TMPDIR = "${{TOPDIR}}/tmp-sstateprintdiff-difftmp-{}" +""".format(time.time())) + self.append_config(change_content) + result_difftmp = bitbake("-S printdiff {}".format(target)) + + for item in expected_sametmp_output: + self.assertIn(item, result_sametmp.output, msg = "Item {} not found in output:\n{}".format(item, result_sametmp.output)) + for item in expected_difftmp_output: + self.assertIn(item, result_difftmp.output, msg = "Item {} not found in output:\n{}".format(item, result_difftmp.output)) + + + # Check if printdiff walks the full dependency chain from the image target to where the change is in a specific recipe + def test_image_minimal_vs_perlcross(self): + expected_output = ("Task perlcross-native:do_install couldn't be used from the cache because:", +"We need hash", +"most recent matching task was") + expected_sametmp_output = expected_output + ( +"Variable do_install value changed", +'+ echo "this changes the task signature"') + expected_difftmp_output = expected_output + + self.run_test_printdiff_changerecipe("core-image-minimal", "perlcross", "-c do_install perlcross-native", +""" +do_install:append() { + echo "this changes the task signature" +} +""", +expected_sametmp_output, expected_difftmp_output) + + # Check if changes to gcc-source (which uses tmp/work-shared) are correctly discovered + def test_gcc_runtime_vs_gcc_source(self): + gcc_source_pn = 'gcc-source-%s' % get_bb_vars(['PV'], 'gcc')['PV'] + + expected_output = ("Task {}:do_preconfigure couldn't be used from the cache because:".format(gcc_source_pn), +"We need hash", +"most recent matching task was") + expected_sametmp_output = expected_output + ( +"Variable do_preconfigure value changed", +'+ print("this changes the task signature")') + expected_difftmp_output = expected_output + + self.run_test_printdiff_changerecipe("gcc-runtime", "gcc-source", "-c do_preconfigure {}".format(gcc_source_pn), +""" +python do_preconfigure:append() { + print("this changes the task signature") +} +""", +expected_sametmp_output, expected_difftmp_output) + + # Check if changing a really base task definiton is reported against multiple core recipes using it + def test_image_minimal_vs_base_do_configure(self): + change_bbtasks = ('zstd-native:do_configure', +'texinfo-dummy-native:do_configure', +'ldconfig-native:do_configure', +'gettext-minimal-native:do_configure', +'tzcode-native:do_configure', +'makedevs-native:do_configure', +'pigz-native:do_configure', +'update-rc.d-native:do_configure', +'unzip-native:do_configure', +'gnu-config-native:do_configure') + + expected_output = ["Task {} couldn't be used from the cache because:".format(t) for t in change_bbtasks] + [ +"We need hash", +"most recent matching task was"] + + expected_sametmp_output = expected_output + [ +"Variable base_do_configure value changed", +'+ echo "this changes base_do_configure() definiton "'] + expected_difftmp_output = expected_output + + self.run_test_printdiff_changeconfig("core-image-minimal",change_bbtasks, +""" +INHERIT += "base-do-configure-modified" +""", +expected_sametmp_output, expected_difftmp_output) + +class SStateCheckObjectPresence(SStateBase): + def check_bb_output(self, output, targets, exceptions, check_cdn): + def is_exception(object, exceptions): + for e in exceptions: + if re.search(e, object): + return True + return False + + # sstate is checked for existence of these, but they never get written out to begin with + exceptions += ["{}.*image_qa".format(t) for t in targets.split()] + exceptions += ["{}.*deploy_source_date_epoch".format(t) for t in targets.split()] + exceptions += ["{}.*image_complete".format(t) for t in targets.split()] + exceptions += ["linux-yocto.*shared_workdir"] + # these get influnced by IMAGE_FSTYPES tweaks in yocto-autobuilder-helper's config.json (on x86-64) + # additionally, they depend on noexec (thus, absent stamps) package, install, etc. image tasks, + # which makes tracing other changes difficult + exceptions += ["{}.*create_.*spdx".format(t) for t in targets.split()] + + output_l = output.splitlines() + for l in output_l: + if l.startswith("Sstate summary"): + for idx, item in enumerate(l.split()): + if item == 'Missed': + missing_objects = int(l.split()[idx+1]) + break + else: + self.fail("Did not find missing objects amount in sstate summary: {}".format(l)) + break + else: + self.fail("Did not find 'Sstate summary' line in bitbake output") + + failed_urls = [] + failed_urls_extrainfo = [] + for l in output_l: + if "SState: Unsuccessful fetch test for" in l and check_cdn: + missing_object = l.split()[6] + elif "SState: Looked for but didn't find file" in l and not check_cdn: + missing_object = l.split()[8] + else: + missing_object = None + if missing_object: + if not is_exception(missing_object, exceptions): + failed_urls.append(missing_object) + else: + missing_objects -= 1 + + if "urlopen failed for" in l and not is_exception(l, exceptions): + failed_urls_extrainfo.append(l) + + self.assertEqual(len(failed_urls), missing_objects, "Amount of reported missing objects does not match failed URLs: {}\nFailed URLs:\n{}\nFetcher diagnostics:\n{}".format(missing_objects, "\n".join(failed_urls), "\n".join(failed_urls_extrainfo))) + self.assertEqual(len(failed_urls), 0, "Missing objects in the cache:\n{}\nFetcher diagnostics:\n{}".format("\n".join(failed_urls), "\n".join(failed_urls_extrainfo))) + +@OETestTag("yocto-mirrors") +class SStateMirrors(SStateCheckObjectPresence): + def run_test(self, machine, targets, exceptions, check_cdn = True, ignore_errors = False): + if check_cdn: + self.config_sstate(True) + self.append_config(""" +MACHINE = "{}" +BB_HASHSERVE_UPSTREAM = "hashserv.yocto.io:8687" +SSTATE_MIRRORS ?= "file://.* http://cdn.jsdelivr.net/yocto/sstate/all/PATH;downloadfilename=PATH" +""".format(machine)) + else: + self.append_config(""" +MACHINE = "{}" +""".format(machine)) + result = bitbake("-DD -n {}".format(targets)) + bitbake("-S none {}".format(targets)) + if ignore_errors: + return + self.check_bb_output(result.output, targets, exceptions, check_cdn) + + def test_cdn_mirror_qemux86_64(self): + exceptions = [] + self.run_test("qemux86-64", "core-image-minimal core-image-full-cmdline core-image-sato-sdk", exceptions, ignore_errors = True) + self.run_test("qemux86-64", "core-image-minimal core-image-full-cmdline core-image-sato-sdk", exceptions) + + def test_cdn_mirror_qemuarm64(self): + exceptions = [] + self.run_test("qemuarm64", "core-image-minimal core-image-full-cmdline core-image-sato-sdk", exceptions, ignore_errors = True) + self.run_test("qemuarm64", "core-image-minimal core-image-full-cmdline core-image-sato-sdk", exceptions) + + def test_local_cache_qemux86_64(self): + exceptions = [] + self.run_test("qemux86-64", "core-image-minimal core-image-full-cmdline core-image-sato-sdk", exceptions, check_cdn = False) + + def test_local_cache_qemuarm64(self): + exceptions = [] + self.run_test("qemuarm64", "core-image-minimal core-image-full-cmdline core-image-sato-sdk", exceptions, check_cdn = False) diff --git a/meta/lib/oeqa/selftest/cases/sysroot.py b/meta/lib/oeqa/selftest/cases/sysroot.py new file mode 100644 index 0000000000..ef854f6fee --- /dev/null +++ b/meta/lib/oeqa/selftest/cases/sysroot.py @@ -0,0 +1,86 @@ +# +# Copyright OpenEmbedded Contributors +# +# SPDX-License-Identifier: MIT +# + +import uuid + +from oeqa.selftest.case import OESelftestTestCase +from oeqa.utils.commands import bitbake + +class SysrootTests(OESelftestTestCase): + def test_sysroot_cleanup(self): + """ + Build sysroot test which depends on virtual/sysroot-test for one machine, + switch machine, switch provider of virtual/sysroot-test and check that the + sysroot is correctly cleaned up. The files in the two providers overlap + so can cause errors if the sysroot code doesn't function correctly. + Yes, sysroot-test should be machine specific really to avoid this, however + the sysroot cleanup should also work [YOCTO #13702]. + """ + + uuid1 = uuid.uuid4() + uuid2 = uuid.uuid4() + + self.write_config(""" +PREFERRED_PROVIDER_virtual/sysroot-test = "sysroot-test-arch1" +MACHINE = "qemux86" +TESTSTRING:pn-sysroot-test-arch1 = "%s" +TESTSTRING:pn-sysroot-test-arch2 = "%s" +""" % (uuid1, uuid2)) + bitbake("sysroot-test") + self.write_config(""" +PREFERRED_PROVIDER_virtual/sysroot-test = "sysroot-test-arch2" +MACHINE = "qemux86copy" +TESTSTRING:pn-sysroot-test-arch1 = "%s" +TESTSTRING:pn-sysroot-test-arch2 = "%s" +""" % (uuid1, uuid2)) + bitbake("sysroot-test") + + def test_sysroot_max_shebang(self): + """ + Summary: Check max shebang triggers. To confirm [YOCTO #11053] is closed. + Expected: Fail when a shebang bigger than the max shebang-size is reached. + Author: Paulo Neves <ptsneves@gmail.com> + """ + expected = "maximum shebang size exceeded, the maximum size is 128. [shebang-size]" + res = bitbake("sysroot-shebang-test-native -c populate_sysroot", ignore_status=True) + self.assertTrue(expected in res.output, msg=res.output) + self.assertTrue(res.status != 0) + + def test_sysroot_la(self): + """ + Summary: Check that workdir paths are not contained in .la files. + Expected: Fail when a workdir path is found in the file content. + Author: Paulo Neves <ptsneves@gmail.com> + """ + expected = "la-test.la failed sanity test (workdir) in path" + + res = bitbake("sysroot-la-test -c populate_sysroot", ignore_status=True) + self.assertTrue(expected in res.output, msg=res.output) + self.assertTrue('[la]' in res.output, msg=res.output) + self.assertTrue(res.status != 0) + + res = bitbake("sysroot-la-test-native -c populate_sysroot", ignore_status=True) + self.assertTrue(expected in res.output, msg=res.output) + self.assertTrue('[la]' in res.output, msg=res.output) + self.assertTrue(res.status != 0) + + def test_sysroot_pkgconfig(self): + """ + Summary: Check that tmpdir paths are not contained in .pc files. + Expected: Fail when a tmpdir path is found in the file content. + Author: Paulo Neves <ptsneves@gmail.com> + """ + expected = "test.pc failed sanity test (tmpdir) in path" + + res = bitbake("sysroot-pc-test -c populate_sysroot", ignore_status=True) + self.assertTrue('[pkgconfig]' in res.output, msg=res.output) + self.assertTrue(expected in res.output, msg=res.output) + self.assertTrue(res.status != 0) + + res = bitbake("sysroot-pc-test-native -c populate_sysroot", ignore_status=True) + self.assertTrue(expected in res.output, msg=res.output) + self.assertTrue('[pkgconfig]' in res.output, msg=res.output) + self.assertTrue(res.status != 0) diff --git a/meta/lib/oeqa/selftest/cases/tinfoil.py b/meta/lib/oeqa/selftest/cases/tinfoil.py index 42a1b6b4f4..21c8686b2a 100644 --- a/meta/lib/oeqa/selftest/cases/tinfoil.py +++ b/meta/lib/oeqa/selftest/cases/tinfoil.py @@ -1,4 +1,6 @@ # +# Copyright OpenEmbedded Contributors +# # SPDX-License-Identifier: MIT # @@ -9,7 +11,6 @@ import logging import bb.tinfoil from oeqa.selftest.case import OESelftestTestCase -from oeqa.utils.commands import runCmd class TinfoilTests(OESelftestTestCase): """ Basic tests for the tinfoil API """ @@ -47,6 +48,17 @@ class TinfoilTests(OESelftestTestCase): rd = tinfoil.parse_recipe_file(best[3]) self.assertEqual(testrecipe, rd.getVar('PN')) + def test_parse_virtual_recipe(self): + with bb.tinfoil.Tinfoil() as tinfoil: + tinfoil.prepare(config_only=False, quiet=2) + testrecipe = 'nativesdk-gcc' + best = tinfoil.find_best_provider(testrecipe) + if not best: + self.fail('Unable to find recipe providing %s' % testrecipe) + rd = tinfoil.parse_recipe_file(best[3]) + self.assertEqual(testrecipe, rd.getVar('PN')) + self.assertIsNotNone(rd.getVar('FILE_LAYERNAME')) + def test_parse_recipe_copy_expand(self): with bb.tinfoil.Tinfoil() as tinfoil: tinfoil.prepare(config_only=False, quiet=2) @@ -65,18 +77,31 @@ class TinfoilTests(OESelftestTestCase): localdata.setVar('PN', 'hello') self.assertEqual('hello', localdata.getVar('BPN')) - def test_parse_recipe_initial_datastore(self): + # The config_data API to parse_recipe_file is used by: + # layerindex-web layerindex/update_layer.py + def test_parse_recipe_custom_data(self): with bb.tinfoil.Tinfoil() as tinfoil: tinfoil.prepare(config_only=False, quiet=2) + localdata = bb.data.createCopy(tinfoil.config_data) + localdata.setVar("TESTVAR", "testval") testrecipe = 'mdadm' best = tinfoil.find_best_provider(testrecipe) if not best: self.fail('Unable to find recipe providing %s' % testrecipe) - dcopy = bb.data.createCopy(tinfoil.config_data) - dcopy.setVar('MYVARIABLE', 'somevalue') - rd = tinfoil.parse_recipe_file(best[3], config_data=dcopy) - # Check we can get variable values - self.assertEqual('somevalue', rd.getVar('MYVARIABLE')) + rd = tinfoil.parse_recipe_file(best[3], config_data=localdata) + self.assertEqual("testval", rd.getVar('TESTVAR')) + + def test_parse_virtual_recipe_custom_data(self): + with bb.tinfoil.Tinfoil() as tinfoil: + tinfoil.prepare(config_only=False, quiet=2) + localdata = bb.data.createCopy(tinfoil.config_data) + localdata.setVar("TESTVAR", "testval") + testrecipe = 'nativesdk-gcc' + best = tinfoil.find_best_provider(testrecipe) + if not best: + self.fail('Unable to find recipe providing %s' % testrecipe) + rd = tinfoil.parse_recipe_file(best[3], config_data=localdata) + self.assertEqual("testval", rd.getVar('TESTVAR')) def test_list_recipes(self): with bb.tinfoil.Tinfoil() as tinfoil: @@ -100,35 +125,38 @@ class TinfoilTests(OESelftestTestCase): with bb.tinfoil.Tinfoil() as tinfoil: tinfoil.prepare(config_only=True) - tinfoil.set_event_mask(['bb.event.FilesMatchingFound', 'bb.command.CommandCompleted']) + tinfoil.set_event_mask(['bb.event.FilesMatchingFound', 'bb.command.CommandCompleted', 'bb.command.CommandFailed', 'bb.command.CommandExit']) # Need to drain events otherwise events that were masked may still be in the queue while tinfoil.wait_event(): pass pattern = 'conf' - res = tinfoil.run_command('findFilesMatchingInDir', pattern, 'conf/machine') + res = tinfoil.run_command('testCookerCommandEvent', pattern, handle_events=False) self.assertTrue(res) eventreceived = False commandcomplete = False start = time.time() - # Wait for 5s in total so we'd detect spurious heartbeat events for example - while time.time() - start < 5: + # Wait for maximum 60s in total so we'd detect spurious heartbeat events for example + while (not (eventreceived == True and commandcomplete == True) + and (time.time() - start < 60)): + # if we received both events (on let's say a good day), we are done event = tinfoil.wait_event(1) if event: if isinstance(event, bb.command.CommandCompleted): commandcomplete = True elif isinstance(event, bb.event.FilesMatchingFound): self.assertEqual(pattern, event._pattern) - self.assertIn('qemuarm.conf', event._matches) + self.assertIn('A', event._matches) + self.assertIn('B', event._matches) eventreceived = True elif isinstance(event, logging.LogRecord): continue else: self.fail('Unexpected event: %s' % event) - self.assertTrue(commandcomplete, 'Timed out waiting for CommandCompleted event from bitbake server') + self.assertTrue(commandcomplete, 'Timed out waiting for CommandCompleted event from bitbake server (Matching event received: %s)' % str(eventreceived)) self.assertTrue(eventreceived, 'Did not receive FilesMatchingFound event from bitbake server') def test_setvariable_clean(self): @@ -183,8 +211,8 @@ class TinfoilTests(OESelftestTestCase): self.assertEqual(value, 'origvalue', 'Variable renamed using config_data.renameVar() does not appear with new name') # Test overrides tinfoil.config_data.setVar('TESTVAR', 'original') - tinfoil.config_data.setVar('TESTVAR_overrideone', 'one') - tinfoil.config_data.setVar('TESTVAR_overridetwo', 'two') + tinfoil.config_data.setVar('TESTVAR:overrideone', 'one') + tinfoil.config_data.setVar('TESTVAR:overridetwo', 'two') tinfoil.config_data.appendVar('OVERRIDES', ':overrideone') value = tinfoil.config_data.getVar('TESTVAR') self.assertEqual(value, 'one', 'Variable overrides not functioning correctly') diff --git a/meta/lib/oeqa/selftest/cases/usergrouptests.py b/meta/lib/oeqa/selftest/cases/usergrouptests.py new file mode 100644 index 0000000000..3c59b0f290 --- /dev/null +++ b/meta/lib/oeqa/selftest/cases/usergrouptests.py @@ -0,0 +1,57 @@ +# +# Copyright OpenEmbedded Contributors +# +# SPDX-License-Identifier: MIT +# + +import os +import shutil +from oeqa.selftest.case import OESelftestTestCase +from oeqa.utils.commands import bitbake +from oeqa.utils.commands import bitbake, get_bb_var, get_test_layer + +class UserGroupTests(OESelftestTestCase): + def test_group_from_dep_package(self): + self.logger.info("Building creategroup2") + bitbake(' creategroup2 creategroup1') + bitbake(' creategroup2 creategroup1 -c clean') + self.logger.info("Packaging creategroup2") + self.assertTrue(bitbake(' creategroup2 -c package')) + + def test_add_task_between_p_sysroot_and_package(self): + # Test for YOCTO #14961 + self.assertTrue(bitbake('useraddbadtask -C fetch')) + + def test_postinst_order(self): + self.logger.info("Building dcreategroup") + self.assertTrue(bitbake(' dcreategroup')) + + def test_static_useradd_from_dynamic(self): + metaselftestpath = get_test_layer() + self.logger.info("Building core-image-minimal to generate passwd/group file") + bitbake(' core-image-minimal') + self.logger.info("Setting up useradd-staticids") + repropassdir = os.path.join(metaselftestpath, "conf/include") + os.makedirs(repropassdir) + etcdir=os.path.join(os.path.join(os.path.join(get_bb_var("TMPDIR"), "work"), \ + os.path.join(get_bb_var("MACHINE").replace("-","_")+"-poky-linux", "core-image-minimal/1.0/rootfs/etc"))) + shutil.copy(os.path.join(etcdir, "passwd"), os.path.join(repropassdir, "reproducable-passwd")) + shutil.copy(os.path.join(etcdir, "group"), os.path.join(repropassdir, "reproducable-group")) + # Copy the original local.conf + shutil.copyfile(os.path.join(os.environ.get('BUILDDIR'), 'conf/local.conf'), os.path.join(os.environ.get('BUILDDIR'), 'conf/local.conf.orig')) + + self.write_config("USERADDEXTENSION = \"useradd-staticids\"") + self.write_config("USERADD_ERROR_DYNAMIC ??= \"error\"") + self.write_config("USERADD_UID_TABLES += \"conf/include/reproducible-passwd\"") + self.write_config("USERADD_GID_TABLES += \"conf/include/reproducible-group\"") + self.logger.info("Rebuild with staticids") + bitbake(' core-image-minimal') + shutil.copyfile(os.path.join(os.environ.get('BUILDDIR'), 'conf/local.conf.orig'), os.path.join(os.environ.get('BUILDDIR'), 'conf/local.conf')) + self.logger.info("Rebuild without staticids") + bitbake(' core-image-minimal') + self.write_config("USERADDEXTENSION = \"useradd-staticids\"") + self.write_config("USERADD_ERROR_DYNAMIC ??= \"error\"") + self.write_config("USERADD_UID_TABLES += \"files/static-passwd\"") + self.write_config("USERADD_GID_TABLES += \"files/static-group\"") + self.logger.info("Rebuild with other staticids") + self.assertTrue(bitbake(' core-image-minimal')) diff --git a/meta/lib/oeqa/selftest/cases/wic.py b/meta/lib/oeqa/selftest/cases/wic.py index 3c5be2f501..b616759209 100644 --- a/meta/lib/oeqa/selftest/cases/wic.py +++ b/meta/lib/oeqa/selftest/cases/wic.py @@ -11,39 +11,43 @@ import os import sys import unittest +import hashlib from glob import glob from shutil import rmtree, copy -from functools import wraps, lru_cache from tempfile import NamedTemporaryFile +from tempfile import TemporaryDirectory from oeqa.selftest.case import OESelftestTestCase +from oeqa.core.decorator import OETestTag +from oeqa.core.decorator.data import skipIfNotArch from oeqa.utils.commands import runCmd, bitbake, get_bb_var, get_bb_vars, runqemu -@lru_cache(maxsize=32) -def get_host_arch(recipe): - """A cached call to get_bb_var('HOST_ARCH', <recipe>)""" - return get_bb_var('HOST_ARCH', recipe) - - -def only_for_arch(archs, image='core-image-minimal'): - """Decorator for wrapping test cases that can be run only for specific target - architectures. A list of compatible architectures is passed in `archs`. - Current architecture will be determined by parsing bitbake output for - `image` recipe. +def extract_files(debugfs_output): """ - def wrapper(func): - @wraps(func) - def wrapped_f(*args, **kwargs): - arch = get_host_arch(image) - if archs and arch not in archs: - raise unittest.SkipTest("Testcase arch dependency not met: %s" % arch) - return func(*args, **kwargs) - wrapped_f.__name__ = func.__name__ - return wrapped_f - return wrapper + extract file names from the output of debugfs -R 'ls -p', + which looks like this: + + /2/040755/0/0/.//\n + /2/040755/0/0/..//\n + /11/040700/0/0/lost+found^M//\n + /12/040755/1002/1002/run//\n + /13/040755/1002/1002/sys//\n + /14/040755/1002/1002/bin//\n + /80/040755/1002/1002/var//\n + /92/040755/1002/1002/tmp//\n + """ + # NOTE the occasional ^M in file names + return [line.split('/')[5].strip() for line in \ + debugfs_output.strip().split('/\n')] +def files_own_by_root(debugfs_output): + for line in debugfs_output.strip().split('/\n'): + if line.split('/')[3:5] != ['0', '0']: + print(debugfs_output) + return False + return True class WicTestCase(OESelftestTestCase): """Wic test class.""" @@ -53,21 +57,18 @@ class WicTestCase(OESelftestTestCase): def setUpLocal(self): """This code is executed before each test method.""" - self.resultdir = self.builddir + "/wic-tmp/" + self.resultdir = os.path.join(self.builddir, "wic-tmp") super(WicTestCase, self).setUpLocal() # Do this here instead of in setUpClass as the base setUp does some # clean up which can result in the native tools built earlier in # setUpClass being unavailable. if not WicTestCase.image_is_ready: - if get_bb_var('USE_NLS') == 'yes': - bitbake('wic-tools') - else: - self.skipTest('wic-tools cannot be built due its (intltool|gettext)-native dependency and NLS disable') + if self.td['USE_NLS'] != 'yes': + self.skipTest('wic-tools needs USE_NLS=yes') - bitbake('core-image-minimal') + bitbake('wic-tools core-image-minimal core-image-minimal-mtdutils') WicTestCase.image_is_ready = True - rmtree(self.resultdir, ignore_errors=True) def tearDownLocal(self): @@ -78,15 +79,13 @@ class WicTestCase(OESelftestTestCase): def _get_image_env_path(self, image): """Generate and obtain the path to <image>.env""" if image not in WicTestCase.wicenv_cache: - self.assertEqual(0, bitbake('%s -c do_rootfs_wicenv' % image).status) - bb_vars = get_bb_vars(['STAGING_DIR', 'MACHINE'], image) - stdir = bb_vars['STAGING_DIR'] - machine = bb_vars['MACHINE'] + bitbake('%s -c do_rootfs_wicenv' % image) + stdir = get_bb_var('STAGING_DIR', image) + machine = self.td["MACHINE"] WicTestCase.wicenv_cache[image] = os.path.join(stdir, machine, 'imgdata') return WicTestCase.wicenv_cache[image] -class Wic(WicTestCase): - +class CLITests(OESelftestTestCase): def test_version(self): """Test wic --version""" runCmd('wic --version') @@ -147,68 +146,147 @@ class Wic(WicTestCase): """Test wic without command""" self.assertEqual(1, runCmd('wic', ignore_status=True).status) +class Wic(WicTestCase): + def test_skip_kernel_install(self): + """Test the functionality of not installing the kernel in the boot directory using the wic plugin""" + # create a temporary file for the WKS content + with NamedTemporaryFile("w", suffix=".wks") as wks: + wks.write( + 'part --source bootimg-efi ' + '--sourceparams="loader=grub-efi,install-kernel-into-boot-dir=false" ' + '--label boot --active\n' + ) + wks.flush() + # create a temporary directory to extract the disk image to + with TemporaryDirectory() as tmpdir: + img = 'core-image-minimal' + # build the image using the WKS file + cmd = "wic create %s -e %s -o %s" % ( + wks.name, img, self.resultdir) + runCmd(cmd) + wksname = os.path.splitext(os.path.basename(wks.name))[0] + out = glob(os.path.join( + self.resultdir, "%s-*.direct" % wksname)) + self.assertEqual(1, len(out)) + sysroot = get_bb_var('RECIPE_SYSROOT_NATIVE', 'wic-tools') + # extract the content of the disk image to the temporary directory + cmd = "wic cp %s:1 %s -n %s" % (out[0], tmpdir, sysroot) + runCmd(cmd) + # check if the kernel is installed or not + kimgtype = get_bb_var('KERNEL_IMAGETYPE', img) + for file in os.listdir(tmpdir): + if file == kimgtype: + raise AssertionError( + "The kernel image '{}' was found in the partition".format(kimgtype) + ) + + def test_kernel_install(self): + """Test the installation of the kernel to the boot directory in the wic plugin""" + # create a temporary file for the WKS content + with NamedTemporaryFile("w", suffix=".wks") as wks: + wks.write( + 'part --source bootimg-efi ' + '--sourceparams="loader=grub-efi,install-kernel-into-boot-dir=true" ' + '--label boot --active\n' + ) + wks.flush() + # create a temporary directory to extract the disk image to + with TemporaryDirectory() as tmpdir: + img = 'core-image-minimal' + # build the image using the WKS file + cmd = "wic create %s -e %s -o %s" % (wks.name, img, self.resultdir) + runCmd(cmd) + wksname = os.path.splitext(os.path.basename(wks.name))[0] + out = glob(os.path.join(self.resultdir, "%s-*.direct" % wksname)) + self.assertEqual(1, len(out)) + sysroot = get_bb_var('RECIPE_SYSROOT_NATIVE', 'wic-tools') + # extract the content of the disk image to the temporary directory + cmd = "wic cp %s:1 %s -n %s" % (out[0], tmpdir, sysroot) + runCmd(cmd) + # check if the kernel is installed or not + kimgtype = get_bb_var('KERNEL_IMAGETYPE', img) + found = False + for file in os.listdir(tmpdir): + if file == kimgtype: + found = True + break + self.assertTrue( + found, "The kernel image '{}' was not found in the boot partition".format(kimgtype) + ) + def test_build_image_name(self): """Test wic create wictestdisk --image-name=core-image-minimal""" cmd = "wic create wictestdisk --image-name=core-image-minimal -o %s" % self.resultdir runCmd(cmd) - self.assertEqual(1, len(glob(self.resultdir + "wictestdisk-*.direct"))) + self.assertEqual(1, len(glob(os.path.join (self.resultdir, "wictestdisk-*.direct")))) - @only_for_arch(['i586', 'i686', 'x86_64']) + @skipIfNotArch(['i586', 'i686', 'x86_64']) def test_gpt_image(self): """Test creation of core-image-minimal with gpt table and UUID boot""" cmd = "wic create directdisk-gpt --image-name core-image-minimal -o %s" % self.resultdir runCmd(cmd) - self.assertEqual(1, len(glob(self.resultdir + "directdisk-*.direct"))) + self.assertEqual(1, len(glob(os.path.join(self.resultdir, "directdisk-*.direct")))) - @only_for_arch(['i586', 'i686', 'x86_64']) + @skipIfNotArch(['i586', 'i686', 'x86_64']) def test_iso_image(self): """Test creation of hybrid iso image with legacy and EFI boot""" config = 'INITRAMFS_IMAGE = "core-image-minimal-initramfs"\n'\ - 'MACHINE_FEATURES_append = " efi"\n'\ - 'DEPENDS_pn-core-image-minimal += "syslinux"\n' + 'MACHINE_FEATURES:append = " efi"\n'\ + 'DEPENDS:pn-core-image-minimal += "syslinux"\n' self.append_config(config) bitbake('core-image-minimal core-image-minimal-initramfs') self.remove_config(config) cmd = "wic create mkhybridiso --image-name core-image-minimal -o %s" % self.resultdir runCmd(cmd) - self.assertEqual(1, len(glob(self.resultdir + "HYBRID_ISO_IMG-*.direct"))) - self.assertEqual(1, len(glob(self.resultdir + "HYBRID_ISO_IMG-*.iso"))) + self.assertEqual(1, len(glob(os.path.join(self.resultdir, "HYBRID_ISO_IMG-*.direct")))) + self.assertEqual(1, len(glob(os.path.join (self.resultdir, "HYBRID_ISO_IMG-*.iso")))) - @only_for_arch(['i586', 'i686', 'x86_64']) + @skipIfNotArch(['i586', 'i686', 'x86_64']) def test_qemux86_directdisk(self): """Test creation of qemux-86-directdisk image""" cmd = "wic create qemux86-directdisk -e core-image-minimal -o %s" % self.resultdir runCmd(cmd) - self.assertEqual(1, len(glob(self.resultdir + "qemux86-directdisk-*direct"))) + self.assertEqual(1, len(glob(os.path.join(self.resultdir, "qemux86-directdisk-*direct")))) - @only_for_arch(['i586', 'i686', 'x86_64']) + @skipIfNotArch(['i586', 'i686', 'x86_64', 'aarch64']) def test_mkefidisk(self): """Test creation of mkefidisk image""" cmd = "wic create mkefidisk -e core-image-minimal -o %s" % self.resultdir runCmd(cmd) - self.assertEqual(1, len(glob(self.resultdir + "mkefidisk-*direct"))) + self.assertEqual(1, len(glob(os.path.join(self.resultdir, "mkefidisk-*direct")))) - @only_for_arch(['i586', 'i686', 'x86_64']) + @skipIfNotArch(['i586', 'i686', 'x86_64']) def test_bootloader_config(self): """Test creation of directdisk-bootloader-config image""" - config = 'DEPENDS_pn-core-image-minimal += "syslinux"\n' + config = 'DEPENDS:pn-core-image-minimal += "syslinux"\n' self.append_config(config) bitbake('core-image-minimal') self.remove_config(config) cmd = "wic create directdisk-bootloader-config -e core-image-minimal -o %s" % self.resultdir runCmd(cmd) - self.assertEqual(1, len(glob(self.resultdir + "directdisk-bootloader-config-*direct"))) + self.assertEqual(1, len(glob(os.path.join(self.resultdir, "directdisk-bootloader-config-*direct")))) - @only_for_arch(['i586', 'i686', 'x86_64']) + @skipIfNotArch(['i586', 'i686', 'x86_64', 'aarch64']) def test_systemd_bootdisk(self): """Test creation of systemd-bootdisk image""" - config = 'MACHINE_FEATURES_append = " efi"\n' + config = 'MACHINE_FEATURES:append = " efi"\n' self.append_config(config) bitbake('core-image-minimal') self.remove_config(config) cmd = "wic create systemd-bootdisk -e core-image-minimal -o %s" % self.resultdir runCmd(cmd) - self.assertEqual(1, len(glob(self.resultdir + "systemd-bootdisk-*direct"))) + self.assertEqual(1, len(glob(os.path.join(self.resultdir, "systemd-bootdisk-*direct")))) + + def test_efi_bootpart(self): + """Test creation of efi-bootpart image""" + cmd = "wic create mkefidisk -e core-image-minimal -o %s" % self.resultdir + kimgtype = get_bb_var('KERNEL_IMAGETYPE', 'core-image-minimal') + self.append_config('IMAGE_EFI_BOOT_FILES = "%s;kernel"\n' % kimgtype) + runCmd(cmd) + sysroot = get_bb_var('RECIPE_SYSROOT_NATIVE', 'wic-tools') + images = glob(os.path.join(self.resultdir, "mkefidisk-*.direct")) + result = runCmd("wic ls %s:1/ -n %s" % (images[0], sysroot)) + self.assertIn("kernel",result.output) def test_sdimage_bootpart(self): """Test creation of sdimage-bootpart image""" @@ -216,14 +294,15 @@ class Wic(WicTestCase): kimgtype = get_bb_var('KERNEL_IMAGETYPE', 'core-image-minimal') self.write_config('IMAGE_BOOT_FILES = "%s"\n' % kimgtype) runCmd(cmd) - self.assertEqual(1, len(glob(self.resultdir + "sdimage-bootpart-*direct"))) + self.assertEqual(1, len(glob(os.path.join(self.resultdir, "sdimage-bootpart-*direct")))) - @only_for_arch(['i586', 'i686', 'x86_64']) + # TODO this doesn't have to be x86-specific + @skipIfNotArch(['i586', 'i686', 'x86_64']) def test_default_output_dir(self): """Test default output location""" for fname in glob("directdisk-*.direct"): os.remove(fname) - config = 'DEPENDS_pn-core-image-minimal += "syslinux"\n' + config = 'DEPENDS:pn-core-image-minimal += "syslinux"\n' self.append_config(config) bitbake('core-image-minimal') self.remove_config(config) @@ -231,7 +310,7 @@ class Wic(WicTestCase): runCmd(cmd) self.assertEqual(1, len(glob("directdisk-*.direct"))) - @only_for_arch(['i586', 'i686', 'x86_64']) + @skipIfNotArch(['i586', 'i686', 'x86_64']) def test_build_artifacts(self): """Test wic create directdisk providing all artifacts.""" bb_vars = get_bb_vars(['STAGING_DATADIR', 'RECIPE_SYSROOT_NATIVE'], @@ -246,28 +325,28 @@ class Wic(WicTestCase): "-n %(recipe_sysroot_native)s " "-r %(image_rootfs)s " "-o %(resultdir)s" % bbvars) - self.assertEqual(1, len(glob(self.resultdir + "directdisk-*.direct"))) + self.assertEqual(1, len(glob(os.path.join(self.resultdir, "directdisk-*.direct")))) def test_compress_gzip(self): """Test compressing an image with gzip""" runCmd("wic create wictestdisk " "--image-name core-image-minimal " "-c gzip -o %s" % self.resultdir) - self.assertEqual(1, len(glob(self.resultdir + "wictestdisk-*.direct.gz"))) + self.assertEqual(1, len(glob(os.path.join(self.resultdir, "wictestdisk-*.direct.gz")))) def test_compress_bzip2(self): """Test compressing an image with bzip2""" runCmd("wic create wictestdisk " "--image-name=core-image-minimal " "-c bzip2 -o %s" % self.resultdir) - self.assertEqual(1, len(glob(self.resultdir + "wictestdisk-*.direct.bz2"))) + self.assertEqual(1, len(glob(os.path.join(self.resultdir, "wictestdisk-*.direct.bz2")))) def test_compress_xz(self): """Test compressing an image with xz""" runCmd("wic create wictestdisk " "--image-name=core-image-minimal " "--compress-with=xz -o %s" % self.resultdir) - self.assertEqual(1, len(glob(self.resultdir + "wictestdisk-*.direct.xz"))) + self.assertEqual(1, len(glob(os.path.join(self.resultdir, "wictestdisk-*.direct.xz")))) def test_wrong_compressor(self): """Test how wic breaks if wrong compressor is provided""" @@ -281,21 +360,23 @@ class Wic(WicTestCase): runCmd("wic create wictestdisk " "--image-name=core-image-minimal " "-D -o %s" % self.resultdir) - self.assertEqual(1, len(glob(self.resultdir + "wictestdisk-*.direct"))) + self.assertEqual(1, len(glob(os.path.join(self.resultdir, "wictestdisk-*.direct")))) + self.assertEqual(1, len(glob(os.path.join(self.resultdir, "tmp.wic*")))) def test_debug_long(self): """Test --debug option""" runCmd("wic create wictestdisk " "--image-name=core-image-minimal " "--debug -o %s" % self.resultdir) - self.assertEqual(1, len(glob(self.resultdir + "wictestdisk-*.direct"))) + self.assertEqual(1, len(glob(os.path.join(self.resultdir, "wictestdisk-*.direct")))) + self.assertEqual(1, len(glob(os.path.join(self.resultdir, "tmp.wic*")))) def test_skip_build_check_short(self): """Test -s option""" runCmd("wic create wictestdisk " "--image-name=core-image-minimal " "-s -o %s" % self.resultdir) - self.assertEqual(1, len(glob(self.resultdir + "wictestdisk-*.direct"))) + self.assertEqual(1, len(glob(os.path.join(self.resultdir, "wictestdisk-*.direct")))) def test_skip_build_check_long(self): """Test --skip-build-check option""" @@ -303,14 +384,14 @@ class Wic(WicTestCase): "--image-name=core-image-minimal " "--skip-build-check " "--outdir %s" % self.resultdir) - self.assertEqual(1, len(glob(self.resultdir + "wictestdisk-*.direct"))) + self.assertEqual(1, len(glob(os.path.join(self.resultdir, "wictestdisk-*.direct")))) def test_build_rootfs_short(self): """Test -f option""" runCmd("wic create wictestdisk " "--image-name=core-image-minimal " "-f -o %s" % self.resultdir) - self.assertEqual(1, len(glob(self.resultdir + "wictestdisk-*.direct"))) + self.assertEqual(1, len(glob(os.path.join(self.resultdir, "wictestdisk-*.direct")))) def test_build_rootfs_long(self): """Test --build-rootfs option""" @@ -318,9 +399,10 @@ class Wic(WicTestCase): "--image-name=core-image-minimal " "--build-rootfs " "--outdir %s" % self.resultdir) - self.assertEqual(1, len(glob(self.resultdir + "wictestdisk-*.direct"))) + self.assertEqual(1, len(glob(os.path.join(self.resultdir, "wictestdisk-*.direct")))) - @only_for_arch(['i586', 'i686', 'x86_64']) + # TODO this doesn't have to be x86-specific + @skipIfNotArch(['i586', 'i686', 'x86_64']) def test_rootfs_indirect_recipes(self): """Test usage of rootfs plugin with rootfs recipes""" runCmd("wic create directdisk-multi-rootfs " @@ -328,9 +410,10 @@ class Wic(WicTestCase): "--rootfs rootfs1=core-image-minimal " "--rootfs rootfs2=core-image-minimal " "--outdir %s" % self.resultdir) - self.assertEqual(1, len(glob(self.resultdir + "directdisk-multi-rootfs*.direct"))) + self.assertEqual(1, len(glob(os.path.join(self.resultdir, "directdisk-multi-rootfs*.direct")))) - @only_for_arch(['i586', 'i686', 'x86_64']) + # TODO this doesn't have to be x86-specific + @skipIfNotArch(['i586', 'i686', 'x86_64']) def test_rootfs_artifacts(self): """Test usage of rootfs plugin with rootfs paths""" bb_vars = get_bb_vars(['STAGING_DATADIR', 'RECIPE_SYSROOT_NATIVE'], @@ -347,7 +430,7 @@ class Wic(WicTestCase): "--rootfs-dir rootfs1=%(image_rootfs)s " "--rootfs-dir rootfs2=%(image_rootfs)s " "--outdir %(resultdir)s" % bbvars) - self.assertEqual(1, len(glob(self.resultdir + "%(wks)s-*.direct" % bbvars))) + self.assertEqual(1, len(glob(os.path.join(self.resultdir, "%(wks)s-*.direct" % bbvars)))) def test_exclude_path(self): """Test --exclude-path wks option.""" @@ -368,7 +451,7 @@ part /etc --source rootfs --ondisk mmcblk0 --fstype=ext4 --exclude-path bin/ --r % (wks_file, self.resultdir)) os.remove(wks_file) - wicout = glob(self.resultdir + "%s-*direct" % 'temp') + wicout = glob(os.path.join(self.resultdir, "%s-*direct" % 'temp')) self.assertEqual(1, len(wicout)) wicimg = wicout[0] @@ -393,24 +476,6 @@ part /etc --source rootfs --ondisk mmcblk0 --fstype=ext4 --exclude-path bin/ --r runCmd("dd if=%s of=%s skip=%d count=%d" % (wicimg, part_file, start, length)) - def extract_files(debugfs_output): - """ - extract file names from the output of debugfs -R 'ls -p', - which looks like this: - - /2/040755/0/0/.//\n - /2/040755/0/0/..//\n - /11/040700/0/0/lost+found^M//\n - /12/040755/1002/1002/run//\n - /13/040755/1002/1002/sys//\n - /14/040755/1002/1002/bin//\n - /80/040755/1002/1002/var//\n - /92/040755/1002/1002/tmp//\n - """ - # NOTE the occasional ^M in file names - return [line.split('/')[5].strip() for line in \ - debugfs_output.strip().split('/\n')] - # Test partition 1, should contain the normal root directories, except # /usr. res = runCmd("debugfs -R 'ls -p' %s 2>/dev/null" % \ @@ -451,6 +516,104 @@ part /etc --source rootfs --ondisk mmcblk0 --fstype=ext4 --exclude-path bin/ --r finally: os.environ['PATH'] = oldpath + def test_include_path(self): + """Test --include-path wks option.""" + + oldpath = os.environ['PATH'] + os.environ['PATH'] = get_bb_var("PATH", "wic-tools") + + try: + include_path = os.path.join(self.resultdir, 'test-include') + os.makedirs(include_path) + with open(os.path.join(include_path, 'test-file'), 'w') as t: + t.write("test\n") + wks_file = os.path.join(include_path, 'temp.wks') + with open(wks_file, 'w') as wks: + rootfs_dir = get_bb_var('IMAGE_ROOTFS', 'core-image-minimal') + wks.write(""" +part /part1 --source rootfs --ondisk mmcblk0 --fstype=ext4 +part /part2 --source rootfs --ondisk mmcblk0 --fstype=ext4 --include-path %s""" + % (include_path)) + runCmd("wic create %s -e core-image-minimal -o %s" \ + % (wks_file, self.resultdir)) + + part1 = glob(os.path.join(self.resultdir, 'temp-*.direct.p1'))[0] + part2 = glob(os.path.join(self.resultdir, 'temp-*.direct.p2'))[0] + + # Test partition 1, should not contain 'test-file' + res = runCmd("debugfs -R 'ls -p' %s 2>/dev/null" % (part1)) + files = extract_files(res.output) + self.assertNotIn('test-file', files) + self.assertEqual(True, files_own_by_root(res.output)) + + # Test partition 2, should contain 'test-file' + res = runCmd("debugfs -R 'ls -p' %s 2>/dev/null" % (part2)) + files = extract_files(res.output) + self.assertIn('test-file', files) + self.assertEqual(True, files_own_by_root(res.output)) + + finally: + os.environ['PATH'] = oldpath + + def test_include_path_embeded(self): + """Test --include-path wks option.""" + + oldpath = os.environ['PATH'] + os.environ['PATH'] = get_bb_var("PATH", "wic-tools") + + try: + include_path = os.path.join(self.resultdir, 'test-include') + os.makedirs(include_path) + with open(os.path.join(include_path, 'test-file'), 'w') as t: + t.write("test\n") + wks_file = os.path.join(include_path, 'temp.wks') + with open(wks_file, 'w') as wks: + wks.write(""" +part / --source rootfs --fstype=ext4 --include-path %s --include-path core-image-minimal-mtdutils export/""" + % (include_path)) + runCmd("wic create %s -e core-image-minimal -o %s" \ + % (wks_file, self.resultdir)) + + part1 = glob(os.path.join(self.resultdir, 'temp-*.direct.p1'))[0] + + res = runCmd("debugfs -R 'ls -p' %s 2>/dev/null" % (part1)) + files = extract_files(res.output) + self.assertIn('test-file', files) + self.assertEqual(True, files_own_by_root(res.output)) + + res = runCmd("debugfs -R 'ls -p /export/etc/' %s 2>/dev/null" % (part1)) + files = extract_files(res.output) + self.assertIn('passwd', files) + self.assertEqual(True, files_own_by_root(res.output)) + + finally: + os.environ['PATH'] = oldpath + + def test_include_path_errors(self): + """Test --include-path wks option error handling.""" + wks_file = 'temp.wks' + + # Absolute argument. + with open(wks_file, 'w') as wks: + wks.write("part / --source rootfs --fstype=ext4 --include-path core-image-minimal-mtdutils /export") + self.assertNotEqual(0, runCmd("wic create %s -e core-image-minimal -o %s" \ + % (wks_file, self.resultdir), ignore_status=True).status) + os.remove(wks_file) + + # Argument pointing to parent directory. + with open(wks_file, 'w') as wks: + wks.write("part / --source rootfs --fstype=ext4 --include-path core-image-minimal-mtdutils ././..") + self.assertNotEqual(0, runCmd("wic create %s -e core-image-minimal -o %s" \ + % (wks_file, self.resultdir), ignore_status=True).status) + os.remove(wks_file) + + # 3 Argument pointing to parent directory. + with open(wks_file, 'w') as wks: + wks.write("part / --source rootfs --fstype=ext4 --include-path core-image-minimal-mtdutils export/ dummy") + self.assertNotEqual(0, runCmd("wic create %s -e core-image-minimal -o %s" \ + % (wks_file, self.resultdir), ignore_status=True).status) + os.remove(wks_file) + def test_exclude_path_errors(self): """Test --exclude-path wks option error handling.""" wks_file = 'temp.wks' @@ -469,21 +632,229 @@ part /etc --source rootfs --ondisk mmcblk0 --fstype=ext4 --exclude-path bin/ --r % (wks_file, self.resultdir), ignore_status=True).status) os.remove(wks_file) + def test_permissions(self): + """Test permissions are respected""" + + # prepare wicenv and rootfs + bitbake('core-image-minimal core-image-minimal-mtdutils -c do_rootfs_wicenv') + + oldpath = os.environ['PATH'] + os.environ['PATH'] = get_bb_var("PATH", "wic-tools") + + t_normal = """ +part / --source rootfs --fstype=ext4 +""" + t_exclude = """ +part / --source rootfs --fstype=ext4 --exclude-path=home +""" + t_multi = """ +part / --source rootfs --ondisk sda --fstype=ext4 +part /export --source rootfs --rootfs=core-image-minimal-mtdutils --fstype=ext4 +""" + t_change = """ +part / --source rootfs --ondisk sda --fstype=ext4 --exclude-path=etc/  +part /etc --source rootfs --fstype=ext4 --change-directory=etc +""" + tests = [t_normal, t_exclude, t_multi, t_change] + + try: + for test in tests: + include_path = os.path.join(self.resultdir, 'test-include') + os.makedirs(include_path) + wks_file = os.path.join(include_path, 'temp.wks') + with open(wks_file, 'w') as wks: + wks.write(test) + runCmd("wic create %s -e core-image-minimal -o %s" \ + % (wks_file, self.resultdir)) + + for part in glob(os.path.join(self.resultdir, 'temp-*.direct.p*')): + res = runCmd("debugfs -R 'ls -p' %s 2>/dev/null" % (part)) + self.assertEqual(True, files_own_by_root(res.output)) + + config = 'IMAGE_FSTYPES += "wic"\nWKS_FILE = "%s"\n' % wks_file + self.append_config(config) + bitbake('core-image-minimal') + tmpdir = os.path.join(get_bb_var('WORKDIR', 'core-image-minimal'),'build-wic') + + # check each partition for permission + for part in glob(os.path.join(tmpdir, 'temp-*.direct.p*')): + res = runCmd("debugfs -R 'ls -p' %s 2>/dev/null" % (part)) + self.assertTrue(files_own_by_root(res.output) + ,msg='Files permission incorrect using wks set "%s"' % test) + + # clean config and result directory for next cases + self.remove_config(config) + rmtree(self.resultdir, ignore_errors=True) + + finally: + os.environ['PATH'] = oldpath + + def test_change_directory(self): + """Test --change-directory wks option.""" + + oldpath = os.environ['PATH'] + os.environ['PATH'] = get_bb_var("PATH", "wic-tools") + + try: + include_path = os.path.join(self.resultdir, 'test-include') + os.makedirs(include_path) + wks_file = os.path.join(include_path, 'temp.wks') + with open(wks_file, 'w') as wks: + wks.write("part /etc --source rootfs --fstype=ext4 --change-directory=etc") + runCmd("wic create %s -e core-image-minimal -o %s" \ + % (wks_file, self.resultdir)) + + part1 = glob(os.path.join(self.resultdir, 'temp-*.direct.p1'))[0] + + res = runCmd("debugfs -R 'ls -p' %s 2>/dev/null" % (part1)) + files = extract_files(res.output) + self.assertIn('passwd', files) + + finally: + os.environ['PATH'] = oldpath + + def test_change_directory_errors(self): + """Test --change-directory wks option error handling.""" + wks_file = 'temp.wks' + + # Absolute argument. + with open(wks_file, 'w') as wks: + wks.write("part / --source rootfs --fstype=ext4 --change-directory /usr") + self.assertNotEqual(0, runCmd("wic create %s -e core-image-minimal -o %s" \ + % (wks_file, self.resultdir), ignore_status=True).status) + os.remove(wks_file) + + # Argument pointing to parent directory. + with open(wks_file, 'w') as wks: + wks.write("part / --source rootfs --fstype=ext4 --change-directory ././..") + self.assertNotEqual(0, runCmd("wic create %s -e core-image-minimal -o %s" \ + % (wks_file, self.resultdir), ignore_status=True).status) + os.remove(wks_file) + + def test_no_fstab_update(self): + """Test --no-fstab-update wks option.""" + + oldpath = os.environ['PATH'] + os.environ['PATH'] = get_bb_var("PATH", "wic-tools") + + # Get stock fstab from base-files recipe + bitbake('base-files -c do_install') + bf_fstab = os.path.join(get_bb_var('D', 'base-files'), 'etc', 'fstab') + self.assertEqual(True, os.path.exists(bf_fstab)) + bf_fstab_md5sum = runCmd('md5sum %s 2>/dev/null' % bf_fstab).output.split(" ")[0] + + try: + no_fstab_update_path = os.path.join(self.resultdir, 'test-no-fstab-update') + os.makedirs(no_fstab_update_path) + wks_file = os.path.join(no_fstab_update_path, 'temp.wks') + with open(wks_file, 'w') as wks: + wks.writelines(['part / --source rootfs --fstype=ext4 --label rootfs\n', + 'part /mnt/p2 --source rootfs --rootfs-dir=core-image-minimal ', + '--fstype=ext4 --label p2 --no-fstab-update\n']) + runCmd("wic create %s -e core-image-minimal -o %s" \ + % (wks_file, self.resultdir)) + + part_fstab_md5sum = [] + for i in range(1, 3): + part = glob(os.path.join(self.resultdir, 'temp-*.direct.p') + str(i))[0] + part_fstab = runCmd("debugfs -R 'cat etc/fstab' %s 2>/dev/null" % (part)) + part_fstab_md5sum.append(hashlib.md5((part_fstab.output + "\n\n").encode('utf-8')).hexdigest()) + + # '/etc/fstab' in partition 2 should contain the same stock fstab file + # as the one installed by the base-file recipe. + self.assertEqual(bf_fstab_md5sum, part_fstab_md5sum[1]) + + # '/etc/fstab' in partition 1 should contain an updated fstab file. + self.assertNotEqual(bf_fstab_md5sum, part_fstab_md5sum[0]) + + finally: + os.environ['PATH'] = oldpath + + def test_no_fstab_update_errors(self): + """Test --no-fstab-update wks option error handling.""" + wks_file = 'temp.wks' + + # Absolute argument. + with open(wks_file, 'w') as wks: + wks.write("part / --source rootfs --fstype=ext4 --no-fstab-update /etc") + self.assertNotEqual(0, runCmd("wic create %s -e core-image-minimal -o %s" \ + % (wks_file, self.resultdir), ignore_status=True).status) + os.remove(wks_file) + + # Argument pointing to parent directory. + with open(wks_file, 'w') as wks: + wks.write("part / --source rootfs --fstype=ext4 --no-fstab-update ././..") + self.assertNotEqual(0, runCmd("wic create %s -e core-image-minimal -o %s" \ + % (wks_file, self.resultdir), ignore_status=True).status) + os.remove(wks_file) + + def test_extra_space(self): + """Test --extra-space wks option.""" + extraspace = 1024**3 + runCmd("wic create wictestdisk " + "--image-name core-image-minimal " + "--extra-space %i -o %s" % (extraspace ,self.resultdir)) + wicout = glob(os.path.join(self.resultdir, "wictestdisk-*.direct")) + self.assertEqual(1, len(wicout)) + size = os.path.getsize(wicout[0]) + self.assertTrue(size > extraspace, msg="Extra space not present (%s vs %s)" % (size, extraspace)) + + def test_no_table(self): + """Test --no-table wks option.""" + wks_file = 'temp.wks' + + # Absolute argument. + with open(wks_file, 'w') as wks: + wks.write("part testspace --no-table --fixed-size 16k --offset 4080k") + runCmd("wic create %s --image-name core-image-minimal -o %s" % (wks_file, self.resultdir)) + + wicout = glob(os.path.join(self.resultdir, "*.*")) + + self.assertEqual(1, len(wicout)) + size = os.path.getsize(wicout[0]) + self.assertEqual(size, 4 * 1024 * 1024) + + os.remove(wks_file) + + def test_partition_hidden_attributes(self): + """Test --hidden wks option.""" + wks_file = 'temp.wks' + sysroot = get_bb_var('RECIPE_SYSROOT_NATIVE', 'wic-tools') + try: + with open(wks_file, 'w') as wks: + wks.write(""" +part / --source rootfs --fstype=ext4 +part / --source rootfs --fstype=ext4 --hidden +bootloader --ptable gpt""") + + runCmd("wic create %s -e core-image-minimal -o %s" \ + % (wks_file, self.resultdir)) + wicout = os.path.join(self.resultdir, "*.direct") + + result = runCmd("%s/usr/sbin/sfdisk --part-attrs %s 1" % (sysroot, wicout)) + self.assertEqual('', result.output) + result = runCmd("%s/usr/sbin/sfdisk --part-attrs %s 2" % (sysroot, wicout)) + self.assertEqual('RequiredPartition', result.output) + + finally: + os.remove(wks_file) + + class Wic2(WicTestCase): def test_bmap_short(self): """Test generation of .bmap file -m option""" cmd = "wic create wictestdisk -e core-image-minimal -m -o %s" % self.resultdir runCmd(cmd) - self.assertEqual(1, len(glob(self.resultdir + "wictestdisk-*direct"))) - self.assertEqual(1, len(glob(self.resultdir + "wictestdisk-*direct.bmap"))) + self.assertEqual(1, len(glob(os.path.join(self.resultdir, "wictestdisk-*direct")))) + self.assertEqual(1, len(glob(os.path.join(self.resultdir, "wictestdisk-*direct.bmap")))) def test_bmap_long(self): """Test generation of .bmap file --bmap option""" cmd = "wic create wictestdisk -e core-image-minimal --bmap -o %s" % self.resultdir runCmd(cmd) - self.assertEqual(1, len(glob(self.resultdir + "wictestdisk-*direct"))) - self.assertEqual(1, len(glob(self.resultdir + "wictestdisk-*direct.bmap"))) + self.assertEqual(1, len(glob(os.path.join(self.resultdir, "wictestdisk-*direct")))) + self.assertEqual(1, len(glob(os.path.join(self.resultdir, "wictestdisk-*direct.bmap")))) def test_image_env(self): """Test generation of <image>.env files.""" @@ -494,19 +865,20 @@ class Wic2(WicTestCase): basename = bb_vars['IMAGE_BASENAME'] self.assertEqual(basename, image) path = os.path.join(imgdatadir, basename) + '.env' - self.assertTrue(os.path.isfile(path)) + self.assertTrue(os.path.isfile(path), msg="File %s wasn't generated as expected" % path) wicvars = set(bb_vars['WICVARS'].split()) # filter out optional variables wicvars = wicvars.difference(('DEPLOY_DIR_IMAGE', 'IMAGE_BOOT_FILES', 'INITRD', 'INITRD_LIVE', 'ISODIR','INITRAMFS_IMAGE', - 'INITRAMFS_IMAGE_BUNDLE', 'INITRAMFS_LINK_NAME')) + 'INITRAMFS_IMAGE_BUNDLE', 'INITRAMFS_LINK_NAME', + 'APPEND', 'IMAGE_EFI_BOOT_FILES')) with open(path) as envfile: content = dict(line.split("=", 1) for line in envfile) # test if variables used by wic present in the .env file for var in wicvars: self.assertTrue(var in content, "%s is not in .env file" % var) - self.assertTrue(content[var]) + self.assertTrue(content[var], "%s doesn't have a value (%s)" % (var, content[var])) def test_image_vars_dir_short(self): """Test image vars directory selection -v option""" @@ -518,7 +890,7 @@ class Wic2(WicTestCase): "--image-name=%s -v %s -n %s -o %s" % (image, imgenvdir, native_sysroot, self.resultdir)) - self.assertEqual(1, len(glob(self.resultdir + "wictestdisk-*direct"))) + self.assertEqual(1, len(glob(os.path.join(self.resultdir, "wictestdisk-*direct")))) def test_image_vars_dir_long(self): """Test image vars directory selection --vars option""" @@ -533,58 +905,62 @@ class Wic2(WicTestCase): "--outdir %s" % (image, imgenvdir, native_sysroot, self.resultdir)) - self.assertEqual(1, len(glob(self.resultdir + "wictestdisk-*direct"))) + self.assertEqual(1, len(glob(os.path.join(self.resultdir, "wictestdisk-*direct")))) - @only_for_arch(['i586', 'i686', 'x86_64']) + # TODO this test could also work on aarch64 + @skipIfNotArch(['i586', 'i686', 'x86_64']) def test_wic_image_type(self): """Test building wic images by bitbake""" config = 'IMAGE_FSTYPES += "wic"\nWKS_FILE = "wic-image-minimal"\n'\ - 'MACHINE_FEATURES_append = " efi"\n' + 'MACHINE_FEATURES:append = " efi"\n' self.append_config(config) - self.assertEqual(0, bitbake('wic-image-minimal').status) + image = 'wic-image-minimal' + bitbake(image) self.remove_config(config) - bb_vars = get_bb_vars(['DEPLOY_DIR_IMAGE', 'MACHINE']) - deploy_dir = bb_vars['DEPLOY_DIR_IMAGE'] - machine = bb_vars['MACHINE'] - prefix = os.path.join(deploy_dir, 'wic-image-minimal-%s.' % machine) + bb_vars = get_bb_vars(['DEPLOY_DIR_IMAGE', 'IMAGE_LINK_NAME'], image) + prefix = os.path.join(bb_vars['DEPLOY_DIR_IMAGE'], '%s.' % bb_vars['IMAGE_LINK_NAME']) + # check if we have result image and manifests symlinks # pointing to existing files for suffix in ('wic', 'manifest'): path = prefix + suffix - self.assertTrue(os.path.islink(path)) - self.assertTrue(os.path.isfile(os.path.realpath(path))) + self.assertTrue(os.path.islink(path), msg="Link %s wasn't generated as expected" % path) + self.assertTrue(os.path.isfile(os.path.realpath(path)), msg="File linked to by %s wasn't generated as expected" % path) - @only_for_arch(['i586', 'i686', 'x86_64']) + # TODO this should work on aarch64 + @skipIfNotArch(['i586', 'i686', 'x86_64']) + @OETestTag("runqemu") def test_qemu(self): """Test wic-image-minimal under qemu""" config = 'IMAGE_FSTYPES += "wic"\nWKS_FILE = "wic-image-minimal"\n'\ - 'MACHINE_FEATURES_append = " efi"\n' + 'MACHINE_FEATURES:append = " efi"\n' self.append_config(config) - self.assertEqual(0, bitbake('wic-image-minimal').status) + bitbake('wic-image-minimal') self.remove_config(config) - with runqemu('wic-image-minimal', ssh=False) as qemu: + with runqemu('wic-image-minimal', ssh=False, runqemuparams='nographic') as qemu: cmd = "mount | grep '^/dev/' | cut -f1,3 -d ' ' | egrep -c -e '/dev/sda1 /boot' " \ "-e '/dev/root /|/dev/sda2 /' -e '/dev/sda3 /media' -e '/dev/sda4 /mnt'" status, output = qemu.run_serial(cmd) self.assertEqual(1, status, 'Failed to run command "%s": %s' % (cmd, output)) self.assertEqual(output, '4') - cmd = "grep UUID= /etc/fstab" + cmd = "grep UUID=2c71ef06-a81d-4735-9d3a-379b69c6bdba /etc/fstab" status, output = qemu.run_serial(cmd) self.assertEqual(1, status, 'Failed to run command "%s": %s' % (cmd, output)) self.assertEqual(output, 'UUID=2c71ef06-a81d-4735-9d3a-379b69c6bdba\t/media\text4\tdefaults\t0\t0') - @only_for_arch(['i586', 'i686', 'x86_64']) + @skipIfNotArch(['i586', 'i686', 'x86_64']) + @OETestTag("runqemu") def test_qemu_efi(self): """Test core-image-minimal efi image under qemu""" config = 'IMAGE_FSTYPES = "wic"\nWKS_FILE = "mkefidisk.wks"\n' self.append_config(config) - self.assertEqual(0, bitbake('core-image-minimal ovmf').status) + bitbake('core-image-minimal ovmf') self.remove_config(config) with runqemu('core-image-minimal', ssh=False, - runqemuparams='ovmf', image_fstype='wic') as qemu: + runqemuparams='nographic ovmf', image_fstype='wic') as qemu: cmd = "grep sda. /proc/partitions |wc -l" status, output = qemu.run_serial(cmd) self.assertEqual(1, status, 'Failed to run command "%s": %s' % (cmd, output)) @@ -601,41 +977,50 @@ class Wic2(WicTestCase): tempf.write("part " \ "--source rootfs --ondisk hda --align 4 --fixed-size %d " "--fstype=ext4\n" % size) - wksname = os.path.splitext(os.path.basename(wkspath))[0] - return wkspath, wksname + return wkspath - def test_fixed_size(self): - """ - Test creation of a simple image with partition size controlled through - --fixed-size flag - """ - wkspath, wksname = Wic2._make_fixed_size_wks(200) + def _get_wic_partitions(self, wkspath, native_sysroot=None, ignore_status=False): + p = runCmd("wic create %s -e core-image-minimal -o %s" % (wkspath, self.resultdir), + ignore_status=ignore_status) - runCmd("wic create %s -e core-image-minimal -o %s" \ - % (wkspath, self.resultdir)) - os.remove(wkspath) - wicout = glob(self.resultdir + "%s-*direct" % wksname) - self.assertEqual(1, len(wicout)) + if p.status: + return (p, None) + + wksname = os.path.splitext(os.path.basename(wkspath))[0] + + wicout = glob(os.path.join(self.resultdir, "%s-*direct" % wksname)) + + if not wicout: + return (p, None) wicimg = wicout[0] - native_sysroot = get_bb_var("RECIPE_SYSROOT_NATIVE", "wic-tools") + if not native_sysroot: + native_sysroot = get_bb_var("RECIPE_SYSROOT_NATIVE", "wic-tools") # verify partition size with wic - res = runCmd("parted -m %s unit mib p 2>/dev/null" % wicimg, + res = runCmd("parted -m %s unit kib p 2>/dev/null" % wicimg, native_sysroot=native_sysroot) # parse parted output which looks like this: # BYT;\n # /var/tmp/wic/build/tmpfwvjjkf_-201611101222-hda.direct:200MiB:file:512:512:msdos::;\n # 1:0.00MiB:200MiB:200MiB:ext4::;\n - partlns = res.output.splitlines()[2:] + return (p, res.output.splitlines()[2:]) - self.assertEqual(1, len(partlns), - msg="Partition list '%s'" % res.output) - self.assertEqual("1:0.00MiB:200MiB:200MiB:ext4::;", partlns[0], - msg="Partition list '%s'" % res.output) + def test_fixed_size(self): + """ + Test creation of a simple image with partition size controlled through + --fixed-size flag + """ + wkspath = Wic2._make_fixed_size_wks(200) + _, partlns = self._get_wic_partitions(wkspath) + os.remove(wkspath) + + self.assertEqual(partlns, [ + "1:4.00kiB:204804kiB:204800kiB:ext4::;", + ]) def test_fixed_size_error(self): """ @@ -643,55 +1028,194 @@ class Wic2(WicTestCase): --fixed-size flag. The size of partition is intentionally set to 1MiB in order to trigger an error in wic. """ - wkspath, wksname = Wic2._make_fixed_size_wks(1) - - self.assertEqual(1, runCmd("wic create %s -e core-image-minimal -o %s" \ - % (wkspath, self.resultdir), ignore_status=True).status) + wkspath = Wic2._make_fixed_size_wks(1) + p, _ = self._get_wic_partitions(wkspath, ignore_status=True) os.remove(wkspath) - wicout = glob(self.resultdir + "%s-*direct" % wksname) - self.assertEqual(0, len(wicout)) - @only_for_arch(['i586', 'i686', 'x86_64']) + self.assertNotEqual(p.status, 0, "wic exited successfully when an error was expected:\n%s" % p.output) + + def test_offset(self): + native_sysroot = get_bb_var("RECIPE_SYSROOT_NATIVE", "wic-tools") + + with NamedTemporaryFile("w", suffix=".wks") as tempf: + # Test that partitions are placed at the correct offsets, default KB + tempf.write("bootloader --ptable gpt\n" \ + "part / --source rootfs --ondisk hda --offset 32 --fixed-size 100M --fstype=ext4\n" \ + "part /bar --ondisk hda --offset 102432 --fixed-size 100M --fstype=ext4\n") + tempf.flush() + + _, partlns = self._get_wic_partitions(tempf.name, native_sysroot) + self.assertEqual(partlns, [ + "1:32.0kiB:102432kiB:102400kiB:ext4:primary:;", + "2:102432kiB:204832kiB:102400kiB:ext4:primary:;", + ]) + + with NamedTemporaryFile("w", suffix=".wks") as tempf: + # Test that partitions are placed at the correct offsets, same with explicit KB + tempf.write("bootloader --ptable gpt\n" \ + "part / --source rootfs --ondisk hda --offset 32K --fixed-size 100M --fstype=ext4\n" \ + "part /bar --ondisk hda --offset 102432K --fixed-size 100M --fstype=ext4\n") + tempf.flush() + + _, partlns = self._get_wic_partitions(tempf.name, native_sysroot) + self.assertEqual(partlns, [ + "1:32.0kiB:102432kiB:102400kiB:ext4:primary:;", + "2:102432kiB:204832kiB:102400kiB:ext4:primary:;", + ]) + + with NamedTemporaryFile("w", suffix=".wks") as tempf: + # Test that partitions are placed at the correct offsets using MB + tempf.write("bootloader --ptable gpt\n" \ + "part / --source rootfs --ondisk hda --offset 32K --fixed-size 100M --fstype=ext4\n" \ + "part /bar --ondisk hda --offset 101M --fixed-size 100M --fstype=ext4\n") + tempf.flush() + + _, partlns = self._get_wic_partitions(tempf.name, native_sysroot) + self.assertEqual(partlns, [ + "1:32.0kiB:102432kiB:102400kiB:ext4:primary:;", + "2:103424kiB:205824kiB:102400kiB:ext4:primary:;", + ]) + + with NamedTemporaryFile("w", suffix=".wks") as tempf: + # Test that partitions can be placed on a 512 byte sector boundary + tempf.write("bootloader --ptable gpt\n" \ + "part / --source rootfs --ondisk hda --offset 65s --fixed-size 99M --fstype=ext4\n" \ + "part /bar --ondisk hda --offset 102432 --fixed-size 100M --fstype=ext4\n") + tempf.flush() + + _, partlns = self._get_wic_partitions(tempf.name, native_sysroot) + self.assertEqual(partlns, [ + "1:32.5kiB:101408kiB:101376kiB:ext4:primary:;", + "2:102432kiB:204832kiB:102400kiB:ext4:primary:;", + ]) + + with NamedTemporaryFile("w", suffix=".wks") as tempf: + # Test that a partition can be placed immediately after a MSDOS partition table + tempf.write("bootloader --ptable msdos\n" \ + "part / --source rootfs --ondisk hda --offset 1s --fixed-size 100M --fstype=ext4\n") + tempf.flush() + + _, partlns = self._get_wic_partitions(tempf.name, native_sysroot) + self.assertEqual(partlns, [ + "1:0.50kiB:102400kiB:102400kiB:ext4::;", + ]) + + with NamedTemporaryFile("w", suffix=".wks") as tempf: + # Test that image creation fails if the partitions would overlap + tempf.write("bootloader --ptable gpt\n" \ + "part / --source rootfs --ondisk hda --offset 32 --fixed-size 100M --fstype=ext4\n" \ + "part /bar --ondisk hda --offset 102431 --fixed-size 100M --fstype=ext4\n") + tempf.flush() + + p, _ = self._get_wic_partitions(tempf.name, ignore_status=True) + self.assertNotEqual(p.status, 0, "wic exited successfully when an error was expected:\n%s" % p.output) + + with NamedTemporaryFile("w", suffix=".wks") as tempf: + # Test that partitions are not allowed to overlap with the booloader + tempf.write("bootloader --ptable gpt\n" \ + "part / --source rootfs --ondisk hda --offset 8 --fixed-size 100M --fstype=ext4\n") + tempf.flush() + + p, _ = self._get_wic_partitions(tempf.name, ignore_status=True) + self.assertNotEqual(p.status, 0, "wic exited successfully when an error was expected:\n%s" % p.output) + + def test_extra_space(self): + native_sysroot = get_bb_var("RECIPE_SYSROOT_NATIVE", "wic-tools") + + with NamedTemporaryFile("w", suffix=".wks") as tempf: + tempf.write("bootloader --ptable gpt\n" \ + "part / --source rootfs --ondisk hda --extra-space 200M --fstype=ext4\n") + tempf.flush() + + _, partlns = self._get_wic_partitions(tempf.name, native_sysroot) + self.assertEqual(len(partlns), 1) + size = partlns[0].split(':')[3] + self.assertRegex(size, r'^[0-9]+kiB$') + size = int(size[:-3]) + self.assertGreaterEqual(size, 204800) + + # TODO this test could also work on aarch64 + @skipIfNotArch(['i586', 'i686', 'x86_64']) + @OETestTag("runqemu") def test_rawcopy_plugin_qemu(self): """Test rawcopy plugin in qemu""" - # build ext4 and wic images - for fstype in ("ext4", "wic"): - config = 'IMAGE_FSTYPES = "%s"\nWKS_FILE = "test_rawcopy_plugin.wks.in"\n' % fstype - self.append_config(config) - self.assertEqual(0, bitbake('core-image-minimal').status) - self.remove_config(config) - - with runqemu('core-image-minimal', ssh=False, image_fstype='wic') as qemu: + # build ext4 and then use it for a wic image + config = 'IMAGE_FSTYPES = "ext4"\n' + self.append_config(config) + bitbake('core-image-minimal') + image_link_name = get_bb_var('IMAGE_LINK_NAME', 'core-image-minimal') + self.remove_config(config) + + config = 'IMAGE_FSTYPES = "wic"\n' \ + 'IMAGE_LINK_NAME_CORE_IMAGE_MINIMAL = "%s"\n'\ + 'WKS_FILE = "test_rawcopy_plugin.wks.in"\n'\ + % image_link_name + self.append_config(config) + bitbake('core-image-minimal-mtdutils') + self.remove_config(config) + + with runqemu('core-image-minimal-mtdutils', ssh=False, + runqemuparams='nographic', image_fstype='wic') as qemu: cmd = "grep sda. /proc/partitions |wc -l" status, output = qemu.run_serial(cmd) self.assertEqual(1, status, 'Failed to run command "%s": %s' % (cmd, output)) self.assertEqual(output, '2') - def test_rawcopy_plugin(self): + def _rawcopy_plugin(self, fstype): """Test rawcopy plugin""" - img = 'core-image-minimal' - machine = get_bb_var('MACHINE', img) + image = 'core-image-minimal' + bb_vars = get_bb_vars(['DEPLOY_DIR_IMAGE', 'IMAGE_LINK_NAME'], image) + params = ',unpack' if fstype.endswith('.gz') else '' with NamedTemporaryFile("w", suffix=".wks") as wks: - wks.writelines(['part /boot --active --source bootimg-pcbios\n', - 'part / --source rawcopy --sourceparams="file=%s-%s.ext4" --use-uuid\n'\ - % (img, machine), - 'bootloader --timeout=0 --append="console=ttyS0,115200n8"\n']) + wks.write('part / --source rawcopy --sourceparams="file=%s.%s%s"\n'\ + % (bb_vars['IMAGE_LINK_NAME'], fstype, params)) wks.flush() - cmd = "wic create %s -e %s -o %s" % (wks.name, img, self.resultdir) + cmd = "wic create %s -e %s -o %s" % (wks.name, image, self.resultdir) runCmd(cmd) wksname = os.path.splitext(os.path.basename(wks.name))[0] - out = glob(self.resultdir + "%s-*direct" % wksname) + out = glob(os.path.join(self.resultdir, "%s-*direct" % wksname)) self.assertEqual(1, len(out)) - @only_for_arch(['i586', 'i686', 'x86_64']) + def test_rawcopy_plugin(self): + self._rawcopy_plugin('ext4') + + def test_rawcopy_plugin_unpack(self): + fstype = 'ext4.gz' + config = 'IMAGE_FSTYPES = "%s"\n' % fstype + self.append_config(config) + self.assertEqual(0, bitbake('core-image-minimal').status) + self.remove_config(config) + self._rawcopy_plugin(fstype) + + def test_empty_plugin(self): + """Test empty plugin""" + config = 'IMAGE_FSTYPES = "wic"\nWKS_FILE = "test_empty_plugin.wks"\n' + self.append_config(config) + image = 'core-image-minimal' + bitbake(image) + self.remove_config(config) + bb_vars = get_bb_vars(['DEPLOY_DIR_IMAGE', 'IMAGE_LINK_NAME'], image) + image_path = os.path.join(bb_vars['DEPLOY_DIR_IMAGE'], '%s.wic' % bb_vars['IMAGE_LINK_NAME']) + self.assertTrue(os.path.exists(image_path), msg="Image file %s wasn't generated as expected" % image_path) + + sysroot = get_bb_var('RECIPE_SYSROOT_NATIVE', 'wic-tools') + + # Fstype column from 'wic ls' should be empty for the second partition + # as listed in test_empty_plugin.wks + result = runCmd("wic ls %s -n %s | awk -F ' ' '{print $1 \" \" $5}' | grep '^2' | wc -w" % (image_path, sysroot)) + self.assertEqual('1', result.output) + + @skipIfNotArch(['i586', 'i686', 'x86_64']) + @OETestTag("runqemu") def test_biosplusefi_plugin_qemu(self): """Test biosplusefi plugin in qemu""" - config = 'IMAGE_FSTYPES = "wic"\nWKS_FILE = "test_biosplusefi_plugin.wks"\nMACHINE_FEATURES_append = " efi"\n' + config = 'IMAGE_FSTYPES = "wic"\nWKS_FILE = "test_biosplusefi_plugin.wks"\nMACHINE_FEATURES:append = " efi"\n' self.append_config(config) - self.assertEqual(0, bitbake('core-image-minimal').status) + bitbake('core-image-minimal') self.remove_config(config) - with runqemu('core-image-minimal', ssh=False, image_fstype='wic') as qemu: + with runqemu('core-image-minimal', ssh=False, + runqemuparams='nographic', image_fstype='wic') as qemu: # Check that we have ONLY two /dev/sda* partitions (/boot and /) cmd = "grep sda. /proc/partitions | wc -l" status, output = qemu.run_serial(cmd) @@ -714,7 +1238,7 @@ class Wic2(WicTestCase): self.assertEqual(1, status, 'Failed to run command "%s": %s' % (cmd, output)) self.assertEqual(output, '*') - @only_for_arch(['i586', 'i686', 'x86_64']) + @skipIfNotArch(['i586', 'i686', 'x86_64']) def test_biosplusefi_plugin(self): """Test biosplusefi plugin""" # Wic generation below may fail depending on the order of the unittests @@ -723,9 +1247,9 @@ class Wic2(WicTestCase): # If an image hasn't been built yet, directory ${STAGING_DATADIR}/syslinux won't exists and _get_bootimg_dir() # will raise with "Couldn't find correct bootimg_dir" # The easiest way to work-around this issue is to make sure we already built an image here, hence the bitbake call - config = 'IMAGE_FSTYPES = "wic"\nWKS_FILE = "test_biosplusefi_plugin.wks"\nMACHINE_FEATURES_append = " efi"\n' + config = 'IMAGE_FSTYPES = "wic"\nWKS_FILE = "test_biosplusefi_plugin.wks"\nMACHINE_FEATURES:append = " efi"\n' self.append_config(config) - self.assertEqual(0, bitbake('core-image-minimal').status) + bitbake('core-image-minimal') self.remove_config(config) img = 'core-image-minimal' @@ -737,9 +1261,60 @@ class Wic2(WicTestCase): cmd = "wic create %s -e %s -o %s" % (wks.name, img, self.resultdir) runCmd(cmd) wksname = os.path.splitext(os.path.basename(wks.name))[0] - out = glob(self.resultdir + "%s-*.direct" % wksname) + out = glob(os.path.join(self.resultdir, "%s-*.direct" % wksname)) self.assertEqual(1, len(out)) + @skipIfNotArch(['i586', 'i686', 'x86_64', 'aarch64']) + def test_uefi_kernel(self): + """ Test uefi-kernel in wic """ + config = 'IMAGE_EFI_BOOT_FILES="/etc/fstab;testfile"\nIMAGE_FSTYPES = "wic"\nWKS_FILE = "test_uefikernel.wks"\nMACHINE_FEATURES:append = " efi"\n' + self.append_config(config) + bitbake('core-image-minimal') + self.remove_config(config) + + img = 'core-image-minimal' + with NamedTemporaryFile("w", suffix=".wks") as wks: + wks.writelines(['part /boot --source bootimg-efi --sourceparams="loader=uefi-kernel"\n' + 'part / --source rootfs --fstype=ext4 --align 1024 --use-uuid\n'\ + 'bootloader --timeout=0 --append="console=ttyS0,115200n8"\n']) + wks.flush() + cmd = "wic create %s -e %s -o %s" % (wks.name, img, self.resultdir) + runCmd(cmd) + wksname = os.path.splitext(os.path.basename(wks.name))[0] + out = glob(os.path.join(self.resultdir, "%s-*.direct" % wksname)) + self.assertEqual(1, len(out)) + + # TODO this test could also work on aarch64 + @skipIfNotArch(['i586', 'i686', 'x86_64']) + @OETestTag("runqemu") + def test_efi_plugin_unified_kernel_image_qemu(self): + """Test efi plugin's Unified Kernel Image feature in qemu""" + config = 'IMAGE_FSTYPES = "wic"\n'\ + 'INITRAMFS_IMAGE = "core-image-minimal-initramfs"\n'\ + 'WKS_FILE = "test_efi_plugin.wks"\n'\ + 'MACHINE_FEATURES:append = " efi"\n' + self.append_config(config) + bitbake('core-image-minimal core-image-minimal-initramfs ovmf') + self.remove_config(config) + + with runqemu('core-image-minimal', ssh=False, + runqemuparams='nographic ovmf', image_fstype='wic') as qemu: + # Check that /boot has EFI bootx64.efi (required for EFI) + cmd = "ls /boot/EFI/BOOT/bootx64.efi | wc -l" + status, output = qemu.run_serial(cmd) + self.assertEqual(1, status, 'Failed to run command "%s": %s' % (cmd, output)) + self.assertEqual(output, '1') + # Check that /boot has EFI/Linux/linux.efi (required for Unified Kernel Images auto detection) + cmd = "ls /boot/EFI/Linux/linux.efi | wc -l" + status, output = qemu.run_serial(cmd) + self.assertEqual(1, status, 'Failed to run command "%s": %s' % (cmd, output)) + self.assertEqual(output, '1') + # Check that /boot doesn't have loader/entries/boot.conf (Unified Kernel Images are auto detected by the bootloader) + cmd = "ls /boot/loader/entries/boot.conf 2&>/dev/null | wc -l" + status, output = qemu.run_serial(cmd) + self.assertEqual(1, status, 'Failed to run command "%s": %s' % (cmd, output)) + self.assertEqual(output, '0') + def test_fs_types(self): """Test filesystem types for empty and not empty partitions""" img = 'core-image-minimal' @@ -756,7 +1331,7 @@ class Wic2(WicTestCase): cmd = "wic create %s -e %s -o %s" % (wks.name, img, self.resultdir) runCmd(cmd) wksname = os.path.splitext(os.path.basename(wks.name))[0] - out = glob(self.resultdir + "%s-*direct" % wksname) + out = glob(os.path.join(self.resultdir, "%s-*direct" % wksname)) self.assertEqual(1, len(out)) def test_kickstart_parser(self): @@ -768,7 +1343,7 @@ class Wic2(WicTestCase): cmd = "wic create %s -e core-image-minimal -o %s" % (wks.name, self.resultdir) runCmd(cmd) wksname = os.path.splitext(os.path.basename(wks.name))[0] - out = glob(self.resultdir + "%s-*direct" % wksname) + out = glob(os.path.join(self.resultdir, "%s-*direct" % wksname)) self.assertEqual(1, len(out)) def test_image_bootpart_globbed(self): @@ -779,11 +1354,11 @@ class Wic2(WicTestCase): self.append_config(config) runCmd(cmd) self.remove_config(config) - self.assertEqual(1, len(glob(self.resultdir + "sdimage-bootpart-*direct"))) + self.assertEqual(1, len(glob(os.path.join(self.resultdir, "sdimage-bootpart-*direct")))) def test_sparse_copy(self): """Test sparse_copy with FIEMAP and SEEK_HOLE filemap APIs""" - libpath = os.path.join(get_bb_var('COREBASE'), 'scripts', 'lib', 'wic') + libpath = os.path.join(self.td['COREBASE'], 'scripts', 'lib', 'wic') sys.path.insert(0, libpath) from filemap import FilemapFiemap, FilemapSeek, sparse_copy, ErrorNotSupp with NamedTemporaryFile("w", suffix=".wic-sparse") as sparse: @@ -809,12 +1384,148 @@ class Wic2(WicTestCase): self.assertEqual(dest_stat.st_blocks, 8) os.unlink(dest) + def test_mkfs_extraopts(self): + """Test wks option --mkfs-extraopts for empty and not empty partitions""" + img = 'core-image-minimal' + with NamedTemporaryFile("w", suffix=".wks") as wks: + wks.writelines( + ['part ext2 --fstype ext2 --source rootfs --mkfs-extraopts "-D -F -i 8192"\n', + "part btrfs --fstype btrfs --source rootfs --size 40M --mkfs-extraopts='--quiet'\n", + 'part squash --fstype squashfs --source rootfs --mkfs-extraopts "-no-sparse -b 4096"\n', + 'part emptyvfat --fstype vfat --size 1M --mkfs-extraopts "-S 1024 -s 64"\n', + 'part emptymsdos --fstype msdos --size 1M --mkfs-extraopts "-S 1024 -s 64"\n', + 'part emptyext2 --fstype ext2 --size 1M --mkfs-extraopts "-D -F -i 8192"\n', + 'part emptybtrfs --fstype btrfs --size 100M --mkfs-extraopts "--mixed -K"\n']) + wks.flush() + cmd = "wic create %s -e %s -o %s" % (wks.name, img, self.resultdir) + runCmd(cmd) + wksname = os.path.splitext(os.path.basename(wks.name))[0] + out = glob(os.path.join(self.resultdir, "%s-*direct" % wksname)) + self.assertEqual(1, len(out)) + + @skipIfNotArch(['i586', 'i686', 'x86_64']) + @OETestTag("runqemu") + def test_expand_mbr_image(self): + """Test wic write --expand command for mbr image""" + # build an image + config = 'IMAGE_FSTYPES = "wic"\nWKS_FILE = "directdisk.wks"\n' + self.append_config(config) + image = 'core-image-minimal' + bitbake(image) + + # get path to the image + bb_vars = get_bb_vars(['DEPLOY_DIR_IMAGE', 'IMAGE_LINK_NAME'], image) + image_path = os.path.join(bb_vars['DEPLOY_DIR_IMAGE'], '%s.wic' % bb_vars['IMAGE_LINK_NAME']) + + self.remove_config(config) + + try: + # expand image to 1G + new_image_path = None + with NamedTemporaryFile(mode='wb', suffix='.wic.exp', + dir=bb_vars['DEPLOY_DIR_IMAGE'], delete=False) as sparse: + sparse.truncate(1024 ** 3) + new_image_path = sparse.name + + sysroot = get_bb_var('RECIPE_SYSROOT_NATIVE', 'wic-tools') + cmd = "wic write -n %s --expand 1:0 %s %s" % (sysroot, image_path, new_image_path) + runCmd(cmd) + + # check if partitions are expanded + orig = runCmd("wic ls %s -n %s" % (image_path, sysroot)) + exp = runCmd("wic ls %s -n %s" % (new_image_path, sysroot)) + orig_sizes = [int(line.split()[3]) for line in orig.output.split('\n')[1:]] + exp_sizes = [int(line.split()[3]) for line in exp.output.split('\n')[1:]] + self.assertEqual(orig_sizes[0], exp_sizes[0]) # first partition is not resized + self.assertTrue(orig_sizes[1] < exp_sizes[1], msg="Parition size wasn't enlarged (%s vs %s)" % (orig_sizes[1], exp_sizes[1])) + + # Check if all free space is partitioned + result = runCmd("%s/usr/sbin/sfdisk -F %s" % (sysroot, new_image_path)) + self.assertIn("0 B, 0 bytes, 0 sectors", result.output) + + os.rename(image_path, image_path + '.bak') + os.rename(new_image_path, image_path) + + # Check if it boots in qemu + with runqemu('core-image-minimal', ssh=False, runqemuparams='nographic') as qemu: + cmd = "ls /etc/" + status, output = qemu.run_serial('true') + self.assertEqual(1, status, 'Failed to run command "%s": %s' % (cmd, output)) + finally: + if os.path.exists(new_image_path): + os.unlink(new_image_path) + if os.path.exists(image_path + '.bak'): + os.rename(image_path + '.bak', image_path) + + def test_gpt_partition_name(self): + """Test --part-name argument to set partition name in GPT table""" + config = 'IMAGE_FSTYPES += "wic"\nWKS_FILE = "test_gpt_partition_name.wks"\n' + self.append_config(config) + image = 'core-image-minimal' + bitbake(image) + self.remove_config(config) + deploy_dir = get_bb_var('DEPLOY_DIR_IMAGE') + bb_vars = get_bb_vars(['DEPLOY_DIR_IMAGE', 'IMAGE_LINK_NAME'], image) + image_path = os.path.join(bb_vars['DEPLOY_DIR_IMAGE'], '%s.wic' % bb_vars['IMAGE_LINK_NAME']) + + sysroot = get_bb_var('RECIPE_SYSROOT_NATIVE', 'wic-tools') + + # Image is created + self.assertTrue(os.path.exists(image_path), "image file %s doesn't exist" % image_path) + + # Check the names of the three partitions + # as listed in test_gpt_partition_name.wks + result = runCmd("%s/usr/sbin/sfdisk --part-label %s 1" % (sysroot, image_path)) + self.assertEqual('boot-A', result.output) + result = runCmd("%s/usr/sbin/sfdisk --part-label %s 2" % (sysroot, image_path)) + self.assertEqual('root-A', result.output) + # When the --part-name is not defined, the partition name is equal to the --label + result = runCmd("%s/usr/sbin/sfdisk --part-label %s 3" % (sysroot, image_path)) + self.assertEqual('ext-space', result.output) + + def test_empty_zeroize_plugin(self): + img = 'core-image-minimal' + expected_size = [ 1024*1024, # 1M + 512*1024, # 512K + 2*1024*1024] # 2M + # Check combination of sourceparams + with NamedTemporaryFile("w", suffix=".wks") as wks: + wks.writelines( + ['part empty --source empty --sourceparams="fill" --ondisk sda --fixed-size 1M\n', + 'part empty --source empty --sourceparams="size=512K" --ondisk sda --size 1M --align 1024\n', + 'part empty --source empty --sourceparams="size=2048k,bs=512K" --ondisk sda --size 4M --align 1024\n' + ]) + wks.flush() + cmd = "wic create %s -e %s -o %s" % (wks.name, img, self.resultdir) + runCmd(cmd) + wksname = os.path.splitext(os.path.basename(wks.name))[0] + wicout = glob(os.path.join(self.resultdir, "%s-*direct" % wksname)) + # Skip the complete image and just look at the single partitions + for idx, value in enumerate(wicout[1:]): + self.logger.info(wicout[idx]) + # Check if partitions are actually zeroized + with open(wicout[idx], mode="rb") as fd: + ba = bytearray(fd.read()) + for b in ba: + self.assertEqual(b, 0) + self.assertEqual(expected_size[idx], os.path.getsize(wicout[idx])) + + # Check inconsistancy check between "fill" and "--size" parameter + with NamedTemporaryFile("w", suffix=".wks") as wks: + wks.writelines(['part empty --source empty --sourceparams="fill" --ondisk sda --size 1M\n']) + wks.flush() + cmd = "wic create %s -e %s -o %s" % (wks.name, img, self.resultdir) + result = runCmd(cmd, ignore_status=True) + self.assertIn("Source parameter 'fill' only works with the '--fixed-size' option, exiting.", result.output) + self.assertNotEqual(0, result.status) + +class ModifyTests(WicTestCase): def test_wic_ls(self): """Test listing image content using 'wic ls'""" runCmd("wic create wictestdisk " "--image-name=core-image-minimal " "-D -o %s" % self.resultdir) - images = glob(self.resultdir + "wictestdisk-*.direct") + images = glob(os.path.join(self.resultdir, "wictestdisk-*.direct")) self.assertEqual(1, len(images)) sysroot = get_bb_var('RECIPE_SYSROOT_NATIVE', 'wic-tools') @@ -832,7 +1543,7 @@ class Wic2(WicTestCase): runCmd("wic create wictestdisk " "--image-name=core-image-minimal " "-D -o %s" % self.resultdir) - images = glob(self.resultdir + "wictestdisk-*.direct") + images = glob(os.path.join(self.resultdir, "wictestdisk-*.direct")) self.assertEqual(1, len(images)) sysroot = get_bb_var('RECIPE_SYSROOT_NATIVE', 'wic-tools') @@ -850,7 +1561,7 @@ class Wic2(WicTestCase): # check if file is there result = runCmd("wic ls %s:1/ -n %s" % (images[0], sysroot)) self.assertEqual(7, len(result.output.split('\n'))) - self.assertTrue(os.path.basename(testfile.name) in result.output) + self.assertIn(os.path.basename(testfile.name), result.output) # prepare directory testdir = os.path.join(self.resultdir, 'wic-test-cp-dir') @@ -864,13 +1575,13 @@ class Wic2(WicTestCase): # check if directory is there result = runCmd("wic ls %s:1/ -n %s" % (images[0], sysroot)) self.assertEqual(8, len(result.output.split('\n'))) - self.assertTrue(os.path.basename(testdir) in result.output) + self.assertIn(os.path.basename(testdir), result.output) # copy the file from the partition and check if it success dest = '%s-cp' % testfile.name runCmd("wic cp %s:1/%s %s -n %s" % (images[0], os.path.basename(testfile.name), dest, sysroot)) - self.assertTrue(os.path.exists(dest)) + self.assertTrue(os.path.exists(dest), msg="File %s wasn't generated as expected" % dest) def test_wic_rm(self): @@ -878,105 +1589,35 @@ class Wic2(WicTestCase): runCmd("wic create mkefidisk " "--image-name=core-image-minimal " "-D -o %s" % self.resultdir) - images = glob(self.resultdir + "mkefidisk-*.direct") + images = glob(os.path.join(self.resultdir, "mkefidisk-*.direct")) self.assertEqual(1, len(images)) sysroot = get_bb_var('RECIPE_SYSROOT_NATIVE', 'wic-tools') + # Not bulletproof but hopefully sufficient + kerneltype = get_bb_var('KERNEL_IMAGETYPE', 'virtual/kernel') # list directory content of the first partition result = runCmd("wic ls %s:1 -n %s" % (images[0], sysroot)) - self.assertIn('\nBZIMAGE ', result.output) + self.assertIn('\n%s ' % kerneltype.upper(), result.output) self.assertIn('\nEFI <DIR> ', result.output) - # remove file - runCmd("wic rm %s:1/bzimage -n %s" % (images[0], sysroot)) + # remove file. EFI partitions are case-insensitive so exercise that too + runCmd("wic rm %s:1/%s -n %s" % (images[0], kerneltype.lower(), sysroot)) # remove directory runCmd("wic rm %s:1/efi -n %s" % (images[0], sysroot)) # check if they're removed result = runCmd("wic ls %s:1 -n %s" % (images[0], sysroot)) - self.assertNotIn('\nBZIMAGE ', result.output) + self.assertNotIn('\n%s ' % kerneltype.upper(), result.output) self.assertNotIn('\nEFI <DIR> ', result.output) - def test_mkfs_extraopts(self): - """Test wks option --mkfs-extraopts for empty and not empty partitions""" - img = 'core-image-minimal' - with NamedTemporaryFile("w", suffix=".wks") as wks: - wks.writelines( - ['part ext2 --fstype ext2 --source rootfs --mkfs-extraopts "-D -F -i 8192"\n', - "part btrfs --fstype btrfs --source rootfs --size 40M --mkfs-extraopts='--quiet'\n", - 'part squash --fstype squashfs --source rootfs --mkfs-extraopts "-no-sparse -b 4096"\n', - 'part emptyvfat --fstype vfat --size 1M --mkfs-extraopts "-S 1024 -s 64"\n', - 'part emptymsdos --fstype msdos --size 1M --mkfs-extraopts "-S 1024 -s 64"\n', - 'part emptyext2 --fstype ext2 --size 1M --mkfs-extraopts "-D -F -i 8192"\n', - 'part emptybtrfs --fstype btrfs --size 100M --mkfs-extraopts "--mixed -K"\n']) - wks.flush() - cmd = "wic create %s -e %s -o %s" % (wks.name, img, self.resultdir) - runCmd(cmd) - wksname = os.path.splitext(os.path.basename(wks.name))[0] - out = glob(self.resultdir + "%s-*direct" % wksname) - self.assertEqual(1, len(out)) - - def test_expand_mbr_image(self): - """Test wic write --expand command for mbr image""" - # build an image - config = 'IMAGE_FSTYPES = "wic"\nWKS_FILE = "directdisk.wks"\n' - self.append_config(config) - self.assertEqual(0, bitbake('core-image-minimal').status) - - # get path to the image - bb_vars = get_bb_vars(['DEPLOY_DIR_IMAGE', 'MACHINE']) - deploy_dir = bb_vars['DEPLOY_DIR_IMAGE'] - machine = bb_vars['MACHINE'] - image_path = os.path.join(deploy_dir, 'core-image-minimal-%s.wic' % machine) - - self.remove_config(config) - - try: - # expand image to 1G - new_image_path = None - with NamedTemporaryFile(mode='wb', suffix='.wic.exp', - dir=deploy_dir, delete=False) as sparse: - sparse.truncate(1024 ** 3) - new_image_path = sparse.name - - sysroot = get_bb_var('RECIPE_SYSROOT_NATIVE', 'wic-tools') - cmd = "wic write -n %s --expand 1:0 %s %s" % (sysroot, image_path, new_image_path) - runCmd(cmd) - - # check if partitions are expanded - orig = runCmd("wic ls %s -n %s" % (image_path, sysroot)) - exp = runCmd("wic ls %s -n %s" % (new_image_path, sysroot)) - orig_sizes = [int(line.split()[3]) for line in orig.output.split('\n')[1:]] - exp_sizes = [int(line.split()[3]) for line in exp.output.split('\n')[1:]] - self.assertEqual(orig_sizes[0], exp_sizes[0]) # first partition is not resized - self.assertTrue(orig_sizes[1] < exp_sizes[1]) - - # Check if all free space is partitioned - result = runCmd("%s/usr/sbin/sfdisk -F %s" % (sysroot, new_image_path)) - self.assertTrue("0 B, 0 bytes, 0 sectors" in result.output) - - os.rename(image_path, image_path + '.bak') - os.rename(new_image_path, image_path) - - # Check if it boots in qemu - with runqemu('core-image-minimal', ssh=False) as qemu: - cmd = "ls /etc/" - status, output = qemu.run_serial('true') - self.assertEqual(1, status, 'Failed to run command "%s": %s' % (cmd, output)) - finally: - if os.path.exists(new_image_path): - os.unlink(new_image_path) - if os.path.exists(image_path + '.bak'): - os.rename(image_path + '.bak', image_path) - def test_wic_ls_ext(self): """Test listing content of the ext partition using 'wic ls'""" runCmd("wic create wictestdisk " "--image-name=core-image-minimal " "-D -o %s" % self.resultdir) - images = glob(self.resultdir + "wictestdisk-*.direct") + images = glob(os.path.join(self.resultdir, "wictestdisk-*.direct")) self.assertEqual(1, len(images)) sysroot = get_bb_var('RECIPE_SYSROOT_NATIVE', 'wic-tools') @@ -984,14 +1625,14 @@ class Wic2(WicTestCase): # list directory content of the second ext4 partition result = runCmd("wic ls %s:2/ -n %s" % (images[0], sysroot)) self.assertTrue(set(['bin', 'home', 'proc', 'usr', 'var', 'dev', 'lib', 'sbin']).issubset( - set(line.split()[-1] for line in result.output.split('\n') if line))) + set(line.split()[-1] for line in result.output.split('\n') if line)), msg="Expected directories not present %s" % result.output) def test_wic_cp_ext(self): """Test copy files and directories to the ext partition.""" runCmd("wic create wictestdisk " "--image-name=core-image-minimal " "-D -o %s" % self.resultdir) - images = glob(self.resultdir + "wictestdisk-*.direct") + images = glob(os.path.join(self.resultdir, "wictestdisk-*.direct")) self.assertEqual(1, len(images)) sysroot = get_bb_var('RECIPE_SYSROOT_NATIVE', 'wic-tools') @@ -999,7 +1640,7 @@ class Wic2(WicTestCase): # list directory content of the ext4 partition result = runCmd("wic ls %s:2/ -n %s" % (images[0], sysroot)) dirs = set(line.split()[-1] for line in result.output.split('\n') if line) - self.assertTrue(set(['bin', 'home', 'proc', 'usr', 'var', 'dev', 'lib', 'sbin']).issubset(dirs)) + self.assertTrue(set(['bin', 'home', 'proc', 'usr', 'var', 'dev', 'lib', 'sbin']).issubset(dirs), msg="Expected directories not present %s" % dirs) with NamedTemporaryFile("w", suffix=".wic-cp") as testfile: testfile.write("test") @@ -1014,12 +1655,12 @@ class Wic2(WicTestCase): # check if the file to copy is in the partition result = runCmd("wic ls %s:2/etc/ -n %s" % (images[0], sysroot)) - self.assertTrue('fstab' in [line.split()[-1] for line in result.output.split('\n') if line]) + self.assertIn('fstab', [line.split()[-1] for line in result.output.split('\n') if line]) # copy file from the partition, replace the temporary file content with it and # check for the file size to validate the copy runCmd("wic cp %s:2/etc/fstab %s -n %s" % (images[0], testfile.name, sysroot)) - self.assertTrue(os.stat(testfile.name).st_size > 0) + self.assertTrue(os.stat(testfile.name).st_size > 0, msg="Filesize not as expected %s" % os.stat(testfile.name).st_size) def test_wic_rm_ext(self): @@ -1027,25 +1668,25 @@ class Wic2(WicTestCase): runCmd("wic create mkefidisk " "--image-name=core-image-minimal " "-D -o %s" % self.resultdir) - images = glob(self.resultdir + "mkefidisk-*.direct") + images = glob(os.path.join(self.resultdir, "mkefidisk-*.direct")) self.assertEqual(1, len(images)) sysroot = get_bb_var('RECIPE_SYSROOT_NATIVE', 'wic-tools') # list directory content of the /etc directory on ext4 partition result = runCmd("wic ls %s:2/etc/ -n %s" % (images[0], sysroot)) - self.assertTrue('fstab' in [line.split()[-1] for line in result.output.split('\n') if line]) + self.assertIn('fstab', [line.split()[-1] for line in result.output.split('\n') if line]) # remove file runCmd("wic rm %s:2/etc/fstab -n %s" % (images[0], sysroot)) # check if it's removed result = runCmd("wic ls %s:2/etc/ -n %s" % (images[0], sysroot)) - self.assertTrue('fstab' not in [line.split()[-1] for line in result.output.split('\n') if line]) + self.assertNotIn('fstab', [line.split()[-1] for line in result.output.split('\n') if line]) # remove non-empty directory runCmd("wic rm -r %s:2/etc/ -n %s" % (images[0], sysroot)) # check if it's removed result = runCmd("wic ls %s:2/ -n %s" % (images[0], sysroot)) - self.assertTrue('etc' not in [line.split()[-1] for line in result.output.split('\n') if line]) + self.assertNotIn('etc', [line.split()[-1] for line in result.output.split('\n') if line]) diff --git a/meta/lib/oeqa/selftest/cases/wrapper.py b/meta/lib/oeqa/selftest/cases/wrapper.py new file mode 100644 index 0000000000..f2be44262c --- /dev/null +++ b/meta/lib/oeqa/selftest/cases/wrapper.py @@ -0,0 +1,16 @@ +# +# Copyright OpenEmbedded Contributors +# +# SPDX-License-Identifier: MIT +# +from oeqa.selftest.case import OESelftestTestCase +from oeqa.utils.commands import bitbake + +class WrapperTests(OESelftestTestCase): + def test_shebang_wrapper(self): + """ + Summary: Build a recipe which will fail if the cmdline_shebang_wrapper function is defective. + Expected: Exit status to be 0. + Author: Paulo Neves <ptsneves@gmail.com> + """ + res = bitbake("cmdline-shebang-wrapper-test -c install", ignore_status=False) diff --git a/meta/lib/oeqa/selftest/cases/yoctotestresultsquerytests.py b/meta/lib/oeqa/selftest/cases/yoctotestresultsquerytests.py new file mode 100644 index 0000000000..312edb6431 --- /dev/null +++ b/meta/lib/oeqa/selftest/cases/yoctotestresultsquerytests.py @@ -0,0 +1,39 @@ +# +# Copyright OpenEmbedded Contributors +# +# SPDX-License-Identifier: MIT +# + +import os +import sys +import subprocess +import shutil +from oeqa.selftest.case import OESelftestTestCase +from yocto_testresults_query import get_sha1, create_workdir +basepath = os.path.abspath(os.path.dirname(__file__) + '/../../../../../') +lib_path = basepath + '/scripts/lib' +sys.path = sys.path + [lib_path] + + +class TestResultsQueryTests(OESelftestTestCase): + def test_get_sha1(self): + test_data_get_sha1 = [ + {"input": "yocto-4.0", "expected": "00cfdde791a0176c134f31e5a09eff725e75b905"}, + {"input": "4.1_M1", "expected": "95066dde6861ee08fdb505ab3e0422156cc24fae"}, + ] + for data in test_data_get_sha1: + test_name = data["input"] + with self.subTest(f"Test SHA1 from {test_name}"): + self.assertEqual( + get_sha1(basepath, data["input"]), data["expected"]) + + def test_create_workdir(self): + workdir = create_workdir() + try: + url = subprocess.check_output( + ["git", "-C", workdir, "remote", "get-url", "origin"]).strip().decode("utf-8") + except: + shutil.rmtree(workdir, ignore_errors=True) + self.fail(f"Can not execute git commands in {workdir}") + shutil.rmtree(workdir) + self.assertEqual(url, "git://git.yoctoproject.org/yocto-testresults") diff --git a/meta/lib/oeqa/selftest/context.py b/meta/lib/oeqa/selftest/context.py index c4eb5d614e..acc3b073bd 100644 --- a/meta/lib/oeqa/selftest/context.py +++ b/meta/lib/oeqa/selftest/context.py @@ -9,33 +9,158 @@ import time import glob import sys import importlib -import signal -from shutil import copyfile +import subprocess +import unittest from random import choice import oeqa import oe +import bb.utils +import bb.tinfoil from oeqa.core.context import OETestContext, OETestContextExecutor from oeqa.core.exception import OEQAPreRun, OEQATestNotFound from oeqa.utils.commands import runCmd, get_bb_vars, get_test_layer +OESELFTEST_METADATA=["run_all_tests", "run_tests", "skips", "machine", "select_tags", "exclude_tags"] + +def get_oeselftest_metadata(args): + result = {} + raw_args = vars(args) + for metadata in OESELFTEST_METADATA: + if metadata in raw_args: + result[metadata] = raw_args[metadata] + + return result + +class NonConcurrentTestSuite(unittest.TestSuite): + def __init__(self, suite, processes, setupfunc, removefunc, bb_vars): + super().__init__([suite]) + self.processes = processes + self.suite = suite + self.setupfunc = setupfunc + self.removefunc = removefunc + self.bb_vars = bb_vars + + def run(self, result): + (builddir, newbuilddir) = self.setupfunc("-st", None, self.suite) + ret = super().run(result) + os.chdir(builddir) + if newbuilddir and ret.wasSuccessful() and self.removefunc: + self.removefunc(newbuilddir) + +def removebuilddir(d): + delay = 5 + while delay and (os.path.exists(d + "/bitbake.lock") or os.path.exists(d + "/cache/hashserv.db-wal")): + time.sleep(1) + delay = delay - 1 + # Deleting these directories takes a lot of time, use autobuilder + # clobberdir if its available + clobberdir = os.path.expanduser("~/yocto-autobuilder-helper/janitor/clobberdir") + if os.path.exists(clobberdir): + try: + subprocess.check_call([clobberdir, d]) + return + except subprocess.CalledProcessError: + pass + bb.utils.prunedir(d, ionice=True) + class OESelftestTestContext(OETestContext): - def __init__(self, td=None, logger=None, machines=None, config_paths=None): + def __init__(self, td=None, logger=None, machines=None, config_paths=None, newbuilddir=None, keep_builddir=None): super(OESelftestTestContext, self).__init__(td, logger) - self.machines = machines - self.custommachine = None self.config_paths = config_paths + self.newbuilddir = newbuilddir + + if keep_builddir: + self.removebuilddir = None + else: + self.removebuilddir = removebuilddir + + def set_variables(self, vars): + self.bb_vars = vars + + def setup_builddir(self, suffix, selftestdir, suite): + sstatedir = self.bb_vars['SSTATE_DIR'] + + builddir = os.environ['BUILDDIR'] + if not selftestdir: + selftestdir = get_test_layer(self.bb_vars['BBLAYERS']) + if self.newbuilddir: + newbuilddir = os.path.join(self.newbuilddir, 'build' + suffix) + else: + newbuilddir = builddir + suffix + newselftestdir = newbuilddir + "/meta-selftest" + + if os.path.exists(newbuilddir): + self.logger.error("Build directory %s already exists, aborting" % newbuilddir) + sys.exit(1) + + bb.utils.mkdirhier(newbuilddir) + oe.path.copytree(builddir + "/conf", newbuilddir + "/conf") + oe.path.copytree(builddir + "/cache", newbuilddir + "/cache") + oe.path.copytree(selftestdir, newselftestdir) + + subprocess.check_output("git init && git add * && git commit -a -m 'initial'", cwd=newselftestdir, shell=True) + + # Tried to used bitbake-layers add/remove but it requires recipe parsing and hence is too slow + subprocess.check_output("sed %s/conf/bblayers.conf -i -e 's#%s#%s#g'" % (newbuilddir, selftestdir, newselftestdir), cwd=newbuilddir, shell=True) + + # Relative paths in BBLAYERS only works when the new build dir share the same ascending node + if self.newbuilddir: + bblayers = subprocess.check_output("bitbake-getvar --value BBLAYERS | tail -1", cwd=builddir, shell=True, text=True) + if '..' in bblayers: + bblayers_abspath = [os.path.abspath(path) for path in bblayers.split()] + with open("%s/conf/bblayers.conf" % newbuilddir, "a") as f: + newbblayers = "# new bblayers to be used by selftest in the new build dir '%s'\n" % newbuilddir + newbblayers += 'BBLAYERS = "%s"\n' % ' '.join(bblayers_abspath) + f.write(newbblayers) + + # Rewrite builddir paths seen in environment variables + for e in os.environ: + # Rewrite paths that absolutely point inside builddir + # (e.g $builddir/conf/ would be rewritten but not $builddir/../bitbake/) + if builddir + "/" in os.environ[e] and builddir + "/" in os.path.abspath(os.environ[e]): + os.environ[e] = os.environ[e].replace(builddir + "/", newbuilddir + "/") + if os.environ[e].endswith(builddir): + os.environ[e] = os.environ[e].replace(builddir, newbuilddir) + + # Set SSTATE_DIR to match the parent SSTATE_DIR + subprocess.check_output("echo 'SSTATE_DIR ?= \"%s\"' >> %s/conf/local.conf" % (sstatedir, newbuilddir), cwd=newbuilddir, shell=True) + + os.chdir(newbuilddir) + + def patch_test(t): + if not hasattr(t, "tc"): + return + cp = t.tc.config_paths + for p in cp: + if selftestdir in cp[p] and newselftestdir not in cp[p]: + cp[p] = cp[p].replace(selftestdir, newselftestdir) + if builddir in cp[p] and newbuilddir not in cp[p]: + cp[p] = cp[p].replace(builddir, newbuilddir) + + def patch_suite(s): + for x in s: + if isinstance(x, unittest.TestSuite): + patch_suite(x) + else: + patch_test(x) + + patch_suite(suite) + + return (builddir, newbuilddir) + + def prepareSuite(self, suites, processes): + if processes: + from oeqa.core.utils.concurrencytest import ConcurrentTestSuite + + return ConcurrentTestSuite(suites, processes, self.setup_builddir, self.removebuilddir, self.bb_vars) + else: + return NonConcurrentTestSuite(suites, processes, self.setup_builddir, self.removebuilddir, self.bb_vars) def runTests(self, processes=None, machine=None, skips=[]): - if machine: - self.custommachine = machine - if machine == 'random': - self.custommachine = choice(self.machines) - self.logger.info('Run tests with custom MACHINE set to: %s' % \ - self.custommachine) return super(OESelftestTestContext, self).runTests(processes, skips) def listTests(self, display_type, machine=None): @@ -55,9 +180,6 @@ class OESelftestTestContextExecutor(OETestContextExecutor): group.add_argument('-a', '--run-all-tests', default=False, action="store_true", dest="run_all_tests", help='Run all (unhidden) tests') - group.add_argument('-R', '--skip-tests', required=False, action='store', - nargs='+', dest="skips", default=None, - help='Run all (unhidden) tests except the ones specified. Format should be <module>[.<class>[.<test_method>]]') group.add_argument('-r', '--run-tests', required=False, action='store', nargs='+', dest="run_tests", default=None, help='Select what tests to run (modules, classes or test methods). Format should be: <module>.<class>.<test_method>') @@ -72,11 +194,26 @@ class OESelftestTestContextExecutor(OETestContextExecutor): action="store_true", default=False, help='List all available tests.') - parser.add_argument('-j', '--num-processes', dest='processes', action='store', - type=int, help="number of processes to execute in parallel with") + parser.add_argument('-R', '--skip-tests', required=False, action='store', + nargs='+', dest="skips", default=None, + help='Skip the tests specified. Format should be <module>[.<class>[.<test_method>]]') + + def check_parallel_support(parameter): + if not parameter.isdigit(): + import argparse + raise argparse.ArgumentTypeError("argument -j/--num-processes: invalid int value: '%s' " % str(parameter)) + + processes = int(parameter) + if processes: + try: + import testtools, subunit + except ImportError: + print("Failed to import testtools or subunit, the testcases will run serially") + processes = None + return processes - parser.add_argument('--machine', required=False, choices=['random', 'all'], - help='Run tests on different machines (random/all).') + parser.add_argument('-j', '--num-processes', dest='processes', action='store', + type=check_parallel_support, help="number of processes to execute in parallel with") parser.add_argument('-t', '--select-tag', dest="select_tags", action='append', default=None, @@ -85,21 +222,12 @@ class OESelftestTestContextExecutor(OETestContextExecutor): action='append', default=None, help='Exclude all (unhidden) tests that match any of the specified tag(s). (exclude applies before select)') - parser.set_defaults(func=self.run) - - def _get_available_machines(self): - machines = [] + parser.add_argument('-K', '--keep-builddir', action='store_true', + help='Keep the test build directory even if all tests pass') - bbpath = self.tc_kwargs['init']['td']['BBPATH'].split(':') - - for path in bbpath: - found_machines = glob.glob(os.path.join(path, 'conf', 'machine', '*.conf')) - if found_machines: - for i in found_machines: - # eg: '/home/<user>/poky/meta-intel/conf/machine/intel-core2-32.conf' - machines.append(os.path.splitext(os.path.basename(i))[0]) - - return machines + parser.add_argument('-B', '--newbuilddir', help='New build directory to use for tests.') + parser.add_argument('-v', '--verbose', action='store_true') + parser.set_defaults(func=self.run) def _get_cases_paths(self, bbpath): cases_paths = [] @@ -131,30 +259,15 @@ class OESelftestTestContextExecutor(OETestContextExecutor): args.list_tests = 'name' self.tc_kwargs['init']['td'] = bbvars - self.tc_kwargs['init']['machines'] = self._get_available_machines() builddir = os.environ.get("BUILDDIR") self.tc_kwargs['init']['config_paths'] = {} - self.tc_kwargs['init']['config_paths']['testlayer_path'] = \ - get_test_layer() + self.tc_kwargs['init']['config_paths']['testlayer_path'] = get_test_layer(bbvars["BBLAYERS"]) self.tc_kwargs['init']['config_paths']['builddir'] = builddir - self.tc_kwargs['init']['config_paths']['localconf'] = \ - os.path.join(builddir, "conf/local.conf") - self.tc_kwargs['init']['config_paths']['localconf_backup'] = \ - os.path.join(builddir, "conf/local.conf.orig") - self.tc_kwargs['init']['config_paths']['localconf_class_backup'] = \ - os.path.join(builddir, "conf/local.conf.bk") - self.tc_kwargs['init']['config_paths']['bblayers'] = \ - os.path.join(builddir, "conf/bblayers.conf") - self.tc_kwargs['init']['config_paths']['bblayers_backup'] = \ - os.path.join(builddir, "conf/bblayers.conf.orig") - self.tc_kwargs['init']['config_paths']['bblayers_class_backup'] = \ - os.path.join(builddir, "conf/bblayers.conf.bk") - - copyfile(self.tc_kwargs['init']['config_paths']['localconf'], - self.tc_kwargs['init']['config_paths']['localconf_backup']) - copyfile(self.tc_kwargs['init']['config_paths']['bblayers'], - self.tc_kwargs['init']['config_paths']['bblayers_backup']) + self.tc_kwargs['init']['config_paths']['localconf'] = os.path.join(builddir, "conf/local.conf") + self.tc_kwargs['init']['config_paths']['bblayers'] = os.path.join(builddir, "conf/bblayers.conf") + self.tc_kwargs['init']['newbuilddir'] = args.newbuilddir + self.tc_kwargs['init']['keep_builddir'] = args.keep_builddir def tag_filter(tags): if args.exclude_tags: @@ -185,14 +298,14 @@ class OESelftestTestContextExecutor(OETestContextExecutor): os.chdir(builddir) if not "meta-selftest" in self.tc.td["BBLAYERS"]: - self.tc.logger.warning("meta-selftest layer not found in BBLAYERS, adding it") + self.tc.logger.info("meta-selftest layer not found in BBLAYERS, adding it") meta_selftestdir = os.path.join( self.tc.td["BBLAYERS_FETCH_DIR"], 'meta-selftest') if os.path.isdir(meta_selftestdir): - runCmd("bitbake-layers add-layer %s" %meta_selftestdir) + runCmd("bitbake-layers add-layer %s" % meta_selftestdir) # reload data is needed because a meta-selftest layer was add self.tc.td = get_bb_vars() - self.tc.config_paths['testlayer_path'] = get_test_layer() + self.tc.config_paths['testlayer_path'] = get_test_layer(self.tc.td["BBLAYERS"]) else: self.tc.logger.error("could not locate meta-selftest in:\n%s" % meta_selftestdir) raise OEQAPreRun @@ -230,8 +343,15 @@ class OESelftestTestContextExecutor(OETestContextExecutor): _add_layer_libs() - self.tc.logger.info("Running bitbake -e to test the configuration is valid/parsable") - runCmd("bitbake -e") + self.tc.logger.info("Checking base configuration is valid/parsable") + + with bb.tinfoil.Tinfoil(tracking=True) as tinfoil: + tinfoil.prepare(quiet=2, config_only=True) + d = tinfoil.config_data + vars = {} + vars['SSTATE_DIR'] = str(d.getVar('SSTATE_DIR')) + vars['BBLAYERS'] = str(d.getVar('BBLAYERS')) + self.tc.set_variables(vars) def get_json_result_dir(self, args): json_result_dir = os.path.join(self.tc.td["LOG_DIR"], 'oeqa') @@ -244,12 +364,14 @@ class OESelftestTestContextExecutor(OETestContextExecutor): import platform from oeqa.utils.metadata import metadata_from_bb metadata = metadata_from_bb() + oeselftest_metadata = get_oeselftest_metadata(args) configuration = {'TEST_TYPE': 'oeselftest', 'STARTTIME': args.test_start_time, 'MACHINE': self.tc.td["MACHINE"], 'HOST_DISTRO': oe.lsb.distro_identifier().replace(' ', '-'), 'HOST_NAME': metadata['hostname'], - 'LAYERS': metadata['layers']} + 'LAYERS': metadata['layers'], + 'OESELFTEST_METADATA': oeselftest_metadata} return configuration def get_result_id(self, configuration): @@ -279,61 +401,19 @@ class OESelftestTestContextExecutor(OETestContextExecutor): return rc - def _signal_clean_handler(self, signum, frame): - sys.exit(1) - def run(self, logger, args): self._process_args(logger, args) - signal.signal(signal.SIGTERM, self._signal_clean_handler) - rc = None try: - if args.machine: - logger.info('Custom machine mode enabled. MACHINE set to %s' % - args.machine) - - if args.machine == 'all': - results = [] - for m in self.tc_kwargs['init']['machines']: - self.tc_kwargs['run']['machine'] = m - results.append(self._internal_run(logger, args)) - - # XXX: the oe-selftest script only needs to know if one - # machine run fails - for r in results: - rc = r - if not r.wasSuccessful(): - break - - else: - self.tc_kwargs['run']['machine'] = args.machine - return self._internal_run(logger, args) - - else: - self.tc_kwargs['run']['machine'] = args.machine - rc = self._internal_run(logger, args) + rc = self._internal_run(logger, args) finally: config_paths = self.tc_kwargs['init']['config_paths'] - if os.path.exists(config_paths['localconf_backup']): - copyfile(config_paths['localconf_backup'], - config_paths['localconf']) - os.remove(config_paths['localconf_backup']) - - if os.path.exists(config_paths['bblayers_backup']): - copyfile(config_paths['bblayers_backup'], - config_paths['bblayers']) - os.remove(config_paths['bblayers_backup']) - - if os.path.exists(config_paths['localconf_class_backup']): - os.remove(config_paths['localconf_class_backup']) - if os.path.exists(config_paths['bblayers_class_backup']): - os.remove(config_paths['bblayers_class_backup']) output_link = os.path.join(os.path.dirname(args.output_log), "%s-results.log" % self.name) if os.path.lexists(output_link): - os.remove(output_link) + os.unlink(output_link) os.symlink(args.output_log, output_link) return rc |