Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate the Opossum reporter to KxS #9484

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions plugins/reporters/opossum/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
plugins {
// Apply precompiled plugins.
id("ort-plugin-conventions")

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commit message nits:

  • We prefer imperative mood, i.e. "Migrates" should say "Migrate".
  • We usually start capitalized after ":" in the title.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The commit message body still uses simple present instead of imperative mood.

// Apply third-party plugins.
alias(libs.plugins.kotlinSerialization)
}

dependencies {
Expand All @@ -33,6 +36,9 @@ dependencies {
implementation(projects.utils.ortUtils)
implementation(projects.utils.spdxUtils)

implementation(libs.jackson.annotations)
implementation(libs.jackson.databind)
implementation(libs.bundles.ks3)
implementation(libs.kotlinx.serialization.core)
implementation(libs.kotlinx.serialization.json)

funTestImplementation(libs.kotest.assertions.json)
}
2,791 changes: 2,791 additions & 0 deletions plugins/reporters/opossum/src/funTest/assets/reporter-test-output.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -19,49 +19,37 @@

package org.ossreviewtoolkit.plugins.reporters.opossum

import com.fasterxml.jackson.databind.json.JsonMapper

import io.kotest.core.TestConfiguration
import io.kotest.assertions.json.shouldEqualSpecifiedJsonIgnoringOrder
import io.kotest.core.spec.style.WordSpec
import io.kotest.engine.spec.tempdir
import io.kotest.matchers.sequences.shouldContain
import io.kotest.matchers.shouldBe
import io.kotest.matchers.string.shouldContain

import org.ossreviewtoolkit.model.OrtResult
import java.time.LocalDateTime

import org.ossreviewtoolkit.reporter.ReporterInput
import org.ossreviewtoolkit.utils.common.normalizeLineBreaks
import org.ossreviewtoolkit.utils.common.unpackZip
import org.ossreviewtoolkit.utils.test.getAssetFile
import org.ossreviewtoolkit.utils.test.patchActualResult
import org.ossreviewtoolkit.utils.test.patchExpectedResult
import org.ossreviewtoolkit.utils.test.readOrtResult

class OpossumReporterFunTest : WordSpec({
"generateReport()" should {
val ortResult = readOrtResult("src/funTest/assets/reporter-test-input.yml")
val reportStr = generateReport(ortResult).normalizeLineBreaks()
val replacements = mapOf(
"\"fileCreationDate\":\"[^\"]+\"" to "\"fileCreationDate\":\"${LocalDateTime.MIN}\"",
"\"[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}\"" to "\"00000000-0000-0000-0000-000000000000\""
)

"create '.opossum' output containing an 'input.json' with expected string" {
reportStr shouldContain "fileCreationDate"
}
"The generated report" should {
"match the expected result" {
val ortResult = readOrtResult("src/funTest/assets/reporter-test-input.yml")
val input = ReporterInput(ortResult)

val outputDir = tempdir()
val expectedFile = getAssetFile("reporter-test-output.json")

"create a parseable result and contain some expected values" {
with(JsonMapper().readTree(reportStr)) {
isObject shouldBe true
get("metadata").get("projectId").asText() shouldBe "0"
get("attributionBreakpoints").size() shouldBe 4
get("externalAttributionSources").size() shouldBe 7
get("resourcesToAttributions").fieldNames().asSequence() shouldContain
"/analyzer/src/funTest/assets/projects/synthetic/gradle/lib/build.gradle/" +
"compile/org.apache.commons/[email protected]/dependencies/org.apache.commons/[email protected]"
}
OpossumReporterFactory.create().generateReport(input, outputDir).single().getOrThrow().unpackZip(outputDir)

val actualResult = patchActualResult(outputDir.resolve("input.json").readText(), custom = replacements)
actualResult shouldEqualSpecifiedJsonIgnoringOrder patchExpectedResult(expectedFile)
}
}
})

private fun TestConfiguration.generateReport(ortResult: OrtResult): String {
val input = ReporterInput(ortResult)
val outputDir = tempdir()

OpossumReporterFactory.create().generateReport(input, outputDir).single().getOrThrow().unpackZip(outputDir)

return outputDir.resolve("input.json").readText()
}
255 changes: 255 additions & 0 deletions plugins/reporters/opossum/src/main/kotlin/OpossumModel.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
/*
* Copyright (C) 2024 The ORT Project Authors (see <https://github.com/oss-review-toolkit/ort/blob/main/NOTICE>)
*
* 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.plugins.reporters.opossum

import io.ks3.java.typealiases.LocalDateTimeAsString
import io.ks3.java.typealiases.UuidAsString

import java.io.File
import java.time.LocalDateTime

import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.MapSerializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.encodeToStream

import org.ossreviewtoolkit.model.Identifier
import org.ossreviewtoolkit.model.utils.getPurlType
import org.ossreviewtoolkit.utils.spdx.SpdxExpression

internal val JSON = Json {
explicitNulls = false
encodeDefaults = true
}

internal fun File.writeReport(opossumInput: OpossumInput): File =
apply { outputStream().use { JSON.encodeToStream(opossumInput, it) } }

@Serializable
internal data class OpossumInput(
val metadata: OpossumInputMetadata = OpossumInputMetadata(),
val resources: OpossumResources,
val externalAttributions: Map<UuidAsString, OpossumSignalFlat>,
val resourcesToAttributions: Map<String, Set<UuidAsString>>,
val attributionBreakpoints: Set<String>,
val filesWithChildren: Set<String>,
val frequentLicenses: Set<OpossumFrequentLicense>,
val baseUrlsForSources: Map<String, String>,
val externalAttributionSources: Map<String, OpossumExternalAttributionSource>
) {
fun getSignalsForFile(file: String): List<OpossumSignalFlat> =

Check warning on line 62 in plugins/reporters/opossum/src/main/kotlin/OpossumModel.kt

View workflow job for this annotation

GitHub Actions / qodana-scan

Unused symbol

Function "getSignalsForFile" is never used

Check warning

Code scanning / QDJVMC

Unused symbol Warning

Function "getSignalsForFile" is never used
resourcesToAttributions[file].orEmpty().mapNotNull { uuid -> externalAttributions[uuid] }
}

@Serializable
internal data class OpossumInputMetadata(
val projectId: String = "0",
val fileCreationDate: LocalDateTimeAsString = LocalDateTime.now()
)

@Serializable(OpossumResourcesSerializer::class)
internal data class OpossumResources(
val tree: MutableMap<String, OpossumResources> = mutableMapOf()
) {
fun addResource(pathPieces: List<String>) {

Check notice on line 76 in plugins/reporters/opossum/src/main/kotlin/OpossumModel.kt

View workflow job for this annotation

GitHub Actions / qodana-scan

Class member can have 'private' visibility

Function 'addResource' could be private

Check notice

Code scanning / QDJVMC

Class member can have 'private' visibility Note

Function 'addResource' could be private
if (pathPieces.isEmpty()) {
return
}

val head = pathPieces.first()
val tail = pathPieces.drop(1)

if (head !in tree) {
tree[head] = OpossumResources()
}

tree.getValue(head).addResource(tail)
}

fun addResource(path: String) {
val pathPieces = path.split("/").filter { it.isNotEmpty() }

addResource(pathPieces)
}

fun isFile() = tree.isEmpty()

fun isPathAFile(path: String): Boolean {
val pathPieces = path.split("/").filter { it.isNotEmpty() }

return isPathAFile(pathPieces)
}

fun isPathAFile(pathPieces: List<String>): Boolean {

Check notice on line 105 in plugins/reporters/opossum/src/main/kotlin/OpossumModel.kt

View workflow job for this annotation

GitHub Actions / qodana-scan

Class member can have 'private' visibility

Function 'isPathAFile' could be private

Check notice

Code scanning / QDJVMC

Class member can have 'private' visibility Note

Function 'isPathAFile' could be private
if (pathPieces.isEmpty()) {
return isFile()
}

val head = pathPieces.first()
val tail = pathPieces.drop(1)

return head !in tree || tree.getValue(head).isPathAFile(tail)
}

fun toFileList(): Set<String> =

Check notice on line 116 in plugins/reporters/opossum/src/main/kotlin/OpossumModel.kt

View workflow job for this annotation

GitHub Actions / qodana-scan

Class member can have 'private' visibility

Function 'toFileList' could be private

Check warning on line 116 in plugins/reporters/opossum/src/main/kotlin/OpossumModel.kt

View workflow job for this annotation

GitHub Actions / qodana-scan

Unused symbol

Function "toFileList" is never used

Check notice

Code scanning / QDJVMC

Class member can have 'private' visibility Note

Function 'toFileList' could be private

Check warning

Code scanning / QDJVMC

Unused symbol Warning

Function "toFileList" is never used
tree.flatMapTo(mutableSetOf()) { (key, value) ->
value.toFileList().map { resolvePath(key, it, isDirectory = false) }
}.plus("/")
}

private object OpossumResourcesSerializer : KSerializer<OpossumResources> {
override val descriptor = buildClassSerialDescriptor("Resource")

override fun serialize(encoder: Encoder, value: OpossumResources) {
if (value.isFile()) {
encoder.encodeInt(1)
} else {
encoder.encodeSerializableValue(MapSerializer(String.serializer(), this), value.tree)
}
}

override fun deserialize(decoder: Decoder): OpossumResources {
throw NotImplementedError("Deserialization of OpossumResources is not supported.")
}
}

@Serializable
internal data class OpossumSignalFlat(
val source: OpossumSignalSource,
val attributionConfidence: Int = 80,
val packageType: String?,
val packageNamespace: String?,
val packageName: String?,
val packageVersion: String?,
val copyright: String?,
val licenseName: String?,
val url: String?,
val preSelected: Boolean,
val followUp: OpossumFollowUp?,
val excludeFromNotice: Boolean,
val comment: String?
) {
companion object {
fun create(signal: OpossumSignal): OpossumSignalFlat =
OpossumSignalFlat(
source = signal.base.source,
attributionConfidence = signal.attributionConfidence,
packageType = signal.base.packageType,
packageNamespace = signal.base.packageNamespace,
packageName = signal.base.packageName,
packageVersion = signal.base.packageVersion,
copyright = signal.base.copyright,
licenseName = signal.base.licenseName,
url = signal.base.url,
preSelected = signal.base.preSelected,
followUp = signal.followUp,
excludeFromNotice = signal.excludeFromNotice,
comment = signal.base.comment
)
}

data class OpossumSignal(
val base: OpossumSignalBase,
val attributionConfidence: Int = 80,
val followUp: OpossumFollowUp?,
val excludeFromNotice: Boolean
) {
companion object {
@Suppress("LongParameterList")
fun create(
source: String,
id: Identifier? = null,
url: String? = null,
license: SpdxExpression? = null,
copyright: String? = null,
comment: String? = null,
preSelected: Boolean = false,
followUp: Boolean = false,
excludeFromNotice: Boolean = false
): OpossumSignal =
OpossumSignal(
base = OpossumSignalBase(
source = OpossumSignalSource(name = source),
packageType = id?.getPurlType().toString(),
packageNamespace = id?.namespace,
packageName = id?.name,
packageVersion = id?.version,
copyright = copyright,
licenseName = license?.toString(),
url = url,
preSelected = preSelected,
comment = comment
),
followUp = OpossumFollowUp.FOLLOW_UP.takeIf { followUp },
excludeFromNotice = excludeFromNotice
)
}

data class OpossumSignalBase(
val source: OpossumSignalSource,
val packageType: String?,
val packageNamespace: String?,
val packageName: String?,
val packageVersion: String?,
val copyright: String?,
val licenseName: String?,
val url: String?,
val preSelected: Boolean,
val comment: String?
)
}
}

@Serializable
internal data class OpossumSignalSource(
val name: String,
val documentConfidence: Int = 80
)

internal enum class OpossumFollowUp {
FOLLOW_UP
}

@Serializable
internal data class OpossumFrequentLicense(
val shortName: String,
val fullName: String?,
val defaultText: String?
) : Comparable<OpossumFrequentLicense> {
override fun compareTo(other: OpossumFrequentLicense) =
compareValuesBy(
this,
other,
{ it.shortName },
{ it.fullName },
{ it.defaultText }
)
}

@Serializable
internal data class OpossumExternalAttributionSource(
val name: String,
val priority: Int
)
Loading
Loading