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

feat(scanner): Add branch name to FossID scan code #8641

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: 3 additions & 7 deletions plugins/scanners/fossid/src/main/kotlin/FossId.kt
Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,7 @@ class FossId internal constructor(
val scanCodeAndId = if (existingScan == null) {
logger.info { "No scan found for $url and revision $revision. Creating scan..." }

val scanCode = namingProvider.createScanCode(projectName)
val scanCode = namingProvider.createScanCode(projectName = projectName, branch = revision)
val newUrl = urlProvider.getUrl(url)
val scanId = createScan(projectCode, scanCode, newUrl, revision)

Expand Down Expand Up @@ -510,10 +510,6 @@ class FossId internal constructor(
val defaultBranch = vcs.getDefaultBranchName(url)
logger.info { "Default branch is '$defaultBranch'." }

// If a scan for the default branch is created, put the default branch name in the scan code (the
// FossIdNamingProvider must also have a scan pattern that makes use of it).
val branchLabel = projectRevision.takeIf { defaultBranch == projectRevision }.orEmpty()

if (projectRevision == null) {
logger.warn { "No project revision has been given." }
} else {
Expand Down Expand Up @@ -545,13 +541,13 @@ class FossId internal constructor(
logger.info {
"No scan found for $mappedUrlWithoutCredentials and revision $revision. Creating origin scan..."
}
namingProvider.createScanCode(projectName, DeltaTag.ORIGIN, branchLabel)
namingProvider.createScanCode(projectName, DeltaTag.ORIGIN, revision)
} else {
logger.info { "Scan '${existingScan.code}' found for $mappedUrlWithoutCredentials and revision $revision." }
logger.info {
"Existing scan has for reference(s): ${existingScan.comment.orEmpty()}. Creating delta scan..."
}
namingProvider.createScanCode(projectName, DeltaTag.DELTA, branchLabel)
namingProvider.createScanCode(projectName, DeltaTag.DELTA, revision)
}

val scanId = createScan(projectCode, scanCode, mappedUrl, revision, projectRevision.orEmpty())
Expand Down
79 changes: 76 additions & 3 deletions plugins/scanners/fossid/src/main/kotlin/FossIdNamingProvider.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import org.apache.logging.log4j.kotlin.logger
* * **currentTimestamp**: The current time.
* * **deltaTag** (scan code only): If delta scans is enabled, this qualifies the scan as an *origin* scan or a *delta*
* scan.
* * **branch**: branch name (revision) given to scan
*/
class FossIdNamingProvider(
private val namingProjectPattern: String?,
Expand All @@ -46,6 +47,8 @@ class FossIdNamingProvider(
companion object {
@JvmStatic
val FORMATTER: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss")

const val MAX_SCAN_CODE_LEN = 254
}

fun createProjectCode(projectName: String): String =
Expand All @@ -57,16 +60,86 @@ class FossIdNamingProvider(
} ?: projectName

fun createScanCode(projectName: String, deltaTag: FossId.DeltaTag? = null, branch: String = ""): String {
return namingScanPattern?.let {
createScanCodeForCustomPattern(namingScanPattern, projectName, deltaTag, branch)
} ?: run {
createScanCodeForDefaultPattern(projectName, deltaTag, branch)
}
}

private fun createScanCodeForDefaultPattern(
projectName: String,
deltaTag: FossId.DeltaTag? = null,
branch: String = ""
): String {
val builtins = mutableMapOf("#projectName" to projectName)
var defaultPattern = "#projectName_#currentTimestamp"
val builtins = mutableMapOf("#projectName" to projectName, "#branch" to branch)

deltaTag?.let {
defaultPattern += "_#deltaTag"
builtins += "#deltaTag" to deltaTag.name.lowercase()
}

val pattern = namingScanPattern ?: defaultPattern
return replaceNamingConventionVariables(pattern, builtins, namingConventionVariables)
if (branch.isNotBlank()) {
val branchName = normalizeBranchName(branch, defaultPattern, builtins)
defaultPattern += "_#branch"
builtins += "#branch" to branchName
}

return replaceNamingConventionVariables(defaultPattern, builtins, namingConventionVariables)
}

private fun createScanCodeForCustomPattern(
namingPattern: String,
projectName: String,
deltaTag: FossId.DeltaTag? = null,
branch: String = ""
): String {
val builtins = mutableMapOf<String, String>()

namingPattern.contains("#projectName").let {
builtins += "#projectName" to projectName
}

namingPattern.contains("#deltaTag").let {
if (deltaTag != null) {
builtins += "#deltaTag" to deltaTag.name.lowercase()
}
}

namingPattern.contains("#branch").let {
val namingPatternWithoutBranchPlaceholder = namingPattern.replace("#branch", "")
builtins += "#branch" to normalizeBranchName(branch, namingPatternWithoutBranchPlaceholder, builtins)
}

return replaceNamingConventionVariables(namingPattern, builtins, namingConventionVariables)
}

/**
* Replaces non-standard characters in branch name and trims its length to one that will not exceed
* maximum length of FossID scan ID, when combined with the rest of variables
*/
private fun normalizeBranchName(
branch: String,
scanCodeNamingPattern: String,
scanCodeVariables: Map<String, String>
): String {
val noBranchScanCode =
replaceNamingConventionVariables(
scanCodeNamingPattern,
scanCodeVariables,
namingConventionVariables
)

require(noBranchScanCode.length < MAX_SCAN_CODE_LEN) {
throw IllegalArgumentException(
"FossID scan code '$noBranchScanCode' is too long. " +
"It must not exceed $MAX_SCAN_CODE_LEN characters. Please consider shorter naming scan pattern."
)
}

val maxBranchNameLength = MAX_SCAN_CODE_LEN - noBranchScanCode.length
return branch.replace(Regex("[^a-zA-Z0-9-_]"), "_").take(maxBranchNameLength)
}

/**
Expand Down
166 changes: 166 additions & 0 deletions plugins/scanners/fossid/src/test/kotlin/FossIdNamingProviderTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/*
* 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.scanners.fossid

import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.WordSpec
import io.kotest.core.test.TestCase
import io.kotest.core.test.TestResult
import io.kotest.matchers.equals.shouldBeEqual
import io.kotest.matchers.ints.shouldBeLessThanOrEqual

import io.mockk.every
import io.mockk.mockkStatic
import io.mockk.unmockkAll

import java.time.LocalDateTime

class FossIdNamingProviderTest : WordSpec() {

Check warning on line 35 in plugins/scanners/fossid/src/test/kotlin/FossIdNamingProviderTest.kt

View workflow job for this annotation

GitHub Actions / qodana-scan

Unused symbol

Class "FossIdNamingProviderTest" is never used

override suspend fun afterEach(testCase: TestCase, result: TestResult) {
unmockkAll()
}

companion object {
const val MAX_SCAN_CODE_LEN = 255
}

init {
"createScanCode" should {
val namingProvider = FossIdNamingProvider(null, null, emptyMap())

val mockedDateTime = LocalDateTime.of(2024, 4, 1, 10, 0)
val expectedTimestamp = "20240401_100000"

val longBranchName = "origin/feature/LOREM-123321_lorem-ipsum-dolor-sit-amet-consectetur-adipiscing-elit-" +
"aliquam-laoreet-ac-nulla-in-bibendum-phasellus-sodales-vel-lorem-consequat-efficitur-morbi-viverra-a" +
"ccumsan-libero-a-tincidunt-libero-venenatis-nec-nulla-facilisi-vestibulum-pharetra-finibus-mi-vitae-" +
"luctus"

val longScanPattern = "#projectName_#currentTimestamp_lorem-ipsum-dolor-sit-amet-consectetur-adipiscing-e" +
"lit-aliquam-laoreet-ac-nulla-in-bibendum-phasellus-sodales-vel-lorem-consequat-efficitur-morbi-viver" +
"ra-accumsan-libero-a-tincidunt-libero-venenatis-nec-nulla-facilisi-vestibulum-pharetra-finibus-mi-vi" +
"tae-luctus"

"create code without branch name, when it's empty" {
mockkStatic(LocalDateTime::class) {
every { LocalDateTime.now() } returns mockedDateTime

namingProvider.createScanCode(
"example-project-name", null, ""
) shouldBeEqual "example-project-name_$expectedTimestamp"
}
}

"create code with branch name" {
mockkStatic(LocalDateTime::class) {
every { LocalDateTime.now() } returns mockedDateTime

namingProvider.createScanCode(
"example-project-name", null, "CODE-2233_Red-dots-added-to-layout"
) shouldBeEqual "example-project-name_" + expectedTimestamp + "_CODE-2233_Red-dots-added-to-layout"
}
unmockkAll()
}

"create code with branch name and delta tag" {
mockkStatic(LocalDateTime::class) {
every { LocalDateTime.now() } returns mockedDateTime

namingProvider.createScanCode(
"example-project-name", FossId.DeltaTag.DELTA, "CODE-2233_Red-dots-added-to-layout"
) shouldBeEqual "example-project-name_" + expectedTimestamp +
"_delta_CODE-2233_Red-dots-added-to-layout"
}
unmockkAll()
}

"remove all non-standard signs from branch name when creating code" {
mockkStatic(LocalDateTime::class) {
every { LocalDateTime.now() } returns mockedDateTime

namingProvider.createScanCode(
"example-project-name", null, "feature/CODE-12%%$@@&^_SOME_*&^#!*text!!"
) shouldBeEqual "example-project-name_" +
expectedTimestamp + "_feature_CODE-12________SOME_______text__"
}
}

"truncate very long scan id to fit maximum length accepted by FossID (255 chars)" {
mockkStatic(LocalDateTime::class) {
every { LocalDateTime.now() } returns mockedDateTime

namingProvider.createScanCode(
"example-project-name", FossId.DeltaTag.DELTA, longBranchName
).length shouldBeLessThanOrEqual MAX_SCAN_CODE_LEN
}
}

"create code without branch name form custom naming pattern" {
val customScanPattern = "#projectName_#currentTimestamp"

val namingProviderWithLongScanPattern = FossIdNamingProvider(null, customScanPattern, emptyMap())
mockkStatic(LocalDateTime::class) {
every { LocalDateTime.now() } returns mockedDateTime

namingProviderWithLongScanPattern.createScanCode(
"example-project-name", null, ""
) shouldBeEqual "example-project-name_20240401_100000"
}
}

"create code without branch name form custom naming pattern when branch name is provided" {
val customScanPattern = "#projectName_#currentTimestamp"

val namingProviderWithLongScanPattern = FossIdNamingProvider(null, customScanPattern, emptyMap())
mockkStatic(LocalDateTime::class) {
every { LocalDateTime.now() } returns mockedDateTime

namingProviderWithLongScanPattern.createScanCode(
"example-project-name", null, "feature/LOREM-3212"
) shouldBeEqual "example-project-name_20240401_100000"
}
}

"create code without branch name form custom naming pattern when too long branch name is provided" {
val customScanPattern = "#projectName_#currentTimestamp_#branch"
val namingProviderWithLongScanPattern = FossIdNamingProvider(null, customScanPattern, emptyMap())
mockkStatic(LocalDateTime::class) {
every { LocalDateTime.now() } returns mockedDateTime

namingProviderWithLongScanPattern.createScanCode(
"example-project-name", null, longBranchName
).length shouldBeLessThanOrEqual MAX_SCAN_CODE_LEN
}
}

"throw an exception if scan code pattern is too long" {
val namingProviderWithLongScanPattern = FossIdNamingProvider(null, longScanPattern, emptyMap())
mockkStatic(LocalDateTime::class) {
every { LocalDateTime.now() } returns mockedDateTime

shouldThrow<IllegalArgumentException> {
namingProviderWithLongScanPattern.createScanCode("example-project-name", null, "")
}
}
}
}
}
}
Loading