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") | ||