/
/
1"""Tests for the license checks of the package safety script."""
2
3from __future__ import annotations
4
5from typing import Any
6
7import pytest
8
9from scripts import check_package_safety
10from scripts.check_package_safety import (
11 check_license_compatibility,
12 check_package,
13 get_package_license,
14)
15
16
17def pypi_info(**overrides: Any) -> dict[str, Any]:
18 """
19 Return the `info` section of a PyPI JSON response.
20
21 :param overrides: Fields to set on top of the (empty) license metadata.
22 """
23 return {"license": None, "license_expression": None, "classifiers": [], **overrides}
24
25
26@pytest.mark.parametrize(
27 ("info", "expected"),
28 [
29 # PEP 639: the SPDX expression is the only license metadata (chardet 7.6.0)
30 (pypi_info(license_expression="0BSD"), "0BSD"),
31 (pypi_info(license_expression="MIT OR Apache-2.0"), "MIT OR Apache-2.0"),
32 # the SPDX expression wins over the less precise legacy field and classifiers
33 (
34 pypi_info(
35 license_expression="AGPL-3.0-only",
36 classifiers=["License :: OSI Approved :: GNU Affero General Public License v3"],
37 ),
38 "AGPL-3.0-only",
39 ),
40 # packages without an SPDX expression fall back to those
41 (pypi_info(license="Apache-2.0"), "Apache-2.0"),
42 (pypi_info(classifiers=["License :: OSI Approved :: MIT License"]), "MIT License"),
43 (
44 pypi_info(
45 license="Apache-2.0",
46 classifiers=["License :: OSI Approved :: Apache Software License"],
47 ),
48 "Apache-2.0",
49 ),
50 (
51 pypi_info(classifiers=["Programming Language :: Python", "License :: OSI Approved"]),
52 "Unknown",
53 ),
54 # several classifiers: the one that fails the check decides, whatever its position
55 (
56 pypi_info(
57 classifiers=[
58 "License :: OSI Approved :: MIT License",
59 "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
60 ]
61 ),
62 "GNU General Public License v3 (GPLv3)",
63 ),
64 (
65 pypi_info(
66 classifiers=[
67 "License :: OSI Approved :: Apache Software License",
68 "License :: OSI Approved :: MIT License",
69 ]
70 ),
71 "Apache Software License",
72 ),
73 # two-part classifiers name a license too, and must not be hidden by a permissive one
74 (
75 pypi_info(
76 classifiers=[
77 "License :: Other/Proprietary License",
78 "License :: OSI Approved :: MIT License",
79 ]
80 ),
81 "Other/Proprietary License",
82 ),
83 (pypi_info(classifiers=["License :: Public Domain"]), "Public Domain"),
84 # nothing at all to go on
85 (pypi_info(), "Unknown"),
86 (pypi_info(license=" "), "Unknown"),
87 ],
88)
89def test_get_package_license(info: dict[str, Any], expected: str) -> None:
90 """Test the license is resolved from any of the fields PyPI exposes it in."""
91 assert get_package_license(info)[0] == expected
92
93
94@pytest.mark.parametrize(
95 ("info", "expected"),
96 [
97 (pypi_info(license_expression="0BSD"), True),
98 (pypi_info(license="0BSD"), False),
99 (pypi_info(classifiers=["License :: OSI Approved :: MIT License"]), False),
100 (pypi_info(), False),
101 ],
102)
103def test_get_package_license_reports_spdx(info: dict[str, Any], expected: bool) -> None:
104 """Test only a PEP 639 expression is reported as one."""
105 assert get_package_license(info)[1] is expected
106
107
108@pytest.mark.parametrize(
109 ("license_str", "expected"),
110 [
111 # an expression is validated by PyPI, so what the evaluator rejects is simply not allowed,
112 # rather than wording we failed to read
113 ("MIT AND Frobnicate-1.0", False),
114 # a malformed expression names nothing we can check, so it is not compatible either
115 ("MIT OR AND", False),
116 ("MIT WITH OR", False),
117 # "or later" is a single marker, not a way to dress up an unknown identifier
118 ("MIT++++", False),
119 ("LGPL-2.1+", True),
120 ("MIT OR (Apache-2.0", False),
121 ("BSD-3-Clause-No-Nuclear-License-2014", False),
122 ("LicenseRef-Proprietary", False),
123 ("0BSD", True),
124 ("MIT OR Apache-2.0", True),
125 # an alternative we do not know does not spoil one we do
126 ("Frobnicate-1.0 OR MIT", True),
127 ("Apache-2.0 WITH LLVM-exception", True),
128 ],
129)
130def test_spdx_expressions_are_not_guessed_at(license_str: str, expected: bool) -> None:
131 """Test an SPDX expression is judged on its identifiers only."""
132 assert check_license_compatibility(license_str, True)[0] is expected
133
134
135@pytest.mark.parametrize(
136 "license_str",
137 [
138 # SPDX identifiers as used in a PEP 639 expression
139 "0BSD",
140 "MIT",
141 "MIT-0",
142 "Apache-2.0",
143 "BSD-3-Clause",
144 "MPL-2.0",
145 "LGPL-2.1-or-later",
146 "MIT OR Apache-2.0",
147 "Apache-2.0 OR BSD-3-Clause",
148 "BSD-3-Clause AND 0BSD AND MIT AND Zlib AND CC0-1.0",
149 "MPL-2.0 AND (Apache-2.0 OR MIT)",
150 "Apache-2.0 AND Apache-2.0 WITH LLVM-exception AND BSD-2-Clause AND MIT",
151 # legacy license strings and classifier names keep working
152 "BSD",
153 "MIT License",
154 "Apache Software License",
155 "GNU Lesser General Public License v3 (LGPLv3)",
156 "ISC License (ISCL)",
157 "PSFL",
158 "LGPLv2+",
159 "Public Domain",
160 # a plain license field can hold an expression too (aiohttp publishes this one)
161 "Apache-2.0 AND MIT",
162 # ...while prose that merely contains the word "and" is still matched on its wording
163 "MIT License\n\nPermission is hereby granted, free of charge, to any person obtaining a"
164 " copy of this software and associated documentation files, to deal in the Software"
165 " without restriction, including without limitation the rights to use and to permit"
166 " persons to whom the Software is furnished to do so.",
167 "The MIT License (MIT)",
168 "CC0 1.0 Universal",
169 # spelling variants of the same licenses
170 "MPL 2.0",
171 "Apache 2.0 License",
172 # a custom license alongside one we accept still leaves a usable option
173 "MIT OR LicenseRef-Proprietary",
174 # an exception only widens what the license allows, so the license itself decides
175 "Zlib WITH LLVM-exception",
176 "LGPL-3.0-only WITH LGPL-3.0-linking-exception",
177 # prose is matched on its wording; the groups in it are not an expression to refuse
178 "MIT License (a) (b) (c) (d) (e) (f) (g) (h) (i) (j) (k) (l) and so on",
179 ],
180)
181def test_compatible_licenses(license_str: str) -> None:
182 """Test permissive licenses are accepted."""
183 compatible, status = check_license_compatibility(license_str)
184 assert compatible, status
185
186
187@pytest.mark.parametrize(
188 ("license_str", "expected_status"),
189 [
190 ("GPL-3.0-only", "Incompatible copyleft license (GPL-3.0-only)"),
191 ("AGPL-3.0-only", "Incompatible copyleft license (AGPL-3.0-only)"),
192 # a permissive term must not mask a copyleft one it is combined with, whether or not the
193 # expression around it parses
194 ("MIT AND GPL-3.0-only", "Incompatible copyleft license (MIT AND GPL-3.0-only)"),
195 ("(GPL-3.0-only AND MIT", "Incompatible copyleft license"),
196 ("MIT OR (GPL-3.0-only", "Incompatible copyleft license"),
197 ("LicenseRef-MIT Custom", "Unknown/unverified license"),
198 # only understood in part is not understood: "Zlib" alone would be compatible
199 ("Zlib plus custom terms", "Unknown/unverified license"),
200 ("GNU General Public License v3 (GPLv3)", "Incompatible copyleft license"),
201 # an LGPL term in the string does not excuse a GPL one standing next to it
202 ("LGPL plus GPL terms", "Incompatible copyleft license"),
203 # an exception widens a license, so a copyleft one cannot be hiding behind "WITH"
204 ("MIT WITH GPL-3.0-only", "Incompatible copyleft license"),
205 ("Apache-2.0 AND MIT WITH GPL-3.0-only", "Incompatible copyleft license"),
206 ("LGPL-2.1-or-later AND GPL-3.0-only", "Incompatible copyleft license"),
207 ("Frobnicate-1.0", "Unknown/unverified license (Frobnicate-1.0)"),
208 # a license name has to be named, not spelled out by unrelated words running together
209 ("This copyright notice shall be included in all copies", "Unknown/unverified license"),
210 ("Redistribution is permitted for internal use only", "Unknown/unverified license"),
211 ("SUBMITTED-1.0", "Unknown/unverified license"),
212 ("Mitigation License 1.0", "Unknown/unverified license"),
213 # a name that merely starts like one we know is not that license
214 ("MITX", "Unknown/unverified license"),
215 # prose that says the opposite of the license it names
216 ("This software is not in the public domain. All rights reserved.", "Unknown/unverified"),
217 ("ISC2", "Unknown/unverified license"),
218 ("Internal use only, do not transmit", "Unknown/unverified license"),
219 # a value that joins licenses is read as an expression, whichever field it came from
220 ("MIT AND Proprietary", "Unknown/unverified license"),
221 ("MIT AND(Proprietary)", "Unknown/unverified license"),
222 ("MIT AND (Proprietary", "Unknown/unverified license"),
223 ("Other/Proprietary License", "Unknown/unverified license"),
224 # a custom license is never pre-approved, not even when its name reads permissive
225 (
226 "LicenseRef-Proprietary-MIT-Terms",
227 "Unknown/unverified license (LicenseRef-Proprietary-MIT-Terms)",
228 ),
229 ("Unknown", "No license information"),
230 ("", "No license information"),
231 # nesting deep enough to exhaust the stack is refused, not approved on the name inside
232 ("(" * 333 + "MIT" + ")" * 333, "Unknown/unverified license"),
233 ],
234)
235def test_incompatible_licenses(license_str: str, expected_status: str) -> None:
236 """Test copyleft and unrecognised licenses are rejected."""
237 compatible, status = check_license_compatibility(license_str)
238 assert not compatible
239 assert status.startswith(expected_status)
240
241
242def test_check_package_reads_license_expression(monkeypatch: pytest.MonkeyPatch) -> None:
243 """Test a package that only declares an SPDX expression passes the license check."""
244 metadata = {
245 "info": {
246 "version": "7.6.0",
247 "license": None,
248 "license_expression": "0BSD",
249 "classifiers": [],
250 "author": "Dan Blanchard",
251 "summary": "Universal encoding detector",
252 "project_urls": {"Homepage": "https://github.com/chardet/chardet"},
253 },
254 "releases": {
255 f"{major}.0.0": [{"upload_time": "2015-01-01T00:00:00"}] for major in range(1, 5)
256 },
257 }
258 monkeypatch.setattr(check_package_safety, "get_pypi_metadata", lambda _: metadata)
259
260 result = check_package("chardet")
261
262 assert result["license"] == "0BSD"
263 assert result["automated_checks"]["license_compatible"]
264 assert result["check_details"]["license"] == "Compatible (0BSD)"
265 assert not [warning for warning in result["warnings"] if "License" in warning]
266