| # Copyright 2014 The ChromiumOS Authors | |
| # Use of this source code is governed by a BSD-style license that can be | |
| # found in the LICENSE file. | |
| """Test the license_lib module.""" | |
| import json | |
| import os | |
| from unittest import mock | |
| from chromite.lib import cros_build_lib | |
| from chromite.lib import cros_test_lib | |
| from chromite.lib import osutils | |
| from chromite.licensing import licenses_lib | |
| # pylint: disable=protected-access | |
| class LicenseLibTest(cros_test_lib.TempDirTestCase): | |
| """Limited tests for license_lib.""" | |
| LICENSE_PUBLIC = "Gentoo Package Stock" | |
| LICENSE_CUSTOM = "Custom" | |
| def setUp(self): | |
| """Sets up the filesystem for the tests.""" | |
| # Licenses for testing: | |
| # key - Name of the license. | |
| # contents - The contents of the license file itself. | |
| # dir - Location of the file containing the license text on the FS. | |
| # board - The board argument to use when testing the license. | |
| # type - The license type (gentoo stock or custom). | |
| # [skip_test] - Skip the tests for that license. | |
| self.licenses = { | |
| "TPL": { | |
| "contents": "Third Party License", | |
| "dir": "src/third_party/portage-stable/licenses/TPL", | |
| "board": "foo", | |
| "type": self.LICENSE_PUBLIC, | |
| }, | |
| "PUB": { | |
| "contents": "Public License", | |
| "dir": "src/third_party/chromiumos-overlay/licenses/PUB", | |
| "board": "foo", | |
| "type": self.LICENSE_CUSTOM, | |
| }, | |
| "CCL": { | |
| "contents": "ChromeOS Custom License", | |
| "dir": "src/private-overlays/chromeos-overlay/licenses/CCL", | |
| "board": "chromeos", | |
| "type": self.LICENSE_CUSTOM, | |
| }, | |
| "CPL": { | |
| "contents": "ChromeOS Partner License", | |
| "dir": ( | |
| "src/private-overlays/chromeos-partner-overlay/licenses/CPL" | |
| ), | |
| "board": "chromeos-partner", | |
| "type": self.LICENSE_CUSTOM, | |
| }, | |
| "FTL": { | |
| "contents": "Fake Test License", | |
| "dir": "src/overlays/overlay-foo/licenses/FTL", | |
| "board": "foo", | |
| "type": self.LICENSE_CUSTOM, | |
| }, | |
| "OOR": { | |
| "contents": "Out Of Reach", | |
| "dir": "src/overlays/overlay-bar/licenses/OOR", | |
| "skip_test": True, | |
| }, | |
| licenses_lib.TAINTED: { | |
| "contents": licenses_lib.TAINTED, | |
| "dir": "src/overlays/overlay-foo/licenses/TAINTED", | |
| "board": "foo", | |
| "type": licenses_lib.TAINTED, | |
| }, | |
| } | |
| # Overlays for testing. | |
| # dir: Overlay base directory. | |
| # repo-name: The repo-name setting for the overlay. | |
| # masters: The masters list for the overlay. | |
| overlays = [ | |
| { | |
| "dir": "src/overlays/overlay-foo", | |
| "repo-name": "foo", | |
| "masters": ["chromiumos", "portage-stable"], | |
| }, | |
| { | |
| "dir": "src/overlays/overlay-bar", | |
| "repo-name": "bar", | |
| "masters": [], | |
| }, | |
| { | |
| "dir": "src/private-overlays/overlay-foo-private", | |
| "repo-name": "foo-private", | |
| "masters": ["foo", "chromeos-partner"], | |
| }, | |
| { | |
| "dir": "src/private-overlays/chromeos-overlay", | |
| "repo-name": "chromeos", | |
| "masters": ["chromiumos", "portage-stable"], | |
| }, | |
| { | |
| "dir": "src/private-overlays/chromeos-partner-overlay", | |
| "repo-name": "chromeos-partner", | |
| "masters": ["chromeos"], | |
| }, | |
| { | |
| "dir": "src/third_party/chromiumos-overlay", | |
| "repo-name": "chromiumos", | |
| "masters": ["portage-stable"], | |
| }, | |
| { | |
| "dir": "src/third_party/portage-stable", | |
| "repo-name": "portage-stable", | |
| "masters": [], | |
| }, | |
| ] | |
| # Template for overlay/metadata/layout.conf. | |
| layout_template = """ | |
| repo-name = %(repo_name)s | |
| masters = %(masters)s | |
| """ | |
| # Convenience variables for ebuild dictionary building. | |
| foo_eb_dir = os.path.join( | |
| self.tempdir, "src/overlays/overlay-foo/category" | |
| ) | |
| foo_ec_dir = os.path.join( | |
| self.tempdir, "src/overlays/overlay-foo/eclass" | |
| ) | |
| _priv_rel_path = "src/private-overlays/overlay-foo-private/category" | |
| foopriv_eb_dir = os.path.join(self.tempdir, _priv_rel_path) | |
| # Ebuilds for testing. | |
| # key: Package name of the ebuild. | |
| # dir: .ebuild file location. | |
| # board: Overlay that contains the ebuild. | |
| # license: The license the ebuild uses. | |
| self.ebuilds = { | |
| "ftl-pkg": { | |
| "dir": os.path.join(foo_eb_dir, "ftl-pkg/ftl-pkg-1.ebuild"), | |
| "board": "foo", | |
| "license": "FTL", | |
| }, | |
| "ttl-pkg": { | |
| "dir": os.path.join(foo_eb_dir, "ttl-pkg/ttl-pkg-1.ebuild"), | |
| "board": "foo", | |
| "license": licenses_lib.TAINTED, | |
| }, | |
| "pub-pkg": { | |
| "dir": os.path.join(foo_eb_dir, "pub-pkg/pub-pkg-1.ebuild"), | |
| "board": "foo", | |
| "license": "PUB", | |
| }, | |
| "pub-cls": { | |
| "dir": os.path.join(foo_ec_dir, "pub-cls.eclass"), | |
| "board": "foo", | |
| "license": "PUB", | |
| }, | |
| "tpl-pkg": { | |
| "dir": os.path.join(foo_eb_dir, "tpl-pkg/tpl-pkg-1.ebuild"), | |
| "board": "foo", | |
| "license": "TPL", | |
| }, | |
| "ccl-pkg": { | |
| "dir": os.path.join(foopriv_eb_dir, "ccl-pkg/ccl-pkg-1.ebuild"), | |
| "board": "foo-private", | |
| "license": "CCL", | |
| }, | |
| } | |
| self.build_infos = { | |
| "metapackage-with-files": { | |
| "files": { | |
| "CATEGORY": "virtual", | |
| "CONTENTS": """dir /foo | |
| sym /sym -> / 1611355469 | |
| obj /file bd1b4ffa168f50b0d45571dae51eefc7 1611355468""", | |
| "LICENSE": "metapackage", | |
| "PF": "metapackage-with-files-1", | |
| }, | |
| "expected_exception": licenses_lib.PackageCorrectnessError, | |
| }, | |
| "metapackage-valid": { | |
| "files": { | |
| "CATEGORY": "virtual", | |
| "CONTENTS": "", | |
| "LICENSE": "metapackage", | |
| "PF": "metapackage-valid-1", | |
| }, | |
| "expected_exception": None, | |
| }, | |
| } | |
| # .ebuild content template. | |
| ebuild_template = 'LICENSE="%(license)s"' | |
| D = cros_test_lib.Directory | |
| # Much of the file layout has to be an exact match due to hard coding of | |
| # a few repos. Prepare for lisp mode. | |
| file_layout = ( | |
| D( | |
| "src", | |
| ( | |
| D( | |
| "overlays", | |
| ( | |
| D( | |
| "overlay-foo", | |
| ( | |
| D( | |
| "category", | |
| ( | |
| D("ftl-pkg", ("ftl-pkg-1.ebuild",)), | |
| D("pub-pkg", ("pub-pkg-1.ebuild",)), | |
| D("tpl-pkg", ("tpl-pkg-1.ebuild",)), | |
| D("ttl-pkg", ("ttl-pkg-1.ebuild",)), | |
| ), | |
| ), | |
| D("eclass", ("pub-cls.eclass",)), | |
| D("licenses", ("FTL",)), | |
| D("metadata", ("layout.conf",)), | |
| ), | |
| ), | |
| D( | |
| "overlay-bar", | |
| ( | |
| D("licenses", ("OOR",)), | |
| D("metadata", ("layout.conf",)), | |
| ), | |
| ), | |
| ), | |
| ), | |
| D( | |
| "private-overlays", | |
| ( | |
| D( | |
| "overlay-foo-private", | |
| ( | |
| D( | |
| "category", | |
| (D("ccl-pkg", ("ccl-pkg-1.ebuild",)),), | |
| ), | |
| D("metadata", ("layout.conf",)), | |
| ), | |
| ), | |
| D( | |
| "chromeos-overlay", | |
| ( | |
| D("licenses", ("CCL",)), | |
| D("metadata", ("layout.conf",)), | |
| ), | |
| ), | |
| D( | |
| "chromeos-partner-overlay", | |
| ( | |
| D("licenses", ("CPL",)), | |
| D("metadata", ("layout.conf",)), | |
| ), | |
| ), | |
| ), | |
| ), | |
| D( | |
| "third_party", | |
| ( | |
| D( | |
| "chromiumos-overlay", | |
| ( | |
| D("licenses", ("PUB",)), | |
| D("metadata", ("layout.conf",)), | |
| ), | |
| ), | |
| D( | |
| "portage-stable", | |
| ( | |
| D("licenses", ("TPL",)), | |
| D("metadata", ("layout.conf",)), | |
| ), | |
| ), | |
| ), | |
| ), | |
| ), | |
| ), | |
| D( | |
| "build_infos", | |
| ( | |
| D( | |
| "metapackage-with-files", | |
| ( | |
| D( | |
| "build-info", | |
| ("CATEGORY", "CONTENTS", "LICENSE", "PF"), | |
| ), | |
| ), | |
| ), | |
| D( | |
| "metapackage-valid", | |
| ( | |
| D( | |
| "build-info", | |
| ("CATEGORY", "CONTENTS", "LICENSE", "PF"), | |
| ), | |
| ), | |
| ), | |
| ), | |
| ), | |
| ) | |
| cros_test_lib.CreateOnDiskHierarchy(self.tempdir, file_layout) | |
| # Write out the files declared above. | |
| for lic in self.licenses.values(): | |
| osutils.WriteFile( | |
| os.path.join(self.tempdir, lic["dir"]), lic["contents"] | |
| ) | |
| for overlay in overlays: | |
| content = layout_template % { | |
| "masters": " ".join(overlay["masters"]), | |
| "repo_name": overlay["repo-name"], | |
| } | |
| file_loc = os.path.join( | |
| self.tempdir, overlay["dir"], "metadata", "layout.conf" | |
| ) | |
| osutils.WriteFile(file_loc, content) | |
| for _, ebuild in self.ebuilds.items(): | |
| content = ebuild_template % {"license": ebuild["license"]} | |
| ebuild["content"] = content | |
| osutils.WriteFile( | |
| os.path.join(self.tempdir, ebuild["dir"]), content | |
| ) | |
| for pkg, build_info in self.build_infos.items(): | |
| for filename, content in build_info["files"].items(): | |
| osutils.WriteFile( | |
| os.path.join( | |
| self.tempdir, "build_infos", pkg, "build-info", filename | |
| ), | |
| content, | |
| ) | |
| def testGetLicenseTypesFromEbuild(self): | |
| """Tests the fetched license from ebuilds are correct.""" | |
| ebuild_content = self.ebuilds["ftl-pkg"]["content"] | |
| overlay_path = os.sep.join( | |
| self.ebuilds["ftl-pkg"]["dir"].split(os.sep)[:-3] | |
| ) | |
| result = licenses_lib.GetLicenseTypesFromEbuild( | |
| ebuild_content, overlay_path, self.tempdir | |
| ) | |
| expected = ["FTL"] | |
| self.assertEqual(expected, sorted(result)) | |
| def testGetLicenseTypeFromEbuildTainted(self): | |
| """Tests the fetched tainted license from ebuilds is correct.""" | |
| ebuild_content = self.ebuilds["ttl-pkg"]["content"] | |
| overlay_path = os.sep.join( | |
| self.ebuilds["ttl-pkg"]["dir"].split(os.sep)[:-3] | |
| ) | |
| result = licenses_lib.GetLicenseTypesFromEbuild( | |
| ebuild_content, overlay_path, self.tempdir | |
| ) | |
| expected = [licenses_lib.TAINTED] | |
| self.assertEqual(expected, sorted(result)) | |
| def testFindLicenseType(self): | |
| """Tests the type for licenses are correctly identified. | |
| e.g. gentoo vs custom. | |
| """ | |
| # Doesn't exist anywhere. | |
| self.assertRaises( | |
| AssertionError, | |
| licenses_lib.Licensing.FindLicenseType, | |
| "DoesNotExist", | |
| buildroot=self.tempdir, | |
| ) | |
| # Not in overlay hierarchy. | |
| self.assertRaises( | |
| AssertionError, | |
| licenses_lib.Licensing.FindLicenseType, | |
| "OOR", | |
| board="foo", | |
| buildroot=self.tempdir, | |
| ) | |
| # Not included in the default search paths. | |
| self.assertRaises( | |
| AssertionError, | |
| licenses_lib.Licensing.FindLicenseType, | |
| "FTL", | |
| buildroot=self.tempdir, | |
| ) | |
| # Checking each license type is the expected. | |
| for name, lic in self.licenses.items(): | |
| if lic.get("skip_test", False): | |
| continue | |
| result = licenses_lib.Licensing.FindLicenseType( | |
| name, board=lic["board"], buildroot=self.tempdir | |
| ) | |
| self.assertEqual(lic["type"], result) | |
| def testReadSharedLicense(self): | |
| """Tests the license text is correctly fetched.""" | |
| # Doesn't exist. | |
| self.assertRaises( | |
| AssertionError, | |
| licenses_lib.Licensing.ReadSharedLicense, | |
| "DoesNotExist", | |
| buildroot=self.tempdir, | |
| ) | |
| # Not in overlay hierarchy. | |
| self.assertRaises( | |
| AssertionError, | |
| licenses_lib.Licensing.ReadSharedLicense, | |
| "OOR", | |
| board="foo", | |
| buildroot=self.tempdir, | |
| ) | |
| # Not included in the default search paths. | |
| self.assertRaises( | |
| AssertionError, | |
| licenses_lib.Licensing.ReadSharedLicense, | |
| "FTL", | |
| buildroot=self.tempdir, | |
| ) | |
| for name, lic in self.licenses.items(): | |
| if lic.get("skip_test", False): | |
| continue | |
| result = licenses_lib.Licensing.ReadSharedLicense( | |
| name, board=lic["board"], buildroot=self.tempdir | |
| ) | |
| self.assertEqual(lic["contents"], result) | |
| def testReadUnknownEncodedFile(self): | |
| """Validate the fix for crbug.com/654894.""" | |
| bad_license = os.path.join(self.tempdir, "license.rtf") | |
| osutils.WriteFile(bad_license, "Foo\x00Bar") | |
| result = licenses_lib.ReadUnknownEncodedFile(bad_license) | |
| self.assertEqual(result, "FooBar") | |
| def testDeprecatedLicenses(self): | |
| """Verify deprecated license checks.""" | |
| # These are known bad packages. | |
| licenses_lib._CheckForKnownBadLicenses( | |
| "chromeos-base/google-sans-fonts-1-r13", {"Google-TOS"} | |
| ) | |
| # These packages should not be allowed. | |
| with self.assertRaises(licenses_lib.PackageLicenseError): | |
| licenses_lib._CheckForKnownBadLicenses( | |
| "sys-apps/portage-123", {"Proprietary-Binary"} | |
| ) | |
| with self.assertRaises(licenses_lib.PackageLicenseError): | |
| licenses_lib._CheckForKnownBadLicenses( | |
| "sys-apps/portage-123", {"GPL-2", "Google-TOS"} | |
| ) | |
| def testHookPackageProcess(self): | |
| build_infos_path = os.path.join(self.tempdir, "build_infos") | |
| for pkg, build_info in self.build_infos.items(): | |
| if build_info["expected_exception"]: | |
| with self.assertRaises(licenses_lib.PackageCorrectnessError): | |
| licenses_lib.HookPackageProcess( | |
| os.path.join(build_infos_path, pkg) | |
| ) | |
| else: | |
| licenses_lib.HookPackageProcess( | |
| os.path.join(build_infos_path, pkg) | |
| ) | |
| @mock.patch("chromite.lib.cros_build_lib.run") | |
| def testListInstalledPackages(self, run_mock): | |
| result = "[ U ] test to /build/test_dir\n[ U ] test2 to /build/test_dir" | |
| run_mock.return_value = cros_build_lib.CompletedProcess( | |
| args=[], returncode=0, stdout=result | |
| ) | |
| self.assertRaisesRegex( | |
| AssertionError, | |
| r"^.*\[ U \] test.*\[ U \] test2.*$", | |
| licenses_lib.ListInstalledPackages, | |
| "", | |
| ) | |
| result = "[ R ] test to /build/test_dir\n[ R ] test2 to /build/test_dir" | |
| run_mock.return_value = cros_build_lib.CompletedProcess( | |
| args=[], returncode=0, stdout=result | |
| ) | |
| self.assertEqual( | |
| licenses_lib.ListInstalledPackages(""), ["test", "test2"] | |
| ) | |
| def testBannedLicenses(self): | |
| """Verify banned license checks.""" | |
| # These are somewhat redundant, but we want to be overly cautious. | |
| # All of these have been used in Gentoo at some point. | |
| BAD_LICENSES = { | |
| "AGPL", | |
| "AGPL-1", | |
| "AGPL-2", | |
| "AGPL-2+", | |
| "AGPL-3", | |
| "AGPL-3+", | |
| "as-is", | |
| "CC-BY-NC-4.0", | |
| "CC-BY-NC-ND-2.0", | |
| "CC-BY-NC-ND-2.5", | |
| "CC-BY-NC-ND-3.0", | |
| "CC-BY-NC-SA-2.5", | |
| "CC-BY-NC-SA-3.0", | |
| "CC-BY-NC-SA-4.0", | |
| "CPAL", | |
| "CPAL-1.0", | |
| "EUPL", | |
| "EUPL-1.1", | |
| "WTFPL", | |
| "WTFPL-2", # nocheck | |
| } | |
| for lic in BAD_LICENSES: | |
| with self.assertRaises(licenses_lib.PackageLicenseError): | |
| licenses_lib._CheckForKnownBadLicenses("sys-libs/db-18", {lic}) | |
| with self.assertRaises(licenses_lib.PackageLicenseError): | |
| licenses_lib._CheckForKnownBadLicenses( | |
| "sys-libs/db-18", {"GPL-2", lic} | |
| ) | |
| def testYamlToJson(self): | |
| """Verify the migration logic.""" | |
| yaml_file = self.tempdir / "foo.yaml" | |
| yaml_file.write_text( | |
| """ | |
| - !!python/tuple [sysroot, null] | |
| - !!python/tuple [category, dev-python] | |
| - !!python/tuple [name, pycryptodome] | |
| - !!python/tuple [version, 3.7.3] | |
| - !!python/tuple [revision, 0] | |
| - !!python/tuple [fullname, dev-python/pycryptodome] | |
| - !!python/tuple [fullnamerev, dev-python/pycryptodome-3.7.3] | |
| - !!python/tuple | |
| - license_names | |
| - !!set {Unlicense: null} | |
| - !!python/tuple | |
| - license_text_scanned | |
| - ["Scanned Source License \ | |
| pycryptodome-3.7.3-python3_6/pycryptodome.egg-info/PKG-INFO:\\n\\ | |
| Requires-Python: >=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*\\n"] | |
| - !!python/tuple | |
| - homepages | |
| - ['https://www.pycryptodome.org', | |
| 'https://github.com/Legrandin/pycryptodome', | |
| 'https://pypi.org/project/pycryptodome/'] | |
| - !!python/tuple [skip, false] | |
| - !!python/tuple [tainted, false] | |
| - !!python/tuple [ebuild_path, null] | |
| """, | |
| encoding="utf-8", | |
| ) | |
| json_file = self.tempdir / "foo.json" | |
| licenses_lib._convert_yaml_to_json(yaml_file, json_file) | |
| data = json.loads(json_file.read_bytes()) | |
| self.assertEqual( | |
| data, | |
| { | |
| "category": "dev-python", | |
| "ebuild_path": None, | |
| "fullname": "dev-python/pycryptodome", | |
| "fullnamerev": "dev-python/pycryptodome-3.7.3", | |
| "homepages": [ | |
| "https://www.pycryptodome.org", | |
| "https://github.com/Legrandin/pycryptodome", | |
| "https://pypi.org/project/pycryptodome/", | |
| ], | |
| "license_names": ["Unlicense"], | |
| "license_text_scanned": [ | |
| "Scanned Source License " | |
| "pycryptodome-3.7.3-python3_6/pycryptodome.egg-info/" | |
| "PKG-INFO:\n" | |
| "Requires-Python: >=2.6, !=3.0.*, !=3.1.*, !=3.2.*, " | |
| "!=3.3.*\n" | |
| ], | |
| "name": "pycryptodome", | |
| "revision": 0, | |
| "skip": False, | |
| "sysroot": None, | |
| "tainted": False, | |
| "version": "3.7.3", | |
| }, | |
| ) |