diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index deb5837f..b5038880 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,6 +17,9 @@ clikt = "4.2.1" chocolate-factory="0.4.3" +chapi = "2.1.3" +archguard="2.0.7" + [plugins] kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } @@ -46,6 +49,18 @@ test-kotlintest-assertions = { module = "io.kotest:kotest-assertions-core", vers test-mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" } test-assertj = { group = "org.assertj", name = "assertj-core", version.ref = "assertj" } +# chapi +chapi-domain = { group = "com.phodal.chapi", name = "chapi-domain", version.ref = "chapi" } +chapi-java = { group = "com.phodal.chapi", name = "chapi-ast-java", version.ref = "chapi" } +chapi-kotlin = { group = "com.phodal.chapi", name = "chapi-ast-kotlin", version.ref = "chapi" } + + +# ArchGurad +archguard-scanner-core = { group = "org.archguard.scanner", name = "scanner_core", version.ref = "archguard" } +archguard-lang-kotlin = { group = "org.archguard.scanner", name = "lang_kotlin", version.ref = "archguard" } +archguard-rule-core = { group = "org.archguard.scanner", name = "rule_core", version.ref = "archguard" } +archguard-analyser-estimate = { group = "org.archguard.scanner", name = "analyser_estimate", version.ref = "archguard" } + # cf => chocolate-factory cf-language = { group = "cc.unitmesh", name = "code-language", version.ref = "chocolate-factory" } diff --git a/unit-picker/build.gradle.kts b/unit-picker/build.gradle.kts index 90c8845a..d45b84c3 100644 --- a/unit-picker/build.gradle.kts +++ b/unit-picker/build.gradle.kts @@ -14,6 +14,12 @@ dependencies { implementation(libs.clikt) implementation(libs.serialization.json) + implementation(libs.chapi.domain) + implementation(libs.chapi.java) + implementation(libs.chapi.kotlin) + + implementation(libs.archguard.analyser.estimate) + // Logging implementation(libs.logging.slf4j.api) implementation(libs.logging.logback.classic) diff --git a/unit-picker/src/main/kotlin/org/unimesh/eval/picker/bs/BadsmellChecker.kt b/unit-picker/src/main/kotlin/org/unimesh/eval/picker/bs/BadsmellChecker.kt new file mode 100644 index 00000000..75499550 --- /dev/null +++ b/unit-picker/src/main/kotlin/org/unimesh/eval/picker/bs/BadsmellChecker.kt @@ -0,0 +1,9 @@ +package org.unimesh.eval.picker.bs + +import chapi.domain.core.CodeDataStruct + +class BadsmellChecker(val data: List) { + fun check() { + + } +} \ No newline at end of file diff --git a/unit-picker/src/main/kotlin/org/unimesh/eval/picker/bs/TestBadsmellAnalyser.kt b/unit-picker/src/main/kotlin/org/unimesh/eval/picker/bs/TestBadsmellAnalyser.kt new file mode 100644 index 00000000..863bb4cc --- /dev/null +++ b/unit-picker/src/main/kotlin/org/unimesh/eval/picker/bs/TestBadsmellAnalyser.kt @@ -0,0 +1,237 @@ +package org.unimesh.eval.picker.bs + +import chapi.domain.core.CodeAnnotation +import chapi.domain.core.CodeCall +import chapi.domain.core.CodeDataStruct +import chapi.domain.core.CodeFunction +import kotlinx.serialization.Serializable + +@Serializable +data class TestBadSmell( + var FileName: String = "", + var Type: String = "", + var Description: String = "", + var Line: Int = 0 +) +data class TbsResult(var results: Array) + +class TbsAnalyser(val nodes: List) { + fun analysisByPath(path: String): Array { + val tbsResult: TbsResult = TbsResult(arrayOf()) + val callMethodMap = buildCallMethodMap(nodes) + + for (node in nodes) { + for (method in node.Functions) { + if (!method.isJUnitTest()) { + continue + } + + val currentMethodCalls = addExtractAssertMethodCall(method, node, callMethodMap) + method.FunctionCalls = currentMethodCalls + + for (annotation in method.Annotations) { + checkIgnoreTest(node.FilePath, annotation, tbsResult, method) + checkEmptyTest(node.FilePath, annotation, tbsResult, method) + } + + val methodCallMap = hashMapOf>() + var hasAssert = false + + for ((index, funcCall) in currentMethodCalls.withIndex()) { + if (funcCall.FunctionName == "") { + val lastFuncCall = index == currentMethodCalls.size - 1 + if (lastFuncCall && !hasAssert) { + appendUnknownTest(node.FilePath, method, tbsResult) + } + continue + } + + updateMethodCallMap(funcCall, methodCallMap) + + checkRedundantPrintTest(node.FilePath, funcCall, tbsResult) + checkSleepyTest(node.FilePath, funcCall, tbsResult) + checkRedundantAssertionTest(node.FilePath, funcCall, tbsResult) + + if (funcCall.hasAssertion()) hasAssert = true + + val lastFuncCall = index == currentMethodCalls.size - 1 + if (lastFuncCall && !hasAssert) { + appendUnknownTest(node.FilePath, method, tbsResult) + } + } + + checkDuplicateAssertTest(node, method, methodCallMap, tbsResult) + } + } + + return tbsResult.results + } + + private fun addExtractAssertMethodCall( + method: CodeFunction, + node: CodeDataStruct, + callMethodMap: MutableMap + ): List { + var methodCalls = method.FunctionCalls + for (methodCall in methodCalls) { + if (methodCall.NodeName == node.NodeName) { + val mapFunc = callMethodMap[methodCall.buildFullMethodName()] + if (mapFunc != null && mapFunc.Name != "") { + methodCalls += mapFunc.FunctionCalls + } + } + } + + return methodCalls + } + + private fun updateMethodCallMap( + funcCall: CodeCall, + methodCallMap: HashMap> + ) { + var calls: Array = arrayOf() + val buildFullMethodName = funcCall.buildFullMethodName() + if (methodCallMap[buildFullMethodName] != null) { + calls = methodCallMap[buildFullMethodName]!! + } + calls += funcCall + methodCallMap[buildFullMethodName] = calls + } + + private fun checkDuplicateAssertTest( + node: CodeDataStruct, + method: CodeFunction, + methodCallMap: MutableMap>, + tbsResult: TbsResult + ) { + var isDuplicateTest = false + for (entry in methodCallMap) { + val methodCalls = entry.value + val duplicatedLimitLength = 5 + if (methodCalls.size >= duplicatedLimitLength) { + if (methodCalls.last().hasAssertion()) { + isDuplicateTest = true + } + } + } + + if (isDuplicateTest) { + val testBadSmell = TestBadSmell( + FileName = node.FilePath, + Type = "DuplicateAssertTest", + Description = "", + Line = method.Position.StartLine + ) + + tbsResult.results += testBadSmell + } + } + + private fun appendUnknownTest(filePath: String, method: CodeFunction, tbsResult: TbsResult) { + val testBadSmell = TestBadSmell( + FileName = filePath, + Type = "UnknownTest", + Description = "", + Line = method.Position.StartLine + ) + + tbsResult.results += testBadSmell + } + + private fun checkRedundantAssertionTest( + filePath: String, + funcCall: CodeCall, + tbsResult: TbsResult + ) { + val assertParametersSize = 2 + if (funcCall.Parameters.size == assertParametersSize) { + if (funcCall.Parameters[0].TypeValue == funcCall.Parameters[1].TypeValue) { + val testBadSmell = TestBadSmell( + FileName = filePath, + Type = "RedundantAssertionTest", + Description = "", + Line = funcCall.Position.StartLine + ) + + tbsResult.results += testBadSmell + } + } + } + + private fun checkSleepyTest(filePath: String, funcCall: CodeCall, tbsResult: TbsResult) { + if (funcCall.isThreadSleep()) { + val testBadSmell = TestBadSmell( + FileName = filePath, + Type = "SleepyTest", + Description = "", + Line = funcCall.Position.StartLine + ) + + tbsResult.results += testBadSmell + } + } + + private fun checkRedundantPrintTest(filePath: String, funcCall: CodeCall, tbsResult: TbsResult) { + if (funcCall.isSystemOutput()) { + val testBadSmell = TestBadSmell( + FileName = filePath, + Type = "RedundantPrintTest", + Description = "", + Line = funcCall.Position.StartLine + ) + + tbsResult.results += testBadSmell + } + } + + private fun checkIgnoreTest( + filePath: String, + annotation: CodeAnnotation, + tbsResult: TbsResult, + method: CodeFunction + ) { + if (annotation.isIgnore()) { + val testBadSmell = TestBadSmell( + FileName = filePath, + Type = "IgnoreTest", + Description = "", + Line = method.Position.StartLine + ) + + tbsResult.results += testBadSmell + } + } + + private fun checkEmptyTest( + filePath: String, + annotation: CodeAnnotation, + tbsResult: TbsResult, + method: CodeFunction + ) { + val isJavaTest = filePath.endsWith(".java") && annotation.isTest() + val isGoTest = filePath.endsWith("_test.go") + if (isJavaTest || isGoTest) { + if (method.FunctionCalls.size <= 1) { + val badSmell = TestBadSmell( + FileName = filePath, + Type = "EmptyTest", + Description = "", + Line = method.Position.StartLine + ) + + tbsResult.results += badSmell + } + } + } + + private fun buildCallMethodMap(nodes: List): MutableMap { + val callMethodMap: MutableMap = mutableMapOf() + for (node in nodes) { + for (method in node.Functions) { + callMethodMap[method.buildFullMethodName(node)] = method + } + } + + return callMethodMap + } +} \ No newline at end of file