Skip to content

Commit

Permalink
feat(scanner): Add branch name to FossID scan code
Browse files Browse the repository at this point in the history
Add branch name (revision) to FossID scan code for easier
identification of scanned source code.
As FossID accepts maximum of 255 characters in scan code, branch name
is trimmed to size, that is compliant with this constraint.

Signed-off-by: Kamil Bielecki <kamil.bielecki@pl.bosch.com>
  • Loading branch information
Kamil Bielecki authored and oheger-bosch committed Jun 12, 2024
1 parent b2328c7 commit 2e1399c
Show file tree
Hide file tree
Showing 3 changed files with 245 additions and 10 deletions.
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() {

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, "")
}
}
}
}
}
}

0 comments on commit 2e1399c

Please sign in to comment.