diff --git a/src/reuse/report.py b/src/reuse/report.py index 0915e4067..f78dce974 100644 --- a/src/reuse/report.py +++ b/src/reuse/report.py @@ -27,9 +27,10 @@ class _MultiprocessingContainer: """Container that remembers some data in order to generate a FileReport.""" - def __init__(self, project, do_checksum): + def __init__(self, project, do_checksum, add_license_concluded): self.project = project self.do_checksum = do_checksum + self.add_license_concluded = add_license_concluded def __call__(self, file_): # pylint: disable=broad-except @@ -37,7 +38,10 @@ def __call__(self, file_): return _MultiprocessingResult( file_, FileReport.generate( - self.project, file_, do_checksum=self.do_checksum + self.project, + file_, + do_checksum=self.do_checksum, + add_license_concluded=self.add_license_concluded, ), None, ) @@ -177,6 +181,7 @@ def generate( project: Project, do_checksum: bool = True, multiprocessing: bool = cpu_count() > 1, + add_license_concluded: bool = False, ) -> "ProjectReport": """Generate a ProjectReport from a Project.""" project_report = cls(do_checksum=do_checksum) @@ -186,7 +191,9 @@ def generate( project.licenses_without_extension ) - container = _MultiprocessingContainer(project, do_checksum) + container = _MultiprocessingContainer( + project, do_checksum, add_license_concluded + ) if multiprocessing: with mp.Pool() as pool: @@ -338,7 +345,11 @@ def to_dict(self): @classmethod def generate( - cls, project: Project, path: PathLike, do_checksum: bool = True + cls, + project: Project, + path: PathLike, + do_checksum: bool = True, + add_license_concluded: bool = False, ) -> "FileReport": """Generate a FileReport from a path in a Project.""" path = Path(path) @@ -379,7 +390,9 @@ def generate( # Add license to report. report.spdxfile.licenses_in_file.append(identifier) - if not spdx_info.spdx_expressions: + if not add_license_concluded: + report.spdxfile.license_concluded = "NOASSERTION" + elif not spdx_info.spdx_expressions: report.spdxfile.license_concluded = "NONE" else: # Merge all the license expressions together, wrapping them in diff --git a/src/reuse/spdx.py b/src/reuse/spdx.py index 6a512ae44..3902e27f1 100644 --- a/src/reuse/spdx.py +++ b/src/reuse/spdx.py @@ -22,6 +22,14 @@ def add_arguments(parser) -> None: parser.add_argument( "--output", "-o", dest="file", action="store", type=PathType("w") ) + parser.add_argument( + "--add-license-concluded", + action="store_true", + help=_( + "Populate the LicenseConcluded field. Note that REUSE cannot guarantee " + "the field is accurate." + ), + ) def run(args, project: Project, out=sys.stdout) -> int: @@ -43,7 +51,9 @@ def run(args, project: Project, out=sys.stdout) -> int: ) report = ProjectReport.generate( - project, multiprocessing=not args.no_multiprocessing + project, + multiprocessing=not args.no_multiprocessing, + add_license_concluded=args.add_license_concluded, ) out.write(report.bill_of_materials()) diff --git a/tests/conftest.py b/tests/conftest.py index 4c257b7c8..0e2dcde8b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -84,6 +84,11 @@ def multiprocessing(request, monkeypatch) -> bool: yield request.param +@pytest.fixture(params=[True, False]) +def add_license_concluded(request) -> bool: + yield request + + @pytest.fixture() def empty_directory(tmpdir_factory) -> Path: """Create a temporary empty directory.""" diff --git a/tests/test_main.py b/tests/test_main.py index d789c37ac..afcbb7902 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -277,10 +277,30 @@ def test_spdx(fake_repository, stringio): """Compile to an SPDX document.""" os.chdir(str(fake_repository)) result = main(["spdx"], out=stringio) + output = stringio.getvalue() + + # Ensure no LicenseConcluded is included without the flag + assert "\nLicenseConcluded: NOASSERTION\n" in output + assert "\nLicenseConcluded: GPL-3.0-or-later\n" not in output # TODO: This test is rubbish. assert result == 0 - assert stringio.getvalue() + assert output + + +def test_spdx_add_license_concluded(fake_repository, stringio): + """Compile to an SPDX document.""" + os.chdir(str(fake_repository)) + result = main(["spdx", "--add-license-concluded"], out=stringio) + output = stringio.getvalue() + + # Ensure no LicenseConcluded is included without the flag + assert "\nLicenseConcluded: NOASSERTION\n" not in output + assert "\nLicenseConcluded: GPL-3.0-or-later\n" in output + + # TODO: This test is rubbish. + assert result == 0 + assert output def test_spdx_no_multiprocessing(fake_repository, stringio, multiprocessing): diff --git a/tests/test_report.py b/tests/test_report.py index 68a3f86da..47b9b8c5f 100644 --- a/tests/test_report.py +++ b/tests/test_report.py @@ -28,64 +28,101 @@ # REUSE-IgnoreStart -def test_generate_file_report_file_simple(fake_repository): +def test_generate_file_report_file_simple( + fake_repository, add_license_concluded +): """An extremely simple generate test, just to see if the function doesn't crash. """ project = Project(fake_repository) - result = FileReport.generate(project, "src/source_code.py") + result = FileReport.generate( + project, + "src/source_code.py", + add_license_concluded=add_license_concluded, + ) + assert result.spdxfile.licenses_in_file == ["GPL-3.0-or-later"] - assert result.spdxfile.license_concluded == "GPL-3.0-or-later" + assert ( + result.spdxfile.license_concluded == "GPL-3.0-or-later" + if add_license_concluded + else "NOASSERTION" + ) assert result.spdxfile.copyright == "SPDX-FileCopyrightText: 2017 Jane Doe" assert not result.bad_licenses assert not result.missing_licenses -def test_generate_file_report_file_from_different_cwd(fake_repository): +def test_generate_file_report_file_from_different_cwd( + fake_repository, add_license_concluded +): """Another simple generate test, but from a different CWD.""" os.chdir("/") project = Project(fake_repository) result = FileReport.generate( - project, fake_repository / "src/source_code.py" + project, + fake_repository / "src/source_code.py", + add_license_concluded=add_license_concluded, ) assert result.spdxfile.licenses_in_file == ["GPL-3.0-or-later"] - assert result.spdxfile.license_concluded == "GPL-3.0-or-later" + assert ( + result.spdxfile.license_concluded == "GPL-3.0-or-later" + if add_license_concluded + else "NOASSERTION" + ) assert result.spdxfile.copyright == "SPDX-FileCopyrightText: 2017 Jane Doe" assert not result.bad_licenses assert not result.missing_licenses -def test_generate_file_report_file_missing_license(fake_repository): +def test_generate_file_report_file_missing_license( + fake_repository, add_license_concluded +): """Simple generate test with a missing license.""" (fake_repository / "foo.py").write_text( "SPDX-License-Identifier: BSD-3-Clause" ) project = Project(fake_repository) - result = FileReport.generate(project, "foo.py") + result = FileReport.generate( + project, "foo.py", add_license_concluded=add_license_concluded + ) assert result.spdxfile.copyright == "" assert result.spdxfile.licenses_in_file == ["BSD-3-Clause"] - assert result.spdxfile.license_concluded == "BSD-3-Clause" + assert ( + result.spdxfile.license_concluded == "BSD-3-Clause" + if add_license_concluded + else "NOASSERTION" + ) assert result.missing_licenses == {"BSD-3-Clause"} assert not result.bad_licenses -def test_generate_file_report_file_bad_license(fake_repository): +def test_generate_file_report_file_bad_license( + fake_repository, add_license_concluded +): """Simple generate test with a bad license.""" (fake_repository / "foo.py").write_text( "SPDX-License-Identifier: fakelicense" ) project = Project(fake_repository) - result = FileReport.generate(project, "foo.py") + result = FileReport.generate( + project, "foo.py", add_license_concluded=add_license_concluded + ) assert result.spdxfile.copyright == "" assert result.spdxfile.licenses_in_file == ["fakelicense"] - assert result.spdxfile.license_concluded == "fakelicense" + assert ( + result.spdxfile.license_concluded == "fakelicense" + if add_license_concluded + else "NOASSERTION" + ) assert result.bad_licenses == {"fakelicense"} assert result.missing_licenses == {"fakelicense"} -def test_generate_file_report_license_contains_plus(fake_repository): +def test_generate_file_report_license_contains_plus( + fake_repository, add_license_concluded +): """Given a license expression akin to Apache-1.0+, LICENSES/Apache-1.0.txt should be an appropriate license file. """ @@ -94,19 +131,28 @@ def test_generate_file_report_license_contains_plus(fake_repository): ) (fake_repository / "LICENSES/Apache-1.0.txt").touch() project = Project(fake_repository) - result = FileReport.generate(project, "foo.py") + result = FileReport.generate( + project, "foo.py", add_license_concluded=add_license_concluded + ) assert result.spdxfile.copyright == "" assert result.spdxfile.licenses_in_file == ["Apache-1.0+"] - assert result.spdxfile.license_concluded == "Apache-1.0+" + assert ( + result.spdxfile.license_concluded == "Apache-1.0+" + if add_license_concluded + else "NOASSERTION" + ) assert not result.bad_licenses assert not result.missing_licenses -def test_generate_file_report_exception(fake_repository): +def test_generate_file_report_exception(fake_repository, add_license_concluded): """Simple generate test to test if the exception is detected.""" project = Project(fake_repository) - result = FileReport.generate(project, "src/exception.py") + result = FileReport.generate( + project, "src/exception.py", add_license_concluded=add_license_concluded + ) + assert set(result.spdxfile.licenses_in_file) == { "GPL-3.0-or-later", "Autoconf-exception-3.0", @@ -114,28 +160,44 @@ def test_generate_file_report_exception(fake_repository): assert ( result.spdxfile.license_concluded == "GPL-3.0-or-later WITH Autoconf-exception-3.0" + if add_license_concluded + else "NOASSERTION" ) assert result.spdxfile.copyright == "SPDX-FileCopyrightText: 2017 Jane Doe" assert not result.bad_licenses assert not result.missing_licenses -def test_generate_file_report_no_licenses(fake_repository): +def test_generate_file_report_no_licenses( + fake_repository, add_license_concluded +): """Test behavior when no license information is present in the file""" (fake_repository / "foo.py").write_text("") project = Project(fake_repository) - result = FileReport.generate(project, "foo.py") + result = FileReport.generate( + project, "foo.py", add_license_concluded=add_license_concluded + ) assert result.spdxfile.copyright == "" assert result.spdxfile.licenses_in_file == [] - assert result.spdxfile.license_concluded == "NONE" + assert ( + result.spdxfile.license_concluded == "NONE" + if add_license_concluded + else "NOASSERTION" + ) assert not result.bad_licenses assert not result.missing_licenses -def test_generate_file_report_multiple_licenses(fake_repository): +def test_generate_file_report_multiple_licenses( + fake_repository, add_license_concluded +): project = Project(fake_repository) - result = FileReport.generate(project, "src/multiple_licenses.rs") + result = FileReport.generate( + project, + "src/multiple_licenses.rs", + add_license_concluded=add_license_concluded, + ) assert result.spdxfile.copyright == "SPDX-FileCopyrightText: 2022 Jane Doe" assert set(result.spdxfile.licenses_in_file) == { @@ -146,6 +208,8 @@ def test_generate_file_report_multiple_licenses(fake_repository): assert ( result.spdxfile.license_concluded == "GPL-3.0-or-later AND (Apache-2.0 OR CC0-1.0)" + if add_license_concluded + else "NOASSERTION" ) assert not result.bad_licenses assert not result.missing_licenses