Skip to content

Commit

Permalink
feat(bazel): Add MultiBazelModuleRegistryService class
Browse files Browse the repository at this point in the history
This is a special `BazelModuleRegistryService` implementation that
wraps an arbitrary number of other registry services. On receiving a
request, it delegates to the other registries and returns the first
successful result.

This implementation is going to be used to support multiple registries
declared in a `.bazelrc` file.

Signed-off-by: Oliver Heger <oliver.heger@bosch.io>
  • Loading branch information
oheger-bosch committed Aug 6, 2024
1 parent 5dd19ff commit ebd6454
Show file tree
Hide file tree
Showing 3 changed files with 337 additions and 0 deletions.
2 changes: 2 additions & 0 deletions plugins/package-managers/bazel/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,7 @@ dependencies {
implementation(libs.kotlinx.serialization.core)
implementation(libs.kotlinx.serialization.json)

testImplementation(libs.mockk)

funTestImplementation(testFixtures(projects.analyzer))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* 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.packagemanagers.bazel

import java.io.File

import org.ossreviewtoolkit.clients.bazelmoduleregistry.BazelModuleRegistryService
import org.ossreviewtoolkit.clients.bazelmoduleregistry.LocalBazelModuleRegistryService
import org.ossreviewtoolkit.clients.bazelmoduleregistry.ModuleMetadata
import org.ossreviewtoolkit.clients.bazelmoduleregistry.ModuleSourceInfo
import org.ossreviewtoolkit.clients.bazelmoduleregistry.RemoteBazelModuleRegistryService

/**
* A special implementation of [BazelModuleRegistryService] that wraps an arbitrary number of other
* [BazelModuleRegistryService] instances. It can be used for projects that declare multiple registries.
*
* The functions of the interface are implemented by iterating over the wrapped services and returning the first
* successful result.
*/
internal class MultiBazelModuleRegistryService(
/** The wrapped [BazelModuleRegistryService] instances. */
private val registryServices: Collection<BazelModuleRegistryService>
) : BazelModuleRegistryService {
companion object {
/**
* Create an instance of [MultiBazelModuleRegistryService] for the given [registryUrls]. Based on the URLs,
* concrete [BazelModuleRegistryService] implementations are created. Local registry services use the given
* [projectDir] as workspace. These services are then queried in the order defined by the passed in collection.
* Note that as the last service a remote module registry for the Bazel Central Registry is added that serves
* as a fallback.
*/
fun create(registryUrls: Collection<String>, projectDir: File): MultiBazelModuleRegistryService {
val registryServices = registryUrls.mapTo(mutableListOf()) { url ->
LocalBazelModuleRegistryService.createForLocalUrl(url, projectDir)
?: RemoteBazelModuleRegistryService.create(url)
}

// Add the default Bazel registry as a fallback.
registryServices += RemoteBazelModuleRegistryService.create(null)

return MultiBazelModuleRegistryService(registryServices)
}

/**
* Return an exception with a message that combines the messages of all [Throwable]s in this list.
*/
private fun List<Throwable>.combinedException(caption: String): Throwable =
IllegalArgumentException(
"$caption:\n${joinToString("\n") { it.message.orEmpty() }}"
)
}

override suspend fun getModuleMetadata(name: String): ModuleMetadata =
queryRegistryServices(
errorMessage = { "Failed to query metadata for package '$name'" },
query = { it.getModuleMetadata(name) }
)

override suspend fun getModuleSourceInfo(name: String, version: String): ModuleSourceInfo =
queryRegistryServices(
errorMessage = { "Failed to query source info for package '$name' and version '$version'" },
query = { it.getModuleSourceInfo(name, version) }
)

/**
* A generic function for sending a [query] to all managed [BazelModuleRegistryService] instances and returning the
* first successful result. In case no registry service can provide a result, throw an exception with the given
* [errorMessage] and a summary of all failures.
*/
private suspend fun <T> queryRegistryServices(
errorMessage: () -> String,
query: suspend (BazelModuleRegistryService) -> T
): T {
val failures = mutableListOf<Throwable>()

tailrec suspend fun queryServices(itServices: Iterator<BazelModuleRegistryService>): T? =
if (!itServices.hasNext()) {
null
} else {
val triedResult = runCatching { query(itServices.next()) }
val result = triedResult.getOrNull()

// The Elvis operator does not work here because of the tailrec modifier.
if (result != null) {
result
} else {
triedResult.exceptionOrNull()?.let(failures::add)
queryServices(itServices)
}
}

val info = queryServices(registryServices.iterator())
return info ?: throw failures.combinedException(errorMessage())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
/*
* 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.packagemanagers.bazel

import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.WordSpec
import io.kotest.engine.spec.tempdir
import io.kotest.matchers.shouldBe

import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkObject
import io.mockk.unmockkAll

import java.io.File

import org.ossreviewtoolkit.clients.bazelmoduleregistry.BazelModuleRegistryService
import org.ossreviewtoolkit.clients.bazelmoduleregistry.LocalBazelModuleRegistryService
import org.ossreviewtoolkit.clients.bazelmoduleregistry.ModuleMetadata
import org.ossreviewtoolkit.clients.bazelmoduleregistry.ModuleSourceInfo
import org.ossreviewtoolkit.clients.bazelmoduleregistry.RemoteBazelModuleRegistryService

class MultiBazelModuleRegistryServiceTest : WordSpec({
beforeTest {
mockkObject(LocalBazelModuleRegistryService, RemoteBazelModuleRegistryService)
}

afterTest {
unmockkAll()
}

"getModuleMetadata" should {
"throw an exception if no metadata can be obtained from all registries" {
val projectDir = tempdir()
val mockRegistries = MockRegistryServices.create(projectDir)
mockRegistries.prepareFailedMetadata()

val multiRegistry = MultiBazelModuleRegistryService.create(registryUrls, projectDir)

shouldThrow<IllegalArgumentException> {
multiRegistry.getModuleMetadata(PACKAGE_NAME)
}
}

"return the metadata from the first registry that contains it" {
val projectDir = tempdir()
val mockRegistries = MockRegistryServices.create(projectDir)
val metadata = mockk<ModuleMetadata>()
mockRegistries.prepareSuccessMetadata(1, metadata)

val multiRegistry = MultiBazelModuleRegistryService.create(registryUrls, projectDir)

multiRegistry.getModuleMetadata(PACKAGE_NAME) shouldBe metadata
}

"fall back to the default registry if no metadata can be obtained from the other registries" {
val projectDir = tempdir()
val mockRegistries = MockRegistryServices.create(projectDir)
val metadata = mockk<ModuleMetadata>()
mockRegistries.prepareSuccessMetadata(3, metadata)

val multiRegistry = MultiBazelModuleRegistryService.create(registryUrls, projectDir)

multiRegistry.getModuleMetadata(PACKAGE_NAME) shouldBe metadata
}
}

"getModuleSourceInfo" should {
"throw an exception if no source info can be obtained from all registries" {
val projectDir = tempdir()
val mockRegistries = MockRegistryServices.create(projectDir)
mockRegistries.prepareFailedSourceInfo()

val multiRegistry = MultiBazelModuleRegistryService.create(registryUrls, projectDir)

shouldThrow<IllegalArgumentException> {
multiRegistry.getModuleSourceInfo(PACKAGE_NAME, PACKAGE_VERSION)
}
}

"return the source info from the first registry that contains it" {
val projectDir = tempdir()
val mockRegistries = MockRegistryServices.create(projectDir)
val sourceInfo = mockk<ModuleSourceInfo>()
mockRegistries.prepareSuccessModuleInfo(1, sourceInfo)

val multiRegistry = MultiBazelModuleRegistryService.create(registryUrls, projectDir)

multiRegistry.getModuleSourceInfo(PACKAGE_NAME, PACKAGE_VERSION) shouldBe sourceInfo
}

"fall back to the default registry if no source info can be obtained from the other registries" {
val projectDir = tempdir()
val mockRegistries = MockRegistryServices.create(projectDir)
val sourceInfo = mockk<ModuleSourceInfo>()
mockRegistries.prepareSuccessModuleInfo(3, sourceInfo)

val multiRegistry = MultiBazelModuleRegistryService.create(registryUrls, projectDir)

multiRegistry.getModuleSourceInfo(PACKAGE_NAME, PACKAGE_VERSION) shouldBe sourceInfo
}
}
})

/** Name of a test package. */
private const val PACKAGE_NAME = "test-package"

/** Version of a test package. */
private const val PACKAGE_VERSION = "0.8.15"

/** A list of URLs for local and remote test registries. */
private val registryUrls = listOf(
"file://local/registry/url",
"https://bazel-remote.example.com",
"file://%workspace%/registry"
)

private class MockRegistryServices(
val registryServices: List<BazelModuleRegistryService>
) {
companion object {
/** An exception thrown by mock registries to simulate a failure. */
private val testException = Exception("Test exception: Registry invocation failed.")

/**
* Create an instance of [MockRegistryServices] with mocks for the test registry URLs. The factory methods
* of the local and remote service implementations have been prepared to return the mock instances.
*/
fun create(projectDir: File): MockRegistryServices {
val localRegistry1 = mockk<LocalBazelModuleRegistryService>()
val localRegistry2 = mockk<LocalBazelModuleRegistryService>()
val remoteRegistry = mockk<RemoteBazelModuleRegistryService>()
val centralRegistry = mockk<RemoteBazelModuleRegistryService>()

every {
LocalBazelModuleRegistryService.createForLocalUrl(any(), projectDir)
} returns null
every {
LocalBazelModuleRegistryService.createForLocalUrl(registryUrls[0], projectDir)
} returns localRegistry1
every {
LocalBazelModuleRegistryService.createForLocalUrl(registryUrls[2], projectDir)
} returns localRegistry2
every {
RemoteBazelModuleRegistryService.create(registryUrls[1])
} returns remoteRegistry
every {
RemoteBazelModuleRegistryService.create(url = null)
} returns centralRegistry

return MockRegistryServices(listOf(localRegistry1, localRegistry2, remoteRegistry, centralRegistry))
}

/**
* Prepare the given [services] mocks to expect a query for module metadata and to fail with a test exception.
*/
private fun prepareFailedMetadata(services: Collection<BazelModuleRegistryService>) {
services.forEach { service ->
coEvery { service.getModuleMetadata(PACKAGE_NAME) } throws testException
}
}

/**
* Prepare the given [services] mocks to expect a query for module source info and to fail with a test
* exception.
*/
fun prepareFailedSourceInfo(services: Collection<BazelModuleRegistryService>) {
services.forEach { service ->
coEvery { service.getModuleSourceInfo(PACKAGE_NAME, PACKAGE_VERSION) } throws testException
}
}
}

/**
* Prepare the mock registries to expect a query for module metadata and to fail with a test exception.
*/
fun prepareFailedMetadata() {
prepareFailedMetadata(registryServices)
}

/**
* Prepare the mock registries to expect a query for module source info and to fail with a test exception.
*/
fun prepareFailedSourceInfo() {
prepareFailedSourceInfo(registryServices)
}

/**
* Prepare the mock registries to expect a query for module metadata that will eventually succeed. The registry
* with the given [index] is configured to return the given [metadata].
*/
fun prepareSuccessMetadata(index: Int, metadata: ModuleMetadata) {
prepareFailedMetadata(registryServices.take(index))
coEvery { registryServices[index].getModuleMetadata(PACKAGE_NAME) } returns metadata
}

/**
* Prepare the mock registries to expect a query for module source info that will eventually succeed. The registry
* with the given [index] is configured to return the given [info].
*/
fun prepareSuccessModuleInfo(index: Int, info: ModuleSourceInfo) {
prepareFailedSourceInfo(registryServices.take(index))
coEvery { registryServices[index].getModuleSourceInfo(PACKAGE_NAME, PACKAGE_VERSION) } returns info
}
}

0 comments on commit ebd6454

Please sign in to comment.