diff options
Diffstat (limited to 'meta/lib/oeqa/selftest/cases/spdx.py')
| -rw-r--r-- | meta/lib/oeqa/selftest/cases/spdx.py | 256 |
1 files changed, 245 insertions, 11 deletions
diff --git a/meta/lib/oeqa/selftest/cases/spdx.py b/meta/lib/oeqa/selftest/cases/spdx.py index 05fc4e390b..8cd4e83ca2 100644 --- a/meta/lib/oeqa/selftest/cases/spdx.py +++ b/meta/lib/oeqa/selftest/cases/spdx.py | |||
| @@ -6,29 +6,39 @@ | |||
| 6 | 6 | ||
| 7 | import json | 7 | import json |
| 8 | import os | 8 | import os |
| 9 | import textwrap | ||
| 10 | import hashlib | ||
| 11 | from pathlib import Path | ||
| 9 | from oeqa.selftest.case import OESelftestTestCase | 12 | from oeqa.selftest.case import OESelftestTestCase |
| 10 | from oeqa.utils.commands import bitbake, get_bb_var, runCmd | 13 | from oeqa.utils.commands import bitbake, get_bb_var, get_bb_vars, runCmd |
| 14 | import oe.spdx30 | ||
| 11 | 15 | ||
| 12 | class SPDXCheck(OESelftestTestCase): | ||
| 13 | 16 | ||
| 17 | class SPDX22Check(OESelftestTestCase): | ||
| 14 | @classmethod | 18 | @classmethod |
| 15 | def setUpClass(cls): | 19 | def setUpClass(cls): |
| 16 | super(SPDXCheck, cls).setUpClass() | 20 | super().setUpClass() |
| 17 | bitbake("python3-spdx-tools-native") | 21 | bitbake("python3-spdx-tools-native") |
| 18 | bitbake("-c addto_recipe_sysroot python3-spdx-tools-native") | 22 | bitbake("-c addto_recipe_sysroot python3-spdx-tools-native") |
| 19 | 23 | ||
| 20 | def check_recipe_spdx(self, high_level_dir, spdx_file, target_name): | 24 | def check_recipe_spdx(self, high_level_dir, spdx_file, target_name): |
| 21 | config = """ | 25 | config = textwrap.dedent( |
| 22 | INHERIT += "create-spdx" | 26 | """\ |
| 23 | """ | 27 | INHERIT:remove = "create-spdx" |
| 28 | INHERIT += "create-spdx-2.2" | ||
| 29 | """ | ||
| 30 | ) | ||
| 24 | self.write_config(config) | 31 | self.write_config(config) |
| 25 | 32 | ||
| 26 | deploy_dir = get_bb_var("DEPLOY_DIR") | 33 | deploy_dir = get_bb_var("DEPLOY_DIR") |
| 27 | machine_var = get_bb_var("MACHINE") | 34 | arch_dir = get_bb_var("PACKAGE_ARCH", target_name) |
| 35 | spdx_version = get_bb_var("SPDX_VERSION") | ||
| 28 | # qemux86-64 creates the directory qemux86_64 | 36 | # qemux86-64 creates the directory qemux86_64 |
| 29 | machine_dir = machine_var.replace("-", "_") | 37 | #arch_dir = arch_var.replace("-", "_") |
| 30 | 38 | ||
| 31 | full_file_path = os.path.join(deploy_dir, "spdx", machine_dir, high_level_dir, spdx_file) | 39 | full_file_path = os.path.join( |
| 40 | deploy_dir, "spdx", spdx_version, arch_dir, high_level_dir, spdx_file | ||
| 41 | ) | ||
| 32 | 42 | ||
| 33 | try: | 43 | try: |
| 34 | os.remove(full_file_path) | 44 | os.remove(full_file_path) |
| @@ -43,8 +53,13 @@ INHERIT += "create-spdx" | |||
| 43 | self.assertNotEqual(report, None) | 53 | self.assertNotEqual(report, None) |
| 44 | self.assertNotEqual(report["SPDXID"], None) | 54 | self.assertNotEqual(report["SPDXID"], None) |
| 45 | 55 | ||
| 46 | python = os.path.join(get_bb_var('STAGING_BINDIR', 'python3-spdx-tools-native'), 'nativepython3') | 56 | python = os.path.join( |
| 47 | validator = os.path.join(get_bb_var('STAGING_BINDIR', 'python3-spdx-tools-native'), 'pyspdxtools') | 57 | get_bb_var("STAGING_BINDIR", "python3-spdx-tools-native"), |
| 58 | "nativepython3", | ||
| 59 | ) | ||
| 60 | validator = os.path.join( | ||
| 61 | get_bb_var("STAGING_BINDIR", "python3-spdx-tools-native"), "pyspdxtools" | ||
| 62 | ) | ||
| 48 | result = runCmd("{} {} -i {}".format(python, validator, filename)) | 63 | result = runCmd("{} {} -i {}".format(python, validator, filename)) |
| 49 | 64 | ||
| 50 | self.assertExists(full_file_path) | 65 | self.assertExists(full_file_path) |
| @@ -52,3 +67,222 @@ INHERIT += "create-spdx" | |||
| 52 | 67 | ||
| 53 | def test_spdx_base_files(self): | 68 | def test_spdx_base_files(self): |
| 54 | self.check_recipe_spdx("packages", "base-files.spdx.json", "base-files") | 69 | self.check_recipe_spdx("packages", "base-files.spdx.json", "base-files") |
| 70 | |||
| 71 | def test_spdx_tar(self): | ||
| 72 | self.check_recipe_spdx("packages", "tar.spdx.json", "tar") | ||
| 73 | |||
| 74 | |||
| 75 | class SPDX3CheckBase(object): | ||
| 76 | """ | ||
| 77 | Base class for checking SPDX 3 based tests | ||
| 78 | """ | ||
| 79 | |||
| 80 | def check_spdx_file(self, filename): | ||
| 81 | self.assertExists(filename) | ||
| 82 | |||
| 83 | # Read the file | ||
| 84 | objset = oe.spdx30.SHACLObjectSet() | ||
| 85 | with open(filename, "r") as f: | ||
| 86 | d = oe.spdx30.JSONLDDeserializer() | ||
| 87 | d.read(f, objset) | ||
| 88 | |||
| 89 | return objset | ||
| 90 | |||
| 91 | def check_recipe_spdx(self, target_name, spdx_path, *, task=None, extraconf=""): | ||
| 92 | config = ( | ||
| 93 | textwrap.dedent( | ||
| 94 | f"""\ | ||
| 95 | INHERIT:remove = "create-spdx" | ||
| 96 | INHERIT += "{self.SPDX_CLASS}" | ||
| 97 | """ | ||
| 98 | ) | ||
| 99 | + textwrap.dedent(extraconf) | ||
| 100 | ) | ||
| 101 | |||
| 102 | self.write_config(config) | ||
| 103 | |||
| 104 | if task: | ||
| 105 | bitbake(f"-c {task} {target_name}") | ||
| 106 | else: | ||
| 107 | bitbake(target_name) | ||
| 108 | |||
| 109 | filename = spdx_path.format( | ||
| 110 | **get_bb_vars( | ||
| 111 | [ | ||
| 112 | "DEPLOY_DIR_IMAGE", | ||
| 113 | "DEPLOY_DIR_SPDX", | ||
| 114 | "MACHINE", | ||
| 115 | "MACHINE_ARCH", | ||
| 116 | "SDKMACHINE", | ||
| 117 | "SDK_DEPLOY", | ||
| 118 | "SPDX_VERSION", | ||
| 119 | "SSTATE_PKGARCH", | ||
| 120 | "TOOLCHAIN_OUTPUTNAME", | ||
| 121 | ], | ||
| 122 | target_name, | ||
| 123 | ) | ||
| 124 | ) | ||
| 125 | |||
| 126 | return self.check_spdx_file(filename) | ||
| 127 | |||
| 128 | def check_objset_missing_ids(self, objset): | ||
| 129 | for o in objset.foreach_type(oe.spdx30.SpdxDocument): | ||
| 130 | doc = o | ||
| 131 | break | ||
| 132 | else: | ||
| 133 | self.assertTrue(False, "Unable to find SpdxDocument") | ||
| 134 | |||
| 135 | missing_ids = objset.missing_ids - set(i.externalSpdxId for i in doc.import_) | ||
| 136 | if missing_ids: | ||
| 137 | self.assertTrue( | ||
| 138 | False, | ||
| 139 | "The following SPDXIDs are unresolved:\n " + "\n ".join(missing_ids), | ||
| 140 | ) | ||
| 141 | |||
| 142 | |||
| 143 | class SPDX30Check(SPDX3CheckBase, OESelftestTestCase): | ||
| 144 | SPDX_CLASS = "create-spdx-3.0" | ||
| 145 | |||
| 146 | def test_base_files(self): | ||
| 147 | self.check_recipe_spdx( | ||
| 148 | "base-files", | ||
| 149 | "{DEPLOY_DIR_SPDX}/{MACHINE_ARCH}/packages/package-base-files.spdx.json", | ||
| 150 | ) | ||
| 151 | |||
| 152 | def test_gcc_include_source(self): | ||
| 153 | objset = self.check_recipe_spdx( | ||
| 154 | "gcc", | ||
| 155 | "{DEPLOY_DIR_SPDX}/{SSTATE_PKGARCH}/recipes/recipe-gcc.spdx.json", | ||
| 156 | extraconf="""\ | ||
| 157 | SPDX_INCLUDE_SOURCES = "1" | ||
| 158 | """, | ||
| 159 | ) | ||
| 160 | |||
| 161 | gcc_pv = get_bb_var("PV", "gcc") | ||
| 162 | filename = f"gcc-{gcc_pv}/README" | ||
| 163 | found = False | ||
| 164 | for software_file in objset.foreach_type(oe.spdx30.software_File): | ||
| 165 | if software_file.name == filename: | ||
| 166 | found = True | ||
| 167 | self.logger.info( | ||
| 168 | f"The spdxId of {filename} in recipe-gcc.spdx.json is {software_file.spdxId}" | ||
| 169 | ) | ||
| 170 | break | ||
| 171 | |||
| 172 | self.assertTrue( | ||
| 173 | found, f"Not found source file {filename} in recipe-gcc.spdx.json\n" | ||
| 174 | ) | ||
| 175 | |||
| 176 | def test_core_image_minimal(self): | ||
| 177 | objset = self.check_recipe_spdx( | ||
| 178 | "core-image-minimal", | ||
| 179 | "{DEPLOY_DIR_IMAGE}/core-image-minimal-{MACHINE}.rootfs.spdx.json", | ||
| 180 | ) | ||
| 181 | |||
| 182 | # Document should be fully linked | ||
| 183 | self.check_objset_missing_ids(objset) | ||
| 184 | |||
| 185 | def test_core_image_minimal_sdk(self): | ||
| 186 | objset = self.check_recipe_spdx( | ||
| 187 | "core-image-minimal", | ||
| 188 | "{SDK_DEPLOY}/{TOOLCHAIN_OUTPUTNAME}.spdx.json", | ||
| 189 | task="populate_sdk", | ||
| 190 | ) | ||
| 191 | |||
| 192 | # Document should be fully linked | ||
| 193 | self.check_objset_missing_ids(objset) | ||
| 194 | |||
| 195 | def test_baremetal_helloworld(self): | ||
| 196 | objset = self.check_recipe_spdx( | ||
| 197 | "baremetal-helloworld", | ||
| 198 | "{DEPLOY_DIR_IMAGE}/baremetal-helloworld-image-{MACHINE}.spdx.json", | ||
| 199 | extraconf="""\ | ||
| 200 | TCLIBC = "baremetal" | ||
| 201 | """, | ||
| 202 | ) | ||
| 203 | |||
| 204 | # Document should be fully linked | ||
| 205 | self.check_objset_missing_ids(objset) | ||
| 206 | |||
| 207 | def test_extra_opts(self): | ||
| 208 | HOST_SPDXID = "http://foo.bar/spdx/bar2" | ||
| 209 | |||
| 210 | EXTRACONF = textwrap.dedent( | ||
| 211 | f"""\ | ||
| 212 | SPDX_INVOKED_BY_name = "CI Tool" | ||
| 213 | SPDX_INVOKED_BY_type = "software" | ||
| 214 | |||
| 215 | SPDX_ON_BEHALF_OF_name = "John Doe" | ||
| 216 | SPDX_ON_BEHALF_OF_type = "person" | ||
| 217 | SPDX_ON_BEHALF_OF_id_email = "John.Doe@noreply.com" | ||
| 218 | |||
| 219 | SPDX_PACKAGE_SUPPLIER_name = "ACME Embedded Widgets" | ||
| 220 | SPDX_PACKAGE_SUPPLIER_type = "organization" | ||
| 221 | |||
| 222 | SPDX_AUTHORS += "authorA" | ||
| 223 | SPDX_AUTHORS_authorA_ref = "SPDX_ON_BEHALF_OF" | ||
| 224 | |||
| 225 | SPDX_BUILD_HOST = "host" | ||
| 226 | |||
| 227 | SPDX_IMPORTS += "host" | ||
| 228 | SPDX_IMPORTS_host_spdxid = "{HOST_SPDXID}" | ||
| 229 | |||
| 230 | SPDX_INCLUDE_BUILD_VARIABLES = "1" | ||
| 231 | SPDX_INCLUDE_BITBAKE_PARENT_BUILD = "1" | ||
| 232 | SPDX_INCLUDE_TIMESTAMPS = "1" | ||
| 233 | |||
| 234 | SPDX_PRETTY = "1" | ||
| 235 | """ | ||
| 236 | ) | ||
| 237 | extraconf_hash = hashlib.sha1(EXTRACONF.encode("utf-8")).hexdigest() | ||
| 238 | |||
| 239 | objset = self.check_recipe_spdx( | ||
| 240 | "core-image-minimal", | ||
| 241 | "{DEPLOY_DIR_IMAGE}/core-image-minimal-{MACHINE}.rootfs.spdx.json", | ||
| 242 | # Many SPDX variables do not trigger a rebuild, since they are | ||
| 243 | # intended to record information at the time of the build. As such, | ||
| 244 | # the extra configuration alone may not trigger a rebuild, and even | ||
| 245 | # if it does, the task hash won't necessarily be unique. In order | ||
| 246 | # to make sure rebuilds happen, but still allow these test objects | ||
| 247 | # to be pulled from sstate (e.g. remain reproducible), change the | ||
| 248 | # namespace prefix to include the hash of the extra configuration | ||
| 249 | extraconf=textwrap.dedent( | ||
| 250 | f"""\ | ||
| 251 | SPDX_NAMESPACE_PREFIX = "http://spdx.org/spdxdocs/{extraconf_hash}" | ||
| 252 | """ | ||
| 253 | ) | ||
| 254 | + EXTRACONF, | ||
| 255 | ) | ||
| 256 | |||
| 257 | # Document should be fully linked | ||
| 258 | self.check_objset_missing_ids(objset) | ||
| 259 | |||
| 260 | for o in objset.foreach_type(oe.spdx30.SoftwareAgent): | ||
| 261 | if o.name == "CI Tool": | ||
| 262 | break | ||
| 263 | else: | ||
| 264 | self.assertTrue(False, "Unable to find software tool") | ||
| 265 | |||
| 266 | for o in objset.foreach_type(oe.spdx30.Person): | ||
| 267 | if o.name == "John Doe": | ||
| 268 | break | ||
| 269 | else: | ||
| 270 | self.assertTrue(False, "Unable to find person") | ||
| 271 | |||
| 272 | for o in objset.foreach_type(oe.spdx30.Organization): | ||
| 273 | if o.name == "ACME Embedded Widgets": | ||
| 274 | break | ||
| 275 | else: | ||
| 276 | self.assertTrue(False, "Unable to find organization") | ||
| 277 | |||
| 278 | for o in objset.foreach_type(oe.spdx30.SpdxDocument): | ||
| 279 | doc = o | ||
| 280 | break | ||
| 281 | else: | ||
| 282 | self.assertTrue(False, "Unable to find SpdxDocument") | ||
| 283 | |||
| 284 | for i in doc.import_: | ||
| 285 | if i.externalSpdxId == HOST_SPDXID: | ||
| 286 | break | ||
| 287 | else: | ||
| 288 | self.assertTrue(False, "Unable to find imported Host SpdxID") | ||
