diff options
Diffstat (limited to 'meta/lib/oeqa/selftest/cases/spdx.py')
-rw-r--r-- | meta/lib/oeqa/selftest/cases/spdx.py | 288 |
1 files changed, 288 insertions, 0 deletions
diff --git a/meta/lib/oeqa/selftest/cases/spdx.py b/meta/lib/oeqa/selftest/cases/spdx.py new file mode 100644 index 0000000000..8cd4e83ca2 --- /dev/null +++ b/meta/lib/oeqa/selftest/cases/spdx.py | |||
@@ -0,0 +1,288 @@ | |||
1 | # | ||
2 | # Copyright OpenEmbedded Contributors | ||
3 | # | ||
4 | # SPDX-License-Identifier: MIT | ||
5 | # | ||
6 | |||
7 | import json | ||
8 | import os | ||
9 | import textwrap | ||
10 | import hashlib | ||
11 | from pathlib import Path | ||
12 | from oeqa.selftest.case import OESelftestTestCase | ||
13 | from oeqa.utils.commands import bitbake, get_bb_var, get_bb_vars, runCmd | ||
14 | import oe.spdx30 | ||
15 | |||
16 | |||
17 | class SPDX22Check(OESelftestTestCase): | ||
18 | @classmethod | ||
19 | def setUpClass(cls): | ||
20 | super().setUpClass() | ||
21 | bitbake("python3-spdx-tools-native") | ||
22 | bitbake("-c addto_recipe_sysroot python3-spdx-tools-native") | ||
23 | |||
24 | def check_recipe_spdx(self, high_level_dir, spdx_file, target_name): | ||
25 | config = textwrap.dedent( | ||
26 | """\ | ||
27 | INHERIT:remove = "create-spdx" | ||
28 | INHERIT += "create-spdx-2.2" | ||
29 | """ | ||
30 | ) | ||
31 | self.write_config(config) | ||
32 | |||
33 | deploy_dir = get_bb_var("DEPLOY_DIR") | ||
34 | arch_dir = get_bb_var("PACKAGE_ARCH", target_name) | ||
35 | spdx_version = get_bb_var("SPDX_VERSION") | ||
36 | # qemux86-64 creates the directory qemux86_64 | ||
37 | #arch_dir = arch_var.replace("-", "_") | ||
38 | |||
39 | full_file_path = os.path.join( | ||
40 | deploy_dir, "spdx", spdx_version, arch_dir, high_level_dir, spdx_file | ||
41 | ) | ||
42 | |||
43 | try: | ||
44 | os.remove(full_file_path) | ||
45 | except FileNotFoundError: | ||
46 | pass | ||
47 | |||
48 | bitbake("%s -c create_spdx" % target_name) | ||
49 | |||
50 | def check_spdx_json(filename): | ||
51 | with open(filename) as f: | ||
52 | report = json.load(f) | ||
53 | self.assertNotEqual(report, None) | ||
54 | self.assertNotEqual(report["SPDXID"], None) | ||
55 | |||
56 | python = os.path.join( | ||
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 | ) | ||
63 | result = runCmd("{} {} -i {}".format(python, validator, filename)) | ||
64 | |||
65 | self.assertExists(full_file_path) | ||
66 | result = check_spdx_json(full_file_path) | ||
67 | |||
68 | def test_spdx_base_files(self): | ||
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") | ||