From: John Ripple <john.ripple@keysight.com>
To: openembedded-core@lists.openembedded.org
Cc: John Ripple <john.ripple@keysight.com>
Subject: [OE-core] [meta][PATCH 2/3] oeqa/selftest: Add go-vendor.bbclass selftest suite
Date: Tue, 7 Jul 2026 15:14:55 -0600 [thread overview]
Message-ID: <20260707211507.2585072-2-john.ripple@keysight.com> (raw)
In-Reply-To: <cover.1783457902.git.john.ripple@keysight.com>
Add govendor.py with two test classes that cover the behaviors fixed and
defined in go-vendor.bbclass:
GoVendorSrcUriTests (offline, ~10 s): unit tests for go_src_uri() parsed
via bb.tinfoil; verifies the basic URI shape, path/pathmajor/subdir/replaces
encoding, and that non-git VCS omits the git-specific nobranch/protocol flags.
GoVendorTaskTests: runs do_go_vendor once against the real recipetool-go-test
project and checks that external deps land in vendor/, *_test.go files are
excluded, pathmajor=/v5 falls back to the repo root when no v5/ subdir exists,
modules.txt is reproduced in vendor/, a local replacement (./is) appears as
a symlink, and the S/src/GO_IMPORT/vendor -> S/src/import/vendor symlink is
created.
Signed-off-by: John Ripple <john.ripple@keysight.com>
---
meta/lib/oeqa/selftest/cases/govendor.py | 264 +++++++++++++++++++++++
1 file changed, 264 insertions(+)
create mode 100644 meta/lib/oeqa/selftest/cases/govendor.py
diff --git a/meta/lib/oeqa/selftest/cases/govendor.py b/meta/lib/oeqa/selftest/cases/govendor.py
new file mode 100644
index 0000000000..a56cb93698
--- /dev/null
+++ b/meta/lib/oeqa/selftest/cases/govendor.py
@@ -0,0 +1,264 @@
+#
+# Copyright OpenEmbedded Contributors
+#
+# SPDX-License-Identifier: MIT
+#
+# Comprehensive tests for go-vendor.bbclass:
+# GoVendorSrcUriTests -- unit tests for the go_src_uri() helper (offline, ~10 s)
+# GoVendorTaskTests -- integration tests using the real recipetool-go-test project
+# (network fetch of git.yoctoproject.org + github.com/godbus/dbus)
+
+import os
+import shutil
+import tempfile
+
+from oeqa.selftest.case import OESelftestTestCase
+from oeqa.utils.commands import runCmd, bitbake, get_bb_var, create_temp_layer
+
+# Module-level temp layer shared by both test classes
+templayerdir = None
+
+
+def setUpModule():
+ global templayerdir
+ templayerdir = tempfile.mkdtemp(prefix='govendorqa')
+ create_temp_layer(templayerdir, 'selftestgovendor')
+ runCmd('bitbake-layers add-layer %s' % templayerdir)
+
+
+def tearDownModule():
+ runCmd('bitbake-layers remove-layer %s' % templayerdir, ignore_status=True)
+ runCmd('rm -rf %s' % templayerdir)
+
+
+# ---------------------------------------------------------------------------
+# Class 1: go_src_uri() helper function
+# ---------------------------------------------------------------------------
+
+class GoVendorSrcUriTests(OESelftestTestCase):
+ """Tests for the go_src_uri() Python helper in go-vendor.bbclass.
+
+ Parses a single fixture recipe via bb.tinfoil (config_only=True) once per
+ class so the function runs inside the real BitBake class-loading machinery.
+ All tests are offline and complete in ~10 s total.
+ """
+
+ @classmethod
+ def setUpClass(cls):
+ import bb.tinfoil
+ super().setUpClass()
+
+ recipe_dir = os.path.join(templayerdir, 'recipes-test-src-uri',
+ 'go-vendor-src-uri')
+ os.makedirs(recipe_dir, exist_ok=True)
+ recipe_path = os.path.join(recipe_dir, 'go-vendor-src-uri_1.0.bb')
+
+ with open(recipe_path, 'w') as f:
+ f.write(
+ 'SUMMARY = "Selftest fixture: go_src_uri parameter variants"\n'
+ 'LICENSE = "CLOSED"\n'
+ '\n'
+ 'inherit go-vendor\n'
+ '\n'
+ 'GO_IMPORT = "example.com/testpkg"\n'
+ '\n'
+ 'GO_SRC_URI_BASIC = "${@go_src_uri(\'github.com/foo/bar\', \'v1.0.0\')}"\n'
+ 'GO_SRC_URI_PATH = "${@go_src_uri(\'github.com/foo/bar\', \'v2.1.0\', path=\'github.com/foo/bar/v2\')}"\n'
+ 'GO_SRC_URI_PATHMAJOR = "${@go_src_uri(\'github.com/foo/bar\', \'v2.0.0\', pathmajor=\'/v2\')}"\n'
+ 'GO_SRC_URI_SUBDIR = "${@go_src_uri(\'github.com/foo/bar\', \'v1.0.0\', subdir=\'sub/pkg\')}"\n'
+ 'GO_SRC_URI_REPLACES = "${@go_src_uri(\'github.com/baz/qux\', \'v1.0.0\', replaces=\'../local/baz\')}"\n'
+ 'GO_SRC_URI_SVN = "${@go_src_uri(\'example.com/svnmod\', \'r42\', vcs=\'svn\')}"\n'
+ )
+
+ with bb.tinfoil.Tinfoil() as tinfoil:
+ tinfoil.prepare(config_only=True, quiet=2)
+ rd = tinfoil.parse_recipe_file(recipe_path)
+ cls.bp = rd.getVar('BP')
+ cls.uris = {k: rd.getVar('GO_SRC_URI_' + k)
+ for k in ('BASIC', 'PATH', 'PATHMAJOR', 'SUBDIR',
+ 'REPLACES', 'SVN')}
+
+ @classmethod
+ def tearDownClass(cls):
+ runCmd('rm -rf %s/recipes-test-src-uri' % templayerdir,
+ ignore_status=True)
+ super().tearDownClass()
+
+ # --- individual test methods -------------------------------------------
+
+ def test_basic(self):
+ """destsuffix uses expanded BP"""
+ uri = self.uris['BASIC']
+ expected_ds = 'destsuffix=%s/src/import/vendor.fetch/github.com/foo/bar@v1.0.0' % self.bp
+ self.assertIn(expected_ds, uri)
+ self.assertIn('go_module_path=github.com/foo/bar', uri)
+ self.assertIn('nobranch=1', uri)
+ self.assertIn('protocol=https', uri)
+ self.assertIn('is_go_dependency=1', uri)
+
+ def test_path_override(self):
+ """Explicit path= sets go_module_path independently of the repo URL."""
+ uri = self.uris['PATH']
+ self.assertIn('go_module_path=github.com/foo/bar/v2', uri)
+ expected_ds = 'destsuffix=%s/src/import/vendor.fetch/github.com/foo/bar@v2.1.0' % self.bp
+ self.assertIn(expected_ds, uri)
+
+ def test_pathmajor(self):
+ """pathmajor is encoded as go_pathmajor in the URI."""
+ self.assertIn('go_pathmajor=/v2', self.uris['PATHMAJOR'])
+
+ def test_subdir(self):
+ """subdir is encoded as go_subdir in the URI."""
+ self.assertIn('go_subdir=sub/pkg', self.uris['SUBDIR'])
+
+ def test_replaces(self):
+ """replaces is encoded as go_module_replacement in the URI."""
+ self.assertIn('go_module_replacement=../local/baz', self.uris['REPLACES'])
+
+ def test_non_git_vcs(self):
+ """Non-git VCS omits the git-specific nobranch and protocol=https flags."""
+ uri = self.uris['SVN']
+ self.assertIn('svn://example.com/svnmod', uri)
+ self.assertNotIn('nobranch', uri)
+ self.assertNotIn('protocol=https', uri)
+ self.assertIn('is_go_dependency=1', uri)
+
+
+# ---------------------------------------------------------------------------
+# Class 2: do_go_vendor task integration — real project
+# ---------------------------------------------------------------------------
+
+class GoVendorTaskTests(OESelftestTestCase):
+ """Integration tests for do_go_vendor using the real recipetool-go-test project.
+
+ The recipe fetches git.yoctoproject.org/recipetool-go-test (commit
+ c3e213c) as its main source and github.com/godbus/dbus/v5 v5.1.0 as an
+ external vendor dependency. The project also has a local module
+ replacement (github.com/matryer/is => ./is) that lives inside the main
+ source tree.
+
+ do_go_vendor runs ONCE in setUpClass; individual test methods inspect the
+ resulting vendor directory without re-running bitbake.
+
+ Verified behaviors:
+ - External dependency files are copied to vendor/
+ - *_test.go files are excluded from the vendor copy
+ - pathmajor fallback: when the /v5 subdir is absent, repo root is used
+ - modules.txt is reproduced inside vendor/
+ - Local module replacement creates a relative symlink in vendor/
+ - S/src/GO_IMPORT/vendor symlink is created pointing at S/src/import/vendor
+ """
+
+ # git.yoctoproject.org/recipetool-go-test main branch HEAD
+ SRCREV = 'c3e213c01b6c1406b430df03ef0d1ae77de5d2f7'
+ GO_IMPORT = 'git.yoctoproject.org/recipetool-go-test'
+ RECIPE_NAME = 'govendor-real-test'
+
+ @classmethod
+ def setUpClass(cls):
+ from oeqa.utils import ftools
+ super().setUpClass()
+
+ # overlayfs-user.bb in meta-selftest requires a machine-specific mount
+ # point that this test environment does not provide. Mask it so that
+ # bitbake can parse the full recipe universe without halting.
+ ftools.append_file(cls.testinc_path,
+ 'BBMASK += "meta-selftest/recipes-test/overlayfs-user"')
+
+ recipe_dir = os.path.join(templayerdir, 'recipes-test', cls.RECIPE_NAME)
+ files_dir = os.path.join(recipe_dir, 'files')
+ os.makedirs(files_dir, exist_ok=True)
+
+ # modules.txt for recipetool-go-test at SRCREV:
+ # - github.com/godbus/dbus/v5 v5.1.0 (external, fetched via go_src_uri)
+ # - github.com/matryer/is v1.4.1 (local replacement => ./is)
+ with open(os.path.join(files_dir, 'modules.txt'), 'w') as fh:
+ fh.write(
+ '# github.com/godbus/dbus/v5 v5.1.0\n'
+ '## explicit; go 1.18\n'
+ 'github.com/godbus/dbus/v5\n'
+ '# github.com/matryer/is v1.4.1 => ./is\n'
+ '## explicit; go 1.14\n'
+ 'github.com/matryer/is\n'
+ )
+
+ with open(os.path.join(recipe_dir, '%s_1.0.bb' % cls.RECIPE_NAME), 'w') as fh:
+ fh.write(
+ 'SUMMARY = "go-vendor integration test: recipetool-go-test"\n'
+ 'LICENSE = "MIT"\n'
+ 'LIC_FILES_CHKSUM = "file://src/${GO_IMPORT}/LICENSE;md5=4e3933dd47afbf115e484d11385fb3bd"\n'
+ '\n'
+ 'inherit go-vendor\n'
+ '\n'
+ 'GO_IMPORT = "%s"\n'
+ '\n'
+ 'SRC_URI = "\\\n'
+ ' git://${GO_IMPORT};name=recipetool-go-test;branch=main;protocol=https'
+ ';nobranch=1;destsuffix=${GO_SRCURI_DESTSUFFIX} \\\n'
+ ' ${@go_src_uri(\'github.com/godbus/dbus\', \'v5.1.0\', \\\n'
+ ' path=\'github.com/godbus/dbus/v5\', pathmajor=\'/v5\')} \\\n'
+ ' file://modules.txt \\\n'
+ ' "\n'
+ 'SRCREV_FORMAT = "recipetool-go-test"\n'
+ 'SRCREV_recipetool-go-test = "%s"\n'
+ 'SRCREV_github.com.godbus.dbus.v5 = "e523abc905595cf17fb0001a7d77eaaddfaa216d"\n'
+ % (cls.GO_IMPORT, cls.SRCREV)
+ )
+
+ bitbake('%s -c go_vendor' % cls.RECIPE_NAME)
+ cls.s_dir = get_bb_var('S', cls.RECIPE_NAME)
+ cls.vendor_dir = os.path.join(cls.s_dir, 'src', 'import', 'vendor')
+
+ @classmethod
+ def tearDownClass(cls):
+ runCmd('rm -rf %s/recipes-test' % templayerdir, ignore_status=True)
+ super().tearDownClass()
+
+ # --- test methods (all inspect cls.vendor_dir, no bitbake) ---------------
+
+ def test_external_dep_copied_to_vendor(self):
+ """godbus/dbus/v5 source files are copied into vendor/."""
+ dbus_dir = os.path.join(self.vendor_dir, 'github.com', 'godbus', 'dbus', 'v5')
+ self.assertTrue(os.path.isdir(dbus_dir),
+ 'vendor/github.com/godbus/dbus/v5/ not created')
+ self.assertTrue(os.path.isfile(os.path.join(dbus_dir, 'conn.go')),
+ 'conn.go not found in vendor/github.com/godbus/dbus/v5/')
+
+ def test_test_files_excluded_from_vendor(self):
+ """*_test.go files from godbus/dbus are not copied into vendor/."""
+ dbus_dir = os.path.join(self.vendor_dir, 'github.com', 'godbus', 'dbus', 'v5')
+ self.assertFalse(
+ os.path.isfile(os.path.join(dbus_dir, 'conn_test.go')),
+ 'conn_test.go should have been excluded from vendor by *_test.go pattern')
+
+ def test_pathmajor_falls_back_to_root(self):
+ """godbus/dbus v5.1.0 has no v5/ subdir; pathmajor=/v5 falls back to root."""
+ dbus_dir = os.path.join(self.vendor_dir, 'github.com', 'godbus', 'dbus', 'v5')
+ # auth.go is at the repo root of godbus/dbus, not inside a v5/ subdir.
+ self.assertTrue(os.path.isfile(os.path.join(dbus_dir, 'auth.go')),
+ 'auth.go not found; pathmajor=/v5 fallback to repo root did not work')
+
+ def test_modules_txt_in_vendor(self):
+ """modules.txt is reproduced inside vendor/ and lists the external dep."""
+ modules_txt = os.path.join(self.vendor_dir, 'modules.txt')
+ self.assertTrue(os.path.isfile(modules_txt))
+ with open(modules_txt) as fh:
+ content = fh.read()
+ self.assertIn('github.com/godbus/dbus/v5 v5.1.0', content)
+
+ def test_local_replacement_symlink(self):
+ """go.mod local replacement (./is) creates a symlink in vendor/ to S/src/GO_IMPORT/is."""
+ symlink = os.path.join(self.vendor_dir, 'github.com', 'matryer', 'is')
+ self.assertTrue(os.path.islink(symlink),
+ 'vendor/github.com/matryer/is should be a symlink for the ./is local replacement')
+ expected_target = os.path.join(self.s_dir, 'src', self.GO_IMPORT, 'is')
+ self.assertEqual(os.path.realpath(symlink), os.path.realpath(expected_target),
+ 'Local replacement symlink does not point to S/src/GO_IMPORT/is')
+
+ def test_go_import_vendor_symlink(self):
+ """do_go_vendor creates S/src/GO_IMPORT/vendor -> S/src/import/vendor."""
+ link = os.path.join(self.s_dir, 'src', self.GO_IMPORT, 'vendor')
+ self.assertTrue(os.path.islink(link),
+ 'S/src/GO_IMPORT/vendor symlink not created')
+ self.assertEqual(os.path.realpath(link), os.path.realpath(self.vendor_dir),
+ 'S/src/GO_IMPORT/vendor does not point to S/src/import/vendor')
next prev parent reply other threads:[~2026-07-07 21:15 UTC|newest]
Thread overview: 6+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-07-07 21:14 [OE Core] [meta][PATCH 0/3] Create selftest suite for go-vendor.bbclass John Ripple
2026-07-07 21:14 ` [OE-core] [meta][PATCH 1/3] go-vendor.bbclass: Fix test file exclusion and license propagation John Ripple
2026-07-07 21:14 ` John Ripple [this message]
2026-07-09 10:08 ` [OE-core] [meta][PATCH 2/3] oeqa/selftest: Add go-vendor.bbclass selftest suite Alexander Kanavin
2026-07-07 21:14 ` [OE-core] [meta][PATCH 3/3] oeqa/utils/metadata: Handle empty git repos in git_rev_info John Ripple
2026-07-09 10:10 ` Alexander Kanavin
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260707211507.2585072-2-john.ripple@keysight.com \
--to=john.ripple@keysight.com \
--cc=openembedded-core@lists.openembedded.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.