Skip to content

Commit

Permalink
chore: Add unit tests for dependency modules
Browse files Browse the repository at this point in the history
  • Loading branch information
tgodzik committed Mar 14, 2024
1 parent 72e88fa commit 804e899
Show file tree
Hide file tree
Showing 4 changed files with 199 additions and 69 deletions.
Expand Up @@ -83,17 +83,17 @@ import org.jetbrains.bsp.bazel.workspacecontext.WorkspaceContextProvider
import java.net.URI
import java.nio.file.Path
import java.nio.file.Paths
import java.util.LinkedList
import java.util.*
import kotlin.io.path.name
import kotlin.io.path.relativeToOrNull
import kotlin.io.path.toPath

class BspProjectMapper(
private val languagePluginsService: LanguagePluginsService,
private val workspaceContextProvider: WorkspaceContextProvider,
private val bazelPathsResolver: BazelPathsResolver,
private val bazelRunner: BazelRunner,
private val bspInfo: BspInfo,
private val languagePluginsService: LanguagePluginsService,
private val workspaceContextProvider: WorkspaceContextProvider,
private val bazelPathsResolver: BazelPathsResolver,
private val bazelRunner: BazelRunner,
private val bspInfo: BspInfo,
) {

fun initializeServer(supportedLanguages: Set<Language>): InitializeBuildResult {
Expand Down Expand Up @@ -202,7 +202,7 @@ class BspProjectMapper(

private fun isBuildableIfManual(module: Module): Boolean =
(!module.tags.contains(Tag.MANUAL)
|| workspaceContextProvider.currentWorkspaceContext().buildManualTargets.value)
|| workspaceContextProvider.currentWorkspaceContext().buildManualTargets.value)


private fun applyLanguageData(module: Module, buildTarget: BuildTarget) {
Expand Down Expand Up @@ -260,7 +260,8 @@ class BspProjectMapper(
val documentUri = BspMappings.toUri(inverseSourcesParams.textDocument)
val documentRelativePath = documentUri
.toPath()
.relativeToOrNull(project.workspaceRoot.toPath()) ?: throw RuntimeException("File path outside of project root")
.relativeToOrNull(project.workspaceRoot.toPath())
?: throw RuntimeException("File path outside of project root")
return InverseSourcesQuery.inverseSourcesQuery(documentRelativePath, bazelRunner, project.bazelRelease, cancelChecker)
}

Expand All @@ -269,9 +270,9 @@ class BspProjectMapper(
): DependencySourcesResult {
fun getDependencySourcesItem(label: Label): DependencySourcesItem {
val sources = project.findModule(label)
?.sourceDependencies
?.map(BspMappings::toBspUri)
.orEmpty()
?.sourceDependencies
?.map(BspMappings::toBspUri)
.orEmpty()
return DependencySourcesItem(BspMappings.toBspId(label), sources)
}

Expand All @@ -295,32 +296,32 @@ class BspProjectMapper(
}

fun jvmRunEnvironment(
project: Project, params: JvmRunEnvironmentParams, cancelChecker: CancelChecker
project: Project, params: JvmRunEnvironmentParams, cancelChecker: CancelChecker
): JvmRunEnvironmentResult {
val targets = params.targets
val result = getJvmEnvironmentItems(project, targets, cancelChecker)
return JvmRunEnvironmentResult(result)
}

fun jvmTestEnvironment(
project: Project, params: JvmTestEnvironmentParams, cancelChecker: CancelChecker
project: Project, params: JvmTestEnvironmentParams, cancelChecker: CancelChecker
): JvmTestEnvironmentResult {
val targets = params.targets
val result = getJvmEnvironmentItems(project, targets, cancelChecker)
return JvmTestEnvironmentResult(result)
}

private fun getJvmEnvironmentItems(
project: Project, targets: List<BuildTargetIdentifier>, cancelChecker: CancelChecker
project: Project, targets: List<BuildTargetIdentifier>, cancelChecker: CancelChecker
): List<JvmEnvironmentItem> {
fun extractJvmEnvironmentItem(module: Module, runtimeClasspath: List<URI>): JvmEnvironmentItem? =
module.javaModule?.let { javaModule ->
JvmEnvironmentItem(
BspMappings.toBspId(module),
runtimeClasspath.map { it.toString() },
javaModule.jvmOps.toList(),
bazelPathsResolver.unresolvedWorkspaceRoot().toString(),
module.environmentVariables
BspMappings.toBspId(module),
runtimeClasspath.map { it.toString() },
javaModule.jvmOps.toList(),
bazelPathsResolver.unresolvedWorkspaceRoot().toString(),
module.environmentVariables
).apply {
mainClasses = javaModule.mainClass?.let { listOf(JvmMainClass(it, javaModule.args)) }.orEmpty()
}
Expand Down Expand Up @@ -365,9 +366,9 @@ class BspProjectMapper(
}

fun buildTargetScalacOptions(
project: Project,
params: ScalacOptionsParams,
cancelChecker: CancelChecker
project: Project,
params: ScalacOptionsParams,
cancelChecker: CancelChecker
): ScalacOptionsResult {
val items = params.targets.collectClasspathForTargetsAndApply(project, cancelChecker) { module, ideClasspath ->
toScalacOptionsItem(module, ideClasspath)
Expand Down Expand Up @@ -438,56 +439,20 @@ class BspProjectMapper(
return ScalaMainClassesResult(items)
}

private fun extractMavenDependencyInfo(lib: Library): MavenDependencyModule? {
val jars = lib.outputs.map { uri -> uri.toString() }.map {
MavenDependencyModuleArtifact(it)
}
val sourceJars = lib.sources.map { uri -> uri.toString() }.map {
val artifact = MavenDependencyModuleArtifact(it)
artifact.classifier = "sources"
artifact
}

// Matches the Maven group (organization), artifact, and version in the Bazel dependency
// string such as .../execroot/monorepo/bazel-out/k8-fastbuild/bin/external/maven/com/google/guava/guava/31.1-jre/processed_guava-31.1-jre.jar
val regexPattern = """.*/maven/(.+)/([^/]+)/([^/]+)/[^/]+.jar""".toRegex()
val dependencyPath = lib.outputs.first().toString()
// Find matches in the dependency path
val matchResult = regexPattern.find(dependencyPath)

// If a match is found, group values are extracted; otherwise, null is returned
return matchResult?.let {
val (organization, artifact, version) = it.destructured
MavenDependencyModule(organization.replace("/", "."), artifact, version, jars + sourceJars)
}
}

private fun allModuleDependencies(project: Project, module: Module): HashSet<Library> {
val toResolve = LinkedList<String>()
toResolve.addAll(module.directDependencies.map { it.value })
val accumulator = HashSet<Library>()
while (toResolve.isNotEmpty()){
val lib = project.libraries[toResolve.pop()]
if (lib != null && !accumulator.contains(lib)) {
accumulator.add(lib)
toResolve.addAll(lib.dependencies)
}
}
return accumulator
}

fun buildDependencyModules(project: Project, params: DependencyModulesParams): DependencyModulesResult {
val targetSet = params.targets.toSet()
val dependencyModulesItems = project.modules.filter { targetSet.contains(BuildTargetIdentifier(it.label.value)) }.map { module ->
val buildTargetId = BuildTargetIdentifier(module.label.value)
val moduleItems = allModuleDependencies(project, module).map { libraryDep ->
val mavenDependencyModule = if (libraryDep.outputs.isNotEmpty()) extractMavenDependencyInfo(libraryDep) else null
val dependencyModule = DependencyModule(libraryDep.label, mavenDependencyModule?.version ?: "")
if (mavenDependencyModule != null) {
dependencyModule.data = mavenDependencyModule
dependencyModule.dataKind = DependencyModuleDataKind.MAVEN
}
dependencyModule
val moduleItems = DependencyMapper.allModuleDependencies(project, module).flatMap { libraryDep ->
if (libraryDep.outputs.isNotEmpty()) {
val mavenDependencyModule = DependencyMapper.extractMavenDependencyInfo(libraryDep)
val dependencyModule = DependencyModule(libraryDep.label, mavenDependencyModule?.version ?: "")
if (mavenDependencyModule != null) {
dependencyModule.data = mavenDependencyModule
dependencyModule.dataKind = DependencyModuleDataKind.MAVEN
}
listOf(dependencyModule)
} else emptyList()
}
DependencyModulesItem(buildTargetId, moduleItems)
}
Expand All @@ -500,7 +465,7 @@ class BspProjectMapper(
): RustWorkspaceResult {
val allRustModules = project.findModulesByLanguage(Language.RUST)
val requestedModules = BspMappings.getModules(project, params.targets)
.filter { Language.RUST in it.languages }
.filter { Language.RUST in it.languages }
val toRustWorkspaceResult = languagePluginsService.rustLanguagePlugin::toRustWorkspaceResult

return toRustWorkspaceResult(requestedModules, allRustModules)
Expand Down
@@ -0,0 +1,52 @@
package org.jetbrains.bsp.bazel.server.sync

import ch.epfl.scala.bsp4j.MavenDependencyModule
import ch.epfl.scala.bsp4j.MavenDependencyModuleArtifact
import org.jetbrains.bsp.bazel.server.sync.model.Library
import org.jetbrains.bsp.bazel.server.sync.model.Module
import org.jetbrains.bsp.bazel.server.sync.model.Project
import java.util.*
import kotlin.collections.HashSet

object DependencyMapper {

fun extractMavenDependencyInfo(lib: Library): MavenDependencyModule? {
if (lib.outputs.isEmpty()) return null
val jars = lib.outputs.map { uri -> uri.toString() }.map {
MavenDependencyModuleArtifact(it)
}
val sourceJars = lib.sources.map { uri -> uri.toString() }.map {
val artifact = MavenDependencyModuleArtifact(it)
artifact.classifier = "sources"
artifact
}

// Matches the Maven group (organization), artifact, and version in the Bazel dependency
// string such as .../execroot/monorepo/bazel-out/k8-fastbuild/bin/external/maven/com/google/guava/guava/31.1-jre/processed_guava-31.1-jre.jar
val regexPattern = """.*/maven/(.+)/([^/]+)/([^/]+)/[^/]+.jar""".toRegex()
val dependencyPath = lib.outputs.first().toString()
// Find matches in the dependency path
val matchResult = regexPattern.find(dependencyPath)

// If a match is found, group values are extracted; otherwise, null is returned
return matchResult?.let {
val (organization, artifact, version) = it.destructured
MavenDependencyModule(organization.replace("/", "."), artifact, version, jars + sourceJars)
}
}


fun allModuleDependencies(project: Project, module: Module): HashSet<Library> {
val toResolve = LinkedList<String>()
toResolve.addAll(module.directDependencies.map { it.value })
val accumulator = HashSet<Library>()
while (toResolve.isNotEmpty()){
val lib = project.libraries[toResolve.pop()]
if (lib != null && !accumulator.contains(lib)) {
accumulator.add(lib)
toResolve.addAll(lib.dependencies)
}
}
return accumulator
}
}
Expand Up @@ -18,3 +18,12 @@ kt_test(
"//server/src/main/kotlin/org/jetbrains/bsp/bazel/server/sync",
],
)

kt_test(
name = "DependencyMapperTest",
size = "small",
src = "DependencyMapperTest.kt",
deps = [
"//server/src/main/kotlin/org/jetbrains/bsp/bazel/server/sync",
],
)
@@ -0,0 +1,104 @@
package org.jetbrains.bsp.bazel.server.sync

import ch.epfl.scala.bsp4j.MavenDependencyModule
import ch.epfl.scala.bsp4j.MavenDependencyModuleArtifact
import io.kotest.matchers.shouldBe
import org.jetbrains.bsp.bazel.bazelrunner.BazelRelease
import org.jetbrains.bsp.bazel.server.sync.model.Label
import org.jetbrains.bsp.bazel.server.sync.model.Library
import org.jetbrains.bsp.bazel.server.sync.model.Module
import org.jetbrains.bsp.bazel.server.sync.model.Project
import org.jetbrains.bsp.bazel.server.sync.model.SourceSet
import org.junit.jupiter.api.Test
import java.net.URI
import java.nio.file.Paths

class DependencyMapperTest {

private val cacheLocation = "file:///home/user/.cache/bazel/_bazel_user/ae7b7b315151086e31e3b97f9ddba009/execroot/monorepo/bazel-out/k8-fastbuild-ST-4a519fd6d3e4"

@Test
fun `should translate dependency`() {
val jarUri = URI.create("$cacheLocation/bin/external/maven/org/scala-lang/scala-library/2.13.11/processed_scala-library-2.13.11.jar")
val jarSourcesUri = URI.create("$cacheLocation/bin/external/maven/org/scala-lang/scala-library/2.13.11/scala-library-2.13.11-sources.jar")
val lib1 = Library(
"@maven//:org_scala_lang_scala_library",
setOf(jarUri),
setOf(jarSourcesUri),
emptyList()
)
val expectedMavenArtifact = MavenDependencyModuleArtifact(jarUri.toString())
val expectedMavenSourcesArtifact = MavenDependencyModuleArtifact(jarSourcesUri.toString())
expectedMavenSourcesArtifact.classifier = "sources"
val expectedDependency = MavenDependencyModule("org.scala-lang", "scala-library", "2.13.11", listOf(
expectedMavenArtifact,
expectedMavenSourcesArtifact
))
val dependency = DependencyMapper.extractMavenDependencyInfo(lib1)

dependency shouldBe expectedDependency
}

@Test
fun `should not translate non maven dependency`() {
val lib1 = Library(
"@//projects/v1:scheduler",
emptySet(),
emptySet(),
emptyList()
)
val dependency = DependencyMapper.extractMavenDependencyInfo(lib1)

dependency shouldBe null
}

@Test
fun `should gather deps transitively`() {
val jarUri = URI.create("$cacheLocation/bin/external/maven/org/scala-lang/scala-library/2.13.11/processed_scala-library-2.13.11.jar")
val jarSourcesUri = URI.create("$cacheLocation/bin/external/maven/org/scala-lang/scala-library/2.13.11/scala-library-2.13.11-sources.jar")
val lib1 = Library(
"@maven//:org_scala_lang_scala_library",
setOf(jarUri),
setOf(jarSourcesUri),
emptyList()
)
val lib2 = Library(
"@maven//:org_scala_lang_scala_library2",
emptySet(),
emptySet(),
listOf(lib1.label)
)
val lib3 = Library(
"@maven//:org_scala_lang_scala_library3",
emptySet(),
emptySet(),
listOf(lib1.label, lib2.label)
)
val lib4 = Library(
"@maven//:org_scala_lang_scala_library4",
emptySet(),
emptySet(),
listOf(lib3.label, lib2.label)
)
val libraries = mapOf(lib1.label to lib1, lib2.label to lib2, lib3.label to lib3, lib4.label to lib4)
val currentUri = Paths.get(".").toUri()
val project = Project(currentUri, emptyList(), emptyMap(), libraries, emptyList(), BazelRelease(6))
val module = Module(
Label(""),
true,
listOf(Label(lib4.label)),
emptySet(),
emptySet(),
currentUri,
SourceSet(emptySet(), emptySet()),
emptySet(),
emptySet(),
emptySet(),
null,
emptyMap()
)
val foundLibraries = DependencyMapper.allModuleDependencies(project, module)

foundLibraries shouldBe setOf(lib1, lib2, lib3, lib4)
}
}

0 comments on commit 804e899

Please sign in to comment.