diff --git a/plugins/reporters/spdx/src/funTest/assets/disclosure-cli-expected-output.spdx.yml b/plugins/reporters/spdx/src/funTest/assets/disclosure-cli-expected-output.spdx.yml index 0893e6272e2ef..427980211f99b 100644 --- a/plugins/reporters/spdx/src/funTest/assets/disclosure-cli-expected-output.spdx.yml +++ b/plugins/reporters/spdx/src/funTest/assets/disclosure-cli-expected-output.spdx.yml @@ -273,7 +273,7 @@ packages: filesAnalyzed: false homepage: "NONE" licenseConcluded: "NOASSERTION" - licenseDeclared: "Apache-2.0 AND Apache-2.0 AND Apache-2.0 AND MIT AND MIT AND MIT" + licenseDeclared: "Apache-2.0 AND MIT" name: "gopkg.in/yaml.v3" summary: "NONE" versionInfo: "3.0.1" @@ -288,7 +288,7 @@ packages: filesAnalyzed: false homepage: "NONE" licenseConcluded: "NOASSERTION" - licenseDeclared: "Apache-2.0 AND Apache-2.0 AND Apache-2.0 AND MIT AND MIT AND MIT" + licenseDeclared: "Apache-2.0 AND MIT" name: "gopkg.in/yaml.v3" summary: "NONE" versionInfo: "3.0.1" diff --git a/plugins/reporters/spdx/src/funTest/assets/spdx-document-reporter-expected-output.spdx.json b/plugins/reporters/spdx/src/funTest/assets/spdx-document-reporter-expected-output.spdx.json index a586ba8ed9258..61fce21d40fe3 100644 --- a/plugins/reporters/spdx/src/funTest/assets/spdx-document-reporter-expected-output.spdx.json +++ b/plugins/reporters/spdx/src/funTest/assets/spdx-document-reporter-expected-output.spdx.json @@ -46,7 +46,7 @@ "filesAnalyzed" : false, "homepage" : "first package's homepage URL", "licenseConcluded" : "BSD-2-Clause AND BSD-3-Clause AND MIT", - "licenseDeclared" : "Apache-2.0 AND BSD-2-Clause AND BSD-2-Clause AND BSD-2-Clause AND BSD-2-Clause AND BSD-3-Clause AND BSD-3-Clause AND BSD-3-Clause AND BSD-3-Clause AND BSD-3-Clause AND BSD-3-Clause AND (GPL-2.0-only OR MIT) AND (GPL-2.0-only OR MIT) AND (GPL-2.0-only OR MIT) AND MIT AND MIT AND MIT", + "licenseDeclared" : "Apache-2.0 AND BSD-2-Clause AND BSD-3-Clause AND (GPL-2.0-only OR MIT) AND MIT", "name" : "first-package", "summary" : "A package with all supported attributes set, with a VCS URL containing a user name, and with two scan results for the VCS containing copyright findings matched to a license finding.", "versionInfo" : "0.0.1" @@ -63,7 +63,7 @@ "hasFiles" : [ "SPDXRef-File-1" ], "homepage" : "first package's homepage URL", "licenseConcluded" : "NOASSERTION", - "licenseDeclared" : "Apache-2.0 AND BSD-2-Clause AND BSD-2-Clause AND BSD-2-Clause AND BSD-2-Clause AND BSD-3-Clause AND BSD-3-Clause AND BSD-3-Clause AND BSD-3-Clause AND BSD-3-Clause AND BSD-3-Clause AND (GPL-2.0-only OR MIT) AND (GPL-2.0-only OR MIT) AND (GPL-2.0-only OR MIT) AND MIT AND MIT AND MIT", + "licenseDeclared" : "Apache-2.0 AND BSD-2-Clause AND BSD-3-Clause AND (GPL-2.0-only OR MIT) AND MIT", "licenseInfoFromFiles" : [ "Apache-2.0", "BSD-2-Clause" ], "name" : "first-package", "packageVerificationCode" : { @@ -87,7 +87,7 @@ "filesAnalyzed" : false, "homepage" : "first package's homepage URL", "licenseConcluded" : "NOASSERTION", - "licenseDeclared" : "Apache-2.0 AND BSD-2-Clause AND BSD-2-Clause AND BSD-2-Clause AND BSD-2-Clause AND BSD-3-Clause AND BSD-3-Clause AND BSD-3-Clause AND BSD-3-Clause AND BSD-3-Clause AND BSD-3-Clause AND (GPL-2.0-only OR MIT) AND (GPL-2.0-only OR MIT) AND (GPL-2.0-only OR MIT) AND MIT AND MIT AND MIT", + "licenseDeclared" : "Apache-2.0 AND BSD-2-Clause AND BSD-3-Clause AND (GPL-2.0-only OR MIT) AND MIT", "name" : "first-package", "summary" : "A package with all supported attributes set, with a VCS URL containing a user name, and with two scan results for the VCS containing copyright findings matched to a license finding.", "versionInfo" : "0.0.1" diff --git a/plugins/reporters/spdx/src/funTest/assets/spdx-document-reporter-expected-output.spdx.yml b/plugins/reporters/spdx/src/funTest/assets/spdx-document-reporter-expected-output.spdx.yml index 66e399e4ed589..fa363b8b6947c 100644 --- a/plugins/reporters/spdx/src/funTest/assets/spdx-document-reporter-expected-output.spdx.yml +++ b/plugins/reporters/spdx/src/funTest/assets/spdx-document-reporter-expected-output.spdx.yml @@ -55,10 +55,8 @@ packages: filesAnalyzed: false homepage: "first package's homepage URL" licenseConcluded: "BSD-2-Clause AND BSD-3-Clause AND MIT" - licenseDeclared: "Apache-2.0 AND BSD-2-Clause AND BSD-2-Clause AND BSD-2-Clause\ - \ AND BSD-2-Clause AND BSD-3-Clause AND BSD-3-Clause AND BSD-3-Clause AND BSD-3-Clause\ - \ AND BSD-3-Clause AND BSD-3-Clause AND (GPL-2.0-only OR MIT) AND (GPL-2.0-only\ - \ OR MIT) AND (GPL-2.0-only OR MIT) AND MIT AND MIT AND MIT" + licenseDeclared: "Apache-2.0 AND BSD-2-Clause AND BSD-3-Clause AND (GPL-2.0-only\ + \ OR MIT) AND MIT" name: "first-package" summary: "A package with all supported attributes set, with a VCS URL containing\ \ a user name, and with two scan results for the VCS containing copyright findings\ @@ -78,10 +76,8 @@ packages: - "SPDXRef-File-1" homepage: "first package's homepage URL" licenseConcluded: "NOASSERTION" - licenseDeclared: "Apache-2.0 AND BSD-2-Clause AND BSD-2-Clause AND BSD-2-Clause\ - \ AND BSD-2-Clause AND BSD-3-Clause AND BSD-3-Clause AND BSD-3-Clause AND BSD-3-Clause\ - \ AND BSD-3-Clause AND BSD-3-Clause AND (GPL-2.0-only OR MIT) AND (GPL-2.0-only\ - \ OR MIT) AND (GPL-2.0-only OR MIT) AND MIT AND MIT AND MIT" + licenseDeclared: "Apache-2.0 AND BSD-2-Clause AND BSD-3-Clause AND (GPL-2.0-only\ + \ OR MIT) AND MIT" licenseInfoFromFiles: - "Apache-2.0" - "BSD-2-Clause" @@ -107,10 +103,8 @@ packages: filesAnalyzed: false homepage: "first package's homepage URL" licenseConcluded: "NOASSERTION" - licenseDeclared: "Apache-2.0 AND BSD-2-Clause AND BSD-2-Clause AND BSD-2-Clause\ - \ AND BSD-2-Clause AND BSD-3-Clause AND BSD-3-Clause AND BSD-3-Clause AND BSD-3-Clause\ - \ AND BSD-3-Clause AND BSD-3-Clause AND (GPL-2.0-only OR MIT) AND (GPL-2.0-only\ - \ OR MIT) AND (GPL-2.0-only OR MIT) AND MIT AND MIT AND MIT" + licenseDeclared: "Apache-2.0 AND BSD-2-Clause AND BSD-3-Clause AND (GPL-2.0-only\ + \ OR MIT) AND MIT" name: "first-package" summary: "A package with all supported attributes set, with a VCS URL containing\ \ a user name, and with two scan results for the VCS containing copyright findings\ diff --git a/plugins/reporters/spdx/src/main/kotlin/Extensions.kt b/plugins/reporters/spdx/src/main/kotlin/Extensions.kt index a7ec8c3ff4d44..63c79d6179174 100644 --- a/plugins/reporters/spdx/src/main/kotlin/Extensions.kt +++ b/plugins/reporters/spdx/src/main/kotlin/Extensions.kt @@ -204,6 +204,7 @@ internal fun Package.toSpdxPackage( SpdxConstants.NONE } else { packageLicenseExpressions.reduce(SpdxExpression::and) + .simplify() .sorted() .nullOrBlankToSpdxNoassertionOrNone() }, diff --git a/utils/spdx/src/main/kotlin/SpdxExpression.kt b/utils/spdx/src/main/kotlin/SpdxExpression.kt index 2f8f5b7b7f7b6..45104948ee27d 100644 --- a/utils/spdx/src/main/kotlin/SpdxExpression.kt +++ b/utils/spdx/src/main/kotlin/SpdxExpression.kt @@ -113,6 +113,11 @@ sealed class SpdxExpression { */ abstract fun normalize(mapDeprecated: Boolean = true): SpdxExpression + /** + * Return a simplified expression that has e.g. redundancies removed. + */ + open fun simplify(): SpdxExpression = this + /** * Return this expression sorted lexicographically. */ @@ -200,12 +205,14 @@ sealed class SpdxExpression { /** * Concatenate [this][SpdxExpression] and [other] using [SpdxOperator.AND]. */ - infix fun and(other: SpdxExpression) = SpdxCompoundExpression(SpdxOperator.AND, listOf(this, other)) + infix fun and(other: SpdxExpression) = + takeIf { this == other } ?: SpdxCompoundExpression(SpdxOperator.AND, listOf(this, other)) /** * Concatenate [this][SpdxExpression] and [other] using [SpdxOperator.OR]. */ - infix fun or(other: SpdxExpression) = SpdxCompoundExpression(SpdxOperator.OR, listOf(this, other)) + infix fun or(other: SpdxExpression) = + takeIf { this == other } ?: SpdxCompoundExpression(SpdxOperator.OR, listOf(this, other)) } /** @@ -242,6 +249,21 @@ class SpdxCompoundExpression( override fun normalize(mapDeprecated: Boolean) = SpdxCompoundExpression(operator, children.map { it.normalize(mapDeprecated) }) + override fun simplify(): SpdxExpression { + val flattenedChildren = children.flatMapTo(mutableSetOf()) { child -> + val simplifiedChild = child.simplify() + + if (simplifiedChild is SpdxCompoundExpression && simplifiedChild.operator == operator) { + // Inline nested children of the same operator. + simplifiedChild.children.map { it.simplify() } + } else { + setOf(simplifiedChild) + } + } + + return flattenedChildren.singleOrNull() ?: SpdxCompoundExpression(operator, flattenedChildren) + } + override fun sorted(): SpdxExpression { /** * Get all transitive children of this expression that are concatenated with the same operator as this compound diff --git a/utils/spdx/src/test/kotlin/SpdxCompoundExpressionTest.kt b/utils/spdx/src/test/kotlin/SpdxCompoundExpressionTest.kt new file mode 100644 index 0000000000000..7c3f93479fc41 --- /dev/null +++ b/utils/spdx/src/test/kotlin/SpdxCompoundExpressionTest.kt @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2017 The ORT Project Authors (see ) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +package org.ossreviewtoolkit.utils.spdx + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.WordSpec +import io.kotest.matchers.shouldBe + +class SpdxCompoundExpressionTest : WordSpec({ + "Creating a compound expression" should { + "fail if the expression has less than two children" { + shouldThrow { + SpdxCompoundExpression(SpdxOperator.AND, emptyList()) + } + + shouldThrow { + SpdxCompoundExpression(SpdxOperator.AND, listOf(SpdxLicenseIdExpression("license"))) + } + } + } + + "Simplifying a compound expression" should { + "inline nested children of the same operator" { + val expression = SpdxCompoundExpression( + SpdxOperator.AND, + listOf( + SpdxCompoundExpression( + SpdxOperator.AND, + listOf( + SpdxLicenseIdExpression("MIT"), + SpdxCompoundExpression( + SpdxOperator.AND, + listOf( + SpdxLicenseIdExpression("MIT"), + SpdxLicenseIdExpression("Apache-2.0") + ) + ) + ) + ), + SpdxLicenseIdExpression("Apache-2.0") + ) + ) + + // Compare string representations to not rely on semantic equality. + expression.simplify().toString() shouldBe SpdxCompoundExpression( + SpdxOperator.AND, + listOf( + SpdxLicenseIdExpression("MIT"), + SpdxLicenseIdExpression("Apache-2.0") + ) + ).toString() + } + + "create a single expression for equal operands" { + val expression = SpdxCompoundExpression( + SpdxOperator.AND, + listOf( + SpdxLicenseIdExpression("MIT"), + SpdxLicenseIdExpression("MIT") + ) + ) + + expression.simplify() shouldBe SpdxLicenseIdExpression("MIT") + } + } +}) diff --git a/utils/spdx/src/test/kotlin/SpdxExpressionChoiceTest.kt b/utils/spdx/src/test/kotlin/SpdxExpressionChoiceTest.kt new file mode 100644 index 0000000000000..69bc41ef82805 --- /dev/null +++ b/utils/spdx/src/test/kotlin/SpdxExpressionChoiceTest.kt @@ -0,0 +1,345 @@ +/* + * Copyright (C) 2017 The ORT Project Authors (see ) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +package org.ossreviewtoolkit.utils.spdx + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.WordSpec +import io.kotest.matchers.collections.containExactlyInAnyOrder +import io.kotest.matchers.should +import io.kotest.matchers.shouldBe + +class SpdxExpressionChoiceTest : WordSpec({ + "applyChoice()" should { + "return the choice for a simple expression" { + val expression = "a".toSpdx() + val choice = "a".toSpdx() + + val result = expression.applyChoice(choice) + + result shouldBe "a".toSpdx() + } + + "throw an exception if the user chose a wrong license for a simple expression" { + val expression = "a".toSpdx() + val choice = "b".toSpdx() + + shouldThrow { expression.applyChoice(choice) } + } + + "return the new expression if only a part of the expression is matched by the subExpression" { + val expression = "a OR b OR c".toSpdx() + val choice = "b".toSpdx() + val subExpression = "a OR b".toSpdx() + + val result = expression.applyChoice(choice, subExpression) + + result shouldBe "b OR c".toSpdx() + } + + "work with choices that itself are a choice" { + val expression = "a OR b OR c OR d".toSpdx() + val choice = "a OR b".toSpdx() + val subExpression = "a OR b OR c".toSpdx() + + val result = expression.applyChoice(choice, subExpression) + + result shouldBe "a OR b OR d".toSpdx() + } + + "apply the choice if the expression contains multiple choices" { + val expression = "a OR b OR c".toSpdx() + val choice = "b".toSpdx() + + val result = expression.applyChoice(choice) + + result shouldBe "b".toSpdx() + } + + "throw an exception if the chosen license is not a valid option" { + val expression = "a OR b".toSpdx() + val choice = "c".toSpdx() + + shouldThrow { expression.applyChoice(choice) } + } + + "apply the choice even if not literally contained in the expression" { + val expression = "(a OR b) AND c".toSpdx() + val choice = "a AND c".toSpdx() + + val result = expression.applyChoice(choice) + + result shouldBe "a AND c".toSpdx() + } + + "return the reduced subExpression if the choice was valid" { + val expression = "(a OR b) AND c AND (d OR e)".toSpdx() + val choice = "a AND c AND d".toSpdx() + val subExpression = "a AND c AND d OR a AND c AND e".toSpdx() + + val result = expression.applyChoice(choice, subExpression) + + result shouldBe "a AND c AND d OR b AND c AND d OR b AND c AND e".toSpdx() + } + + "throw an exception if the subExpression does not match the simple expression" { + val expression = "a".toSpdx() + val choice = "x".toSpdx() + val subExpression = "x OR y".toSpdx() + + shouldThrow { expression.applyChoice(choice, subExpression) } + } + + "throw an exception if the subExpression does not match the expression" { + val expression = "a OR b OR c".toSpdx() + val choice = "x".toSpdx() + val subExpression = "x OR y OR z".toSpdx() + + shouldThrow { expression.applyChoice(choice, subExpression) } + } + + "throw an exception if the subExpression does not match" { + val expression = "(a OR b) AND c AND (d OR e)".toSpdx() + val choice = "a AND c AND d".toSpdx() + val subExpression = "(a AND c AND d) OR (x AND y AND z)".toSpdx() + + shouldThrow { expression.applyChoice(choice, subExpression) } + } + } + + "applyChoices()" should { + "return the correct result if a single choice is applied" { + val expression = "a OR b OR c OR d".toSpdx() + + val choices = listOf(SpdxLicenseChoice(expression, "a".toSpdx())) + + val result = expression.applyChoices(choices) + + result shouldBe "a".toSpdx() + } + + "return the correct result if multiple simple choices are applied" { + val expression = "(a OR b) AND (c OR d)".toSpdx() + + val choices = listOf( + SpdxLicenseChoice("a OR b".toSpdx(), "a".toSpdx()), + SpdxLicenseChoice("c OR d".toSpdx(), "c".toSpdx()) + ) + + val result = expression.applyChoices(choices) + + result shouldBe "a AND c".toSpdx() + } + + "ignore invalid sub-expressions and return the correct result for valid choices" { + val expression = "a OR b OR c OR d".toSpdx() + + val choices = listOf( + SpdxLicenseChoice("a OR b".toSpdx(), "b".toSpdx()), // b OR c OR d + SpdxLicenseChoice("a OR c".toSpdx(), "a".toSpdx()) // not applied + ) + + val result = expression.applyChoices(choices) + + result shouldBe "b OR c OR d".toSpdx() + } + + "apply the second choice to the effective license after the first choice" { + val expression = "a OR b OR c OR d".toSpdx() + + val choices = listOf( + SpdxLicenseChoice("a OR b".toSpdx(), "b".toSpdx()), // b OR c OR d + SpdxLicenseChoice("b OR c".toSpdx(), "b".toSpdx()) // b OR d + ) + + val result = expression.applyChoices(choices) + + result shouldBe "b OR d".toSpdx() + } + + "apply a single choice to multiple expressions" { + val expression = "(a OR b) AND (c OR d) AND (a OR e)".toSpdx() + + val choices = listOf( + SpdxLicenseChoice("a OR b".toSpdx(), "a".toSpdx()), + SpdxLicenseChoice("a OR e".toSpdx(), "a".toSpdx()) + ) + + val result = expression.applyChoices(choices) + + result shouldBe "a AND (c OR d) AND a".toSpdx() + } + + "given expressions should match semantically equivalent license expressions" { + val expression = "a OR b".toSpdx() + + val choices = listOf( + SpdxLicenseChoice("b OR a".toSpdx(), "a".toSpdx()) + ) + + val result = expression.applyChoices(choices) + + result shouldBe "a".toSpdx() + } + } + + "isSubExpression()" should { + "return true for the same simple expression" { + val mit = "MIT".toSpdx() as SpdxSimpleExpression + + mit.isSubExpression(mit) shouldBe true + } + + "return true for the same single expression" { + val mit = "GPL-2.0-only WITH Classpath-exception-2.0".toSpdx() as SpdxSingleLicenseExpression + + mit.isSubExpression(mit) shouldBe true + } + + "return true for the same compound expression" { + val mit = "CDDL-1.1 OR GPL-2.0-only".toSpdx() as SpdxCompoundExpression + + mit.isSubExpression(mit) shouldBe true + } + + "work correctly for compound expressions with exceptions" { + val gplWithException = "CDDL-1.1 OR GPL-2.0-only WITH Classpath-exception-2.0".toSpdx() + val gpl = "CDDL-1.1 OR GPL-2.0-only".toSpdx() + + gplWithException.isSubExpression(gpl) shouldBe false + } + + "work correctly for nested compound expressions" { + val expression = "(CDDL-1.1 OR GPL-2.0-only) AND (CDDL-1.1 OR GPL-2.0-only WITH Classpath-exception-2.0)" + .toSpdx() + val subExpression = "CDDL-1.1 OR GPL-2.0-only".toSpdx() + + expression.isSubExpression(subExpression) shouldBe true + } + } + + "isValidChoice()" should { + "return true if a choice is valid" { + val spdxExpression = "(a OR b) AND c AND (d OR e)".toSpdx() + + spdxExpression.isValidChoice("a AND c AND d".toSpdx()) shouldBe true + spdxExpression.isValidChoice("a AND d AND c".toSpdx()) shouldBe true + spdxExpression.isValidChoice("c AND a AND d".toSpdx()) shouldBe true + spdxExpression.isValidChoice("c AND d AND a".toSpdx()) shouldBe true + spdxExpression.isValidChoice("d AND a AND c".toSpdx()) shouldBe true + spdxExpression.isValidChoice("d AND c AND a".toSpdx()) shouldBe true + + spdxExpression.isValidChoice("a AND c AND e".toSpdx()) shouldBe true + spdxExpression.isValidChoice("b AND c AND d".toSpdx()) shouldBe true + spdxExpression.isValidChoice("b AND c AND e".toSpdx()) shouldBe true + } + + "return false if a choice is invalid" { + val spdxExpression = "(a OR b) AND c AND (d OR e)".toSpdx() + + spdxExpression.isValidChoice("a".toSpdx()) shouldBe false + spdxExpression.isValidChoice("a AND b".toSpdx()) shouldBe false + spdxExpression.isValidChoice("a AND c".toSpdx()) shouldBe false + spdxExpression.isValidChoice("a AND d".toSpdx()) shouldBe false + spdxExpression.isValidChoice("a AND e".toSpdx()) shouldBe false + spdxExpression.isValidChoice("a AND b AND c".toSpdx()) shouldBe false + spdxExpression.isValidChoice("a AND b AND d".toSpdx()) shouldBe false + spdxExpression.isValidChoice("a AND b AND c AND d".toSpdx()) shouldBe false + } + + "return true for a simplified choice for a complex expression" { + val license = "(MIT OR GPL-2.0-only) AND (MIT OR BSD-3-Clause OR GPL-1.0-or-later) AND " + + "(MIT OR BSD-3-Clause OR GPL-2.0-only)" + + license.toSpdx().isValidChoice("MIT".toSpdx()) shouldBe true + } + } + + "offersChoice()" should { + "return true if the expression contains the OR operator" { + "a OR b".toSpdx().offersChoice() shouldBe true + "a AND b OR c".toSpdx().offersChoice() shouldBe true + "a OR b AND c".toSpdx().offersChoice() shouldBe true + "a AND b AND c OR d".toSpdx().offersChoice() shouldBe true + } + + "return false if the expression does not contain the OR operator" { + "a".toSpdx().offersChoice() shouldBe false + "a AND b".toSpdx().offersChoice() shouldBe false + "a AND b AND c".toSpdx().offersChoice() shouldBe false + } + } + + "validChoices()" should { + "return the original terms of an expression in DNF" { + val choices = "(a AND b) OR (c AND d) OR (e AND f)".toSpdx().validChoices() + + choices.map { it.toString() } should containExactlyInAnyOrder( + "a AND b", + "c AND d", + "e AND f" + ) + } + + "return the distribution of all terms of an expression in CNF" { + val choices = "(a OR b) AND (c OR d)".toSpdx().validChoices() + + choices.map { it.toString() } should containExactlyInAnyOrder( + "a AND c", + "a AND d", + "b AND c", + "b AND d" + ) + } + + "return the valid choices for a complex expression" { + val choices = "(a OR b) AND c AND (d OR e)".toSpdx().validChoices() + + choices.map { it.toString() } should containExactlyInAnyOrder( + "a AND c AND d", + "a AND c AND e", + "b AND c AND d", + "b AND c AND e" + ) + } + + "return the valid choices for a nested expression" { + val choices = "(a OR (b AND (c OR d))) AND (e OR f)".toSpdx().validChoices() + + choices.map { it.toString() } should containExactlyInAnyOrder( + "a AND e", + "a AND f", + "b AND c AND e", + "b AND c AND f", + "b AND d AND e", + "b AND d AND f" + ) + } + + "be explicit about the choices even if they could be simplified" { + val choices = "(a OR b) AND (a OR b)".toSpdx().validChoices() + + choices.map { it.toString() } should containExactlyInAnyOrder( + "a", + "b AND a", + "b" + ) + } + } +}) diff --git a/utils/spdx/src/test/kotlin/SpdxExpressionTest.kt b/utils/spdx/src/test/kotlin/SpdxExpressionTest.kt index 3cbb827ac0bb4..bc7f9b36cdb60 100644 --- a/utils/spdx/src/test/kotlin/SpdxExpressionTest.kt +++ b/utils/spdx/src/test/kotlin/SpdxExpressionTest.kt @@ -37,7 +37,6 @@ import org.ossreviewtoolkit.utils.spdx.SpdxExpression.Strictness import org.ossreviewtoolkit.utils.spdx.SpdxLicense.* import org.ossreviewtoolkit.utils.spdx.SpdxLicenseException.* -@Suppress("LargeClass") class SpdxExpressionTest : WordSpec({ "A dummy SpdxExpression" should { val dummyExpression = "a+ AND (b WITH exception1 OR c+) AND d WITH exception2" @@ -392,18 +391,6 @@ class SpdxExpressionTest : WordSpec({ } } - "creating a compound expression" should { - "fail if the expression has less than two children" { - shouldThrow { - SpdxCompoundExpression(SpdxOperator.AND, emptyList()) - } - - shouldThrow { - SpdxCompoundExpression(SpdxOperator.AND, listOf(SpdxLicenseIdExpression("license"))) - } - } - } - "normalize()" should { "normalize the case of SPDX licenses" { SpdxLicense.entries.filterNot { it.deprecated }.forEach { @@ -530,323 +517,6 @@ class SpdxExpressionTest : WordSpec({ } } - "validChoices()" should { - "return the original terms of an expression in DNF" { - val choices = "(a AND b) OR (c AND d) OR (e AND f)".toSpdx().validChoices() - - choices.map { it.toString() } should containExactlyInAnyOrder( - "a AND b", - "c AND d", - "e AND f" - ) - } - - "return the distribution of all terms of an expression in CNF" { - val choices = "(a OR b) AND (c OR d)".toSpdx().validChoices() - - choices.map { it.toString() } should containExactlyInAnyOrder( - "a AND c", - "a AND d", - "b AND c", - "b AND d" - ) - } - - "return the valid choices for a complex expression" { - val choices = "(a OR b) AND c AND (d OR e)".toSpdx().validChoices() - - choices.map { it.toString() } should containExactlyInAnyOrder( - "a AND c AND d", - "a AND c AND e", - "b AND c AND d", - "b AND c AND e" - ) - } - - "return the valid choices for a nested expression" { - val choices = "(a OR (b AND (c OR d))) AND (e OR f)".toSpdx().validChoices() - - choices.map { it.toString() } should containExactlyInAnyOrder( - "a AND e", - "a AND f", - "b AND c AND e", - "b AND c AND f", - "b AND d AND e", - "b AND d AND f" - ) - } - - "be explicit about the choices even if they could be simplified" { - val choices = "(a OR b) AND (a OR b)".toSpdx().validChoices() - - choices.map { it.toString() } should containExactlyInAnyOrder( - "a AND a", - "b AND a", - "b AND b" - ) - } - } - - "offersChoice()" should { - "return true if the expression contains the OR operator" { - "a OR b".toSpdx().offersChoice() shouldBe true - "a AND b OR c".toSpdx().offersChoice() shouldBe true - "a OR b AND c".toSpdx().offersChoice() shouldBe true - "a AND b AND c OR d".toSpdx().offersChoice() shouldBe true - } - - "return false if the expression does not contain the OR operator" { - "a".toSpdx().offersChoice() shouldBe false - "a AND b".toSpdx().offersChoice() shouldBe false - "a AND b AND c".toSpdx().offersChoice() shouldBe false - } - } - - "isValidChoice()" should { - "return true if a choice is valid" { - val spdxExpression = "(a OR b) AND c AND (d OR e)".toSpdx() - - spdxExpression.isValidChoice("a AND c AND d".toSpdx()) shouldBe true - spdxExpression.isValidChoice("a AND d AND c".toSpdx()) shouldBe true - spdxExpression.isValidChoice("c AND a AND d".toSpdx()) shouldBe true - spdxExpression.isValidChoice("c AND d AND a".toSpdx()) shouldBe true - spdxExpression.isValidChoice("d AND a AND c".toSpdx()) shouldBe true - spdxExpression.isValidChoice("d AND c AND a".toSpdx()) shouldBe true - - spdxExpression.isValidChoice("a AND c AND e".toSpdx()) shouldBe true - spdxExpression.isValidChoice("b AND c AND d".toSpdx()) shouldBe true - spdxExpression.isValidChoice("b AND c AND e".toSpdx()) shouldBe true - } - - "return false if a choice is invalid" { - val spdxExpression = "(a OR b) AND c AND (d OR e)".toSpdx() - - spdxExpression.isValidChoice("a".toSpdx()) shouldBe false - spdxExpression.isValidChoice("a AND b".toSpdx()) shouldBe false - spdxExpression.isValidChoice("a AND c".toSpdx()) shouldBe false - spdxExpression.isValidChoice("a AND d".toSpdx()) shouldBe false - spdxExpression.isValidChoice("a AND e".toSpdx()) shouldBe false - spdxExpression.isValidChoice("a AND b AND c".toSpdx()) shouldBe false - spdxExpression.isValidChoice("a AND b AND d".toSpdx()) shouldBe false - spdxExpression.isValidChoice("a AND b AND c AND d".toSpdx()) shouldBe false - } - - "return true for a simplified choice for a complex expression" { - val license = "(MIT OR GPL-2.0-only) AND (MIT OR BSD-3-Clause OR GPL-1.0-or-later) AND " + - "(MIT OR BSD-3-Clause OR GPL-2.0-only)" - - license.toSpdx().isValidChoice("MIT".toSpdx()) shouldBe true - } - } - - "isSubExpression()" should { - "return true for the same simple expression" { - val mit = "MIT".toSpdx() as SpdxSimpleExpression - - mit.isSubExpression(mit) shouldBe true - } - - "return true for the same single expression" { - val mit = "GPL-2.0-only WITH Classpath-exception-2.0".toSpdx() as SpdxSingleLicenseExpression - - mit.isSubExpression(mit) shouldBe true - } - - "return true for the same compound expression" { - val mit = "CDDL-1.1 OR GPL-2.0-only".toSpdx() as SpdxCompoundExpression - - mit.isSubExpression(mit) shouldBe true - } - - "work correctly for compound expressions with exceptions" { - val gplWithException = "CDDL-1.1 OR GPL-2.0-only WITH Classpath-exception-2.0".toSpdx() - val gpl = "CDDL-1.1 OR GPL-2.0-only".toSpdx() - - gplWithException.isSubExpression(gpl) shouldBe false - } - - "work correctly for nested compound expressions" { - val expression = "(CDDL-1.1 OR GPL-2.0-only) AND (CDDL-1.1 OR GPL-2.0-only WITH Classpath-exception-2.0)" - .toSpdx() - val subExpression = "CDDL-1.1 OR GPL-2.0-only".toSpdx() - - expression.isSubExpression(subExpression) shouldBe true - } - } - - "applyChoice()" should { - "return the choice for a simple expression" { - val expression = "a".toSpdx() - val choice = "a".toSpdx() - - val result = expression.applyChoice(choice) - - result shouldBe "a".toSpdx() - } - - "throw an exception if the user chose a wrong license for a simple expression" { - val expression = "a".toSpdx() - val choice = "b".toSpdx() - - shouldThrow { expression.applyChoice(choice) } - } - - "return the new expression if only a part of the expression is matched by the subExpression" { - val expression = "a OR b OR c".toSpdx() - val choice = "b".toSpdx() - val subExpression = "a OR b".toSpdx() - - val result = expression.applyChoice(choice, subExpression) - - result shouldBe "b OR c".toSpdx() - } - - "work with choices that itself are a choice" { - val expression = "a OR b OR c OR d".toSpdx() - val choice = "a OR b".toSpdx() - val subExpression = "a OR b OR c".toSpdx() - - val result = expression.applyChoice(choice, subExpression) - - result shouldBe "a OR b OR d".toSpdx() - } - - "apply the choice if the expression contains multiple choices" { - val expression = "a OR b OR c".toSpdx() - val choice = "b".toSpdx() - - val result = expression.applyChoice(choice) - - result shouldBe "b".toSpdx() - } - - "throw an exception if the chosen license is not a valid option" { - val expression = "a OR b".toSpdx() - val choice = "c".toSpdx() - - shouldThrow { expression.applyChoice(choice) } - } - - "apply the choice even if not literally contained in the expression" { - val expression = "(a OR b) AND c".toSpdx() - val choice = "a AND c".toSpdx() - - val result = expression.applyChoice(choice) - - result shouldBe "a AND c".toSpdx() - } - - "return the reduced subExpression if the choice was valid" { - val expression = "(a OR b) AND c AND (d OR e)".toSpdx() - val choice = "a AND c AND d".toSpdx() - val subExpression = "a AND c AND d OR a AND c AND e".toSpdx() - - val result = expression.applyChoice(choice, subExpression) - - result shouldBe "a AND c AND d OR b AND c AND d OR b AND c AND e".toSpdx() - } - - "throw an exception if the subExpression does not match the simple expression" { - val expression = "a".toSpdx() - val choice = "x".toSpdx() - val subExpression = "x OR y".toSpdx() - - shouldThrow { expression.applyChoice(choice, subExpression) } - } - - "throw an exception if the subExpression does not match the expression" { - val expression = "a OR b OR c".toSpdx() - val choice = "x".toSpdx() - val subExpression = "x OR y OR z".toSpdx() - - shouldThrow { expression.applyChoice(choice, subExpression) } - } - - "throw an exception if the subExpression does not match" { - val expression = "(a OR b) AND c AND (d OR e)".toSpdx() - val choice = "a AND c AND d".toSpdx() - val subExpression = "(a AND c AND d) OR (x AND y AND z)".toSpdx() - - shouldThrow { expression.applyChoice(choice, subExpression) } - } - } - - "applyChoices()" should { - "return the correct result if a single choice is applied" { - val expression = "a OR b OR c OR d".toSpdx() - - val choices = listOf(SpdxLicenseChoice(expression, "a".toSpdx())) - - val result = expression.applyChoices(choices) - - result shouldBe "a".toSpdx() - } - - "return the correct result if multiple simple choices are applied" { - val expression = "(a OR b) AND (c OR d)".toSpdx() - - val choices = listOf( - SpdxLicenseChoice("a OR b".toSpdx(), "a".toSpdx()), - SpdxLicenseChoice("c OR d".toSpdx(), "c".toSpdx()) - ) - - val result = expression.applyChoices(choices) - - result shouldBe "a AND c".toSpdx() - } - - "ignore invalid sub-expressions and return the correct result for valid choices" { - val expression = "a OR b OR c OR d".toSpdx() - - val choices = listOf( - SpdxLicenseChoice("a OR b".toSpdx(), "b".toSpdx()), // b OR c OR d - SpdxLicenseChoice("a OR c".toSpdx(), "a".toSpdx()) // not applied - ) - - val result = expression.applyChoices(choices) - - result shouldBe "b OR c OR d".toSpdx() - } - - "apply the second choice to the effective license after the first choice" { - val expression = "a OR b OR c OR d".toSpdx() - - val choices = listOf( - SpdxLicenseChoice("a OR b".toSpdx(), "b".toSpdx()), // b OR c OR d - SpdxLicenseChoice("b OR c".toSpdx(), "b".toSpdx()) // b OR d - ) - - val result = expression.applyChoices(choices) - - result shouldBe "b OR d".toSpdx() - } - - "apply a single choice to multiple expressions" { - val expression = "(a OR b) AND (c OR d) AND (a OR e)".toSpdx() - - val choices = listOf( - SpdxLicenseChoice("a OR b".toSpdx(), "a".toSpdx()), - SpdxLicenseChoice("a OR e".toSpdx(), "a".toSpdx()) - ) - - val result = expression.applyChoices(choices) - - result shouldBe "a AND (c OR d) AND a".toSpdx() - } - - "given expressions should match semantically equivalent license expressions" { - val expression = "a OR b".toSpdx() - - val choices = listOf( - SpdxLicenseChoice("b OR a".toSpdx(), "a".toSpdx()) - ) - - val result = expression.applyChoices(choices) - - result shouldBe "a".toSpdx() - } - } - "equals()" should { "return true for semantically equal expressions" { "a".toSpdx() shouldBe "a".toSpdx()