Skip to content
This repository has been archived by the owner on May 25, 2022. It is now read-only.

Commit

Permalink
feat: load analyser via absolute path at runtime
Browse files Browse the repository at this point in the history
  • Loading branch information
Andong Liao committed Apr 22, 2022
1 parent 0a19a93 commit 52faea2
Show file tree
Hide file tree
Showing 12 changed files with 244 additions and 25 deletions.
1 change: 1 addition & 0 deletions scanner_cli/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dependencies/analysers/*
1 change: 1 addition & 0 deletions scanner_cli/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2")
// http client, on trial
implementation("io.ktor:ktor-client-core:2.0.0")
implementation("ch.qos.logback:logback-classic:1.3.0-alpha14")
// chapi domain
implementation("com.phodal.chapi:chapi-domain:1.5.6")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ class Runner {
identifier = it.getValue("identifier"),
host = it.getValue("host"),
version = it.getValue("version"),
jar = it.getValue("jar"),
className = it.getValue("className"),
jar = it.getValue("jar"),
)
} ?: emptyList()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@ package org.archguard.scanner.ctl.impl
import org.archguard.scanner.core.client.ArchGuardClient
import org.archguard.scanner.core.sourcecode.SourceCodeContext

// extend SourceCodeContext to accept cil specific parameters(infra related), like memory limit, queue size, etc.
class CliSourceCodeContext(
val systemId: String,
override val language: String,
override val features: List<String>,
override val client: ArchGuardClient,
override val path: String,
override val withoutStorage: Boolean,
) : SourceCodeContext {
// extend SourceCodeContext to accept cil specific parameters(infra related), like memory limit, queue size, etc.
}
) : SourceCodeContext
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,23 @@ package org.archguard.scanner.ctl.impl

import org.archguard.scanner.core.AnalyserSpec

enum class OfficialAnalyserSpecs(val spec: AnalyserSpec) {
JAVA(
AnalyserSpec(
identifier = "java",
host = "https://github.com/archguard/scanner/releases/download/v1.5.0",
version = "1.5.0",
jar = "scan_sourcecode-1.5.0-all.jar",
className = "JavaAnalyser",
)
)
enum class OfficialAnalyserSpecs(
private val host: String,
private val version: String,
private val className: String,
) {
LANG_KOTLIN(
host = "https://github.com/archguard/scanner/tree/master/analyser_sourcecode/lang_kotlin/src/test/resources/kotlin",
version = "1.6.1",
className = "KotlinAnalyser",
),
;

fun spec(): AnalyserSpec {
val identifier = name.lowercase()
return AnalyserSpec(identifier, host, version, "$identifier-$version-all.jar", className)
}

companion object {
fun specs() = values().map(OfficialAnalyserSpecs::spec)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,70 @@ package org.archguard.scanner.ctl.loader
import org.archguard.scanner.core.Analyser
import org.archguard.scanner.core.AnalyserSpec
import org.archguard.scanner.core.context.Context
import org.slf4j.LoggerFactory
import java.net.URLClassLoader
import java.nio.file.Path
import java.nio.file.Paths

// TODO load the scanner via classloader
// 扫描指定包/路径/类
object AnalyserLoader {
private val logger = LoggerFactory.getLogger(this.javaClass)
private val rootPath = Paths.get("").toAbsolutePath()
private const val folder = "dependencies/analysers"

val installPath: Path = rootPath.resolve(folder)

private fun AnalyserSpec.isInstalled(): Boolean {
logger.debug("workspace path: $rootPath")
logger.debug("analyser install path: $installPath")

return (!installPath.toFile().listFiles { _, name -> name == jar }.isNullOrEmpty()).also {
if (it) logger.debug("analyser: $identifier - [$version] is installed")
else logger.debug("analyser: $identifier - [$version] is not installed")
}
}

private fun AnalyserSpec.install() {
when {
// TODO fix windows
host.startsWith("/") -> this.copyFrom()
host.startsWith("http") -> this.download()
else -> throw IllegalArgumentException("please use absolute path or http url to install the analyser")
}
}

private fun AnalyserSpec.download() {
TODO("download from remote url")
}

private fun AnalyserSpec.copyFrom() {
val sourceJar = Paths.get(host).resolve(jar)
val targetJar = getLocalPath().toFile()

logger.debug("analyser is configured as absolute path, copying to installation path...")
logger.debug("| $sourceJar -> $targetJar |")

sourceJar.toFile().copyTo(targetJar)
}

private fun AnalyserSpec.getFullClassName(): String {
if (className.contains(".")) return className

return "org.archguard.scanner.analyser.$className"
}

private fun AnalyserSpec.getLocalPath(): Path {
return rootPath.resolve(folder).resolve(jar)
}

fun load(context: Context, spec: AnalyserSpec): Analyser<Context> {
// isInstalled
// install
// get with class for name

// support full class name or short class name
return Class.forName("org.archguard.scanner.ctl." + spec.className)
.declaredConstructors[0]
.newInstance(context) as Analyser<Context>
if (!spec.isInstalled()) spec.install()

return spec.run {
val jarUrl = getLocalPath().toUri().toURL()
val cl = URLClassLoader(arrayOf(jarUrl), this.javaClass.classLoader)
Class.forName(spec.getFullClassName(), true, cl)
.declaredConstructors[0]
.newInstance(context) as Analyser<Context>
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.archguard.scanner.ctl.impl

import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test

internal class OfficialAnalyserSpecsTest {
@Test
fun `should output the spec with the default jar for all the official analysers`() {
val specs = OfficialAnalyserSpecs.specs()

assertThat(specs).allMatch {
it.jar == "${it.identifier}-${it.version}-all.jar"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package org.archguard.scanner.ctl.loader

import chapi.domain.core.CodeDataStruct
import io.mockk.clearAllMocks
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkObject
import io.mockk.verify
import org.archguard.scanner.core.Analyser
import org.archguard.scanner.core.AnalyserSpec
import org.archguard.scanner.core.context.Context
import org.archguard.scanner.core.sourcecode.SourceCodeAnalyser
import org.archguard.scanner.core.sourcecode.SourceCodeContext
import org.archguard.scanner.ctl.impl.OfficialAnalyserSpecs
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test

internal class AnalyserDispatcherTest {
private val context = mockk<SourceCodeContext> {
every { language } returns "lang_kotlin"
every { features } returns listOf("feature1", "feature2")
}

@BeforeEach
internal fun setUp() {
mockkObject(AnalyserLoader)
}

@AfterEach
internal fun tearDown() {
clearAllMocks()
}

@Test
fun `should dispatch to the official source code analyser, pass the ast to following feature analyser`() {
val customized = listOf<AnalyserSpec>(
mockk { every { identifier } returns "feature1" },
mockk { every { identifier } returns "feature2" },
)
val languageAnalyser = mockk<SourceCodeAnalyser>()
val feature1Analyser = mockk<SourceCodeAnalyser>()
val feature2Analyser = mockk<SourceCodeAnalyser>()
val ast = mockk<List<CodeDataStruct>>()
every { languageAnalyser.analyse(null) } returns ast
every { feature1Analyser.analyse(ast) } returns null
every { feature2Analyser.analyse(ast) } returns null
every {
AnalyserLoader.load(context, any())
} returns (languageAnalyser as Analyser<Context>) andThen (feature1Analyser as Analyser<Context>) andThen (feature2Analyser as Analyser<Context>)

val dispatcher = AnalyserDispatcher(context, customized)
dispatcher.dispatch()

verify {
AnalyserLoader.load(context, OfficialAnalyserSpecs.LANG_KOTLIN.spec())
}
}

@Test
fun `should dispatch to the customized source code analyser`() {
val customizedLanguageAnalyser = mockk<AnalyserSpec> { every { identifier } returns "lang_kotlin" }
val customized = listOf<AnalyserSpec>(
customizedLanguageAnalyser,
mockk { every { identifier } returns "feature1" },
mockk { every { identifier } returns "feature2" },
)
val languageAnalyser = mockk<SourceCodeAnalyser>()
val feature1Analyser = mockk<SourceCodeAnalyser>()
val feature2Analyser = mockk<SourceCodeAnalyser>()
val ast = mockk<List<CodeDataStruct>>()
every { languageAnalyser.analyse(null) } returns ast
every { feature1Analyser.analyse(ast) } returns null
every { feature2Analyser.analyse(ast) } returns null
every {
AnalyserLoader.load(context, any())
} returns (languageAnalyser as Analyser<Context>) andThen (feature1Analyser as Analyser<Context>) andThen (feature2Analyser as Analyser<Context>)

val dispatcher = AnalyserDispatcher(context, customized)
dispatcher.dispatch()

verify {
AnalyserLoader.load(context, customizedLanguageAnalyser)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package org.archguard.scanner.ctl.loader

import io.mockk.every
import io.mockk.mockk
import org.archguard.scanner.core.AnalyserSpec
import org.archguard.scanner.core.sourcecode.SourceCodeContext
import org.archguard.scanner.ctl.loader.AnalyserLoader.installPath
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.io.File
import kotlin.io.path.Path
import kotlin.io.path.deleteIfExists
import kotlin.reflect.full.memberFunctions

internal class AnalyserLoaderTest {
private val fakeJarName = "testonly-lang_kotlin-1.6.1-all.jar"
private val spec = AnalyserSpec(
identifier = "lang_kotlin",
host = "<unknown>",
version = "1.6.1",
jar = fakeJarName,
className = "KotlinAnalyser",
)
private val context = mockk<SourceCodeContext> {
every { client } returns mockk()
}

private fun fakeInstall(source: File) = source.copyTo(installPath.resolve(spec.jar).toFile(), overwrite = true)
private fun fakeUninstall() = installPath.resolve(spec.jar).deleteIfExists()

@BeforeEach
internal fun setUp() {
fakeUninstall()
}

@Test
fun `should load the analyser from remote jar via http url`() {
// FIXME: implement this after upload the test jar
}

@Test
fun `should load the analyser from local jar via absolute path`() {
val folder = this.javaClass.classLoader.getResource("kotlin")!!
val analyser = AnalyserLoader.load(context, spec.copy(host = folder.path))

val kClass = analyser::class
assertThat(kClass.simpleName).isEqualTo("KotlinAnalyser")
assertThat(kClass.memberFunctions.map { it.name }).contains("analyse")
}

@Test
fun `should load the analyser from existing jar`() {
val folder = this.javaClass.classLoader.getResource("kotlin")!!
fakeInstall(Path(folder.path).resolve(fakeJarName).toFile())
val analyser = AnalyserLoader.load(context, spec)

val kClass = analyser::class
assertThat(kClass.simpleName).isEqualTo("KotlinAnalyser")
assertThat(kClass.memberFunctions.map { it.name }).contains("analyse")
}
}
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,3 @@ data class AnalyserSpec(
val jar: String,
val className: String, // calculate via identifier??
)

0 comments on commit 52faea2

Please sign in to comment.