From 3b4ad222ccc8844ac34d7f515ef08af0357f4cf2 Mon Sep 17 00:00:00 2001 From: tangcent Date: Fri, 3 Jan 2025 08:02:32 +0800 Subject: [PATCH] test: add unit tests for ClassApiExporterHelper (#560) --- .../idea/plugin/api/ClassApiExporterHelper.kt | 57 +++++----- .../plugin/api/export/suv/SuvApiExporter.kt | 34 +++--- .../plugin/api/ClassApiExporterHelperTest.kt | 102 ++++++++++++++++++ .../idea/plugin/api/call/ApiCallerTest.kt | 6 +- ...piCallerTest.ExportFailedApiCallerTest.txt | 2 +- 5 files changed, 150 insertions(+), 51 deletions(-) create mode 100644 idea-plugin/src/test/kotlin/com/itangcent/idea/plugin/api/ClassApiExporterHelperTest.kt diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/ClassApiExporterHelper.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/ClassApiExporterHelper.kt index 6a1eeeb4..37ec563e 100644 --- a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/ClassApiExporterHelper.kt +++ b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/ClassApiExporterHelper.kt @@ -31,25 +31,25 @@ import java.util.concurrent.LinkedBlockingQueue open class ClassApiExporterHelper { @Inject - protected val jvmClassHelper: JvmClassHelper? = null + protected lateinit var jvmClassHelper: JvmClassHelper @Inject - protected val ruleComputer: RuleComputer? = null + protected lateinit var ruleComputer: RuleComputer @Inject - private val docHelper: DocHelper? = null + private lateinit var docHelper: DocHelper @Inject - protected val psiClassHelper: PsiClassHelper? = null + protected lateinit var psiClassHelper: PsiClassHelper @Inject - private val linkExtractor: LinkExtractor? = null + private lateinit var linkExtractor: LinkExtractor @Inject - private val linkResolver: LinkResolver? = null + private lateinit var linkResolver: LinkResolver @Inject - protected val duckTypeHelper: DuckTypeHelper? = null + protected lateinit var duckTypeHelper: DuckTypeHelper @Inject protected lateinit var actionContext: ActionContext @@ -58,7 +58,7 @@ open class ClassApiExporterHelper { protected lateinit var logger: Logger @Inject - protected val classExporter: ClassExporter? = null + protected lateinit var classExporter: ClassExporter @Inject protected lateinit var messagesHelper: MessagesHelper @@ -69,7 +69,7 @@ open class ClassApiExporterHelper { companion object : Log() fun extractParamComment(psiMethod: PsiMethod): MutableMap? { - val subTagMap = docHelper!!.getSubTagMapOfDocComment(psiMethod, "param") + val subTagMap = docHelper.getSubTagMapOfDocComment(psiMethod, "param") if (subTagMap.isEmpty()) { return null } @@ -82,36 +82,34 @@ open class ClassApiExporterHelper { if (value.notNullOrBlank()) { val options: ArrayList> = ArrayList() - val comment = linkExtractor!!.extract(value, psiMethod, object : AbstractLinkResolve() { + val comment = linkExtractor.extract(value, psiMethod, object : AbstractLinkResolve() { override fun linkToPsiElement(plainText: String, linkTo: Any?): String? { - - psiClassHelper!!.resolveEnumOrStatic( + psiClassHelper.resolveEnumOrStatic( plainText, parameters.firstOrNull { it.name == name } ?: psiMethod, name - ) - ?.let { options.addAll(it) } + )?.let { options.addAll(it) } return super.linkToPsiElement(plainText, linkTo) } override fun linkToClass(plainText: String, linkClass: PsiClass): String? { - return linkResolver!!.linkToClass(linkClass) + return linkResolver.linkToClass(linkClass) } override fun linkToType(plainText: String, linkType: PsiType): String? { - return jvmClassHelper!!.resolveClassInType(linkType)?.let { - linkResolver!!.linkToClass(it) + return jvmClassHelper.resolveClassInType(linkType)?.let { + linkResolver.linkToClass(it) } } override fun linkToField(plainText: String, linkField: PsiField): String? { - return linkResolver!!.linkToProperty(linkField) + return linkResolver.linkToProperty(linkField) } override fun linkToMethod(plainText: String, linkMethod: PsiMethod): String? { - return linkResolver!!.linkToMethod(linkMethod) + return linkResolver.linkToMethod(linkMethod) } override fun linkToUnresolved(plainText: String): String { @@ -124,7 +122,6 @@ open class ClassApiExporterHelper { methodParamComment["$name@options"] = options } } - } return methodParamComment @@ -137,7 +134,7 @@ open class ClassApiExporterHelper { cls: PsiClass, handle: (ExplicitMethod) -> Unit, ) { actionContext.runInReadUI { - val methods = duckTypeHelper!!.explicit(cls) + val methods = duckTypeHelper.explicit(cls) .methods() .filter { !shouldIgnore(it) } actionContext.runAsync { @@ -158,28 +155,28 @@ open class ClassApiExporterHelper { } protected open fun shouldIgnore(explicitElement: ExplicitMethod): Boolean { - if (ignoreIrregularApiMethod() && (jvmClassHelper!!.isBasicMethod(explicitElement.psi().name) + if (ignoreIrregularApiMethod() && (jvmClassHelper.isBasicMethod(explicitElement.psi().name) || explicitElement.psi().hasModifierProperty("static") || explicitElement.psi().isConstructor) ) { return true } - return ruleComputer!!.computer(ClassExportRuleKeys.IGNORE, explicitElement) ?: false + return ruleComputer.computer(ClassExportRuleKeys.IGNORE, explicitElement) ?: false } protected open fun shouldIgnore(psiMethod: PsiMethod): Boolean { - if (ignoreIrregularApiMethod() && (jvmClassHelper!!.isBasicMethod(psiMethod.name) + if (ignoreIrregularApiMethod() && (jvmClassHelper.isBasicMethod(psiMethod.name) || psiMethod.hasModifierProperty("static") || psiMethod.isConstructor) ) { return true } - return ruleComputer!!.computer(ClassExportRuleKeys.IGNORE, psiMethod) ?: false + return ruleComputer.computer(ClassExportRuleKeys.IGNORE, psiMethod) ?: false } fun foreachPsiMethod(cls: PsiClass, handle: (PsiMethod) -> Unit) { actionContext.runInReadUI { - jvmClassHelper!!.getAllMethods(cls) + jvmClassHelper.getAllMethods(cls) .asSequence() .filter { !shouldIgnore(it) } .forEach(handle) @@ -193,7 +190,7 @@ open class ClassApiExporterHelper { } fun export(handle: (Doc) -> Unit) { - logger.info("Start export api...") + logger.info("Starting API export process...") val psiClassQueue: BlockingQueue = LinkedBlockingQueue() val boundary = actionContext.createBoundary() @@ -238,11 +235,11 @@ open class ClassApiExporterHelper { } } else { val classQualifiedName = actionContext.callInReadUI { psiClass.qualifiedName } - LOG.info("wait api parsing... $classQualifiedName") + LOG.info("Processing API for class: $classQualifiedName") actionContext.withBoundary { - classExporter!!.export(psiClass) { handle(it) } + classExporter.export(psiClass) { handle(it) } } - LOG.info("api parse $classQualifiedName completed.") + LOG.info("Successfully parsed API for class: $classQualifiedName") } } } diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/suv/SuvApiExporter.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/suv/SuvApiExporter.kt index dd1e522d..d7c91acd 100644 --- a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/suv/SuvApiExporter.kt +++ b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/suv/SuvApiExporter.kt @@ -75,7 +75,7 @@ open class SuvApiExporter { val docs = classApiExporterHelper.export().map { DocWrapper(it) } if (docs.isEmpty()) { - logger.info("No api be found!") + logger.info("No API found in the selected files") return } @@ -153,11 +153,11 @@ open class SuvApiExporter { abstract class ApiExporterAdapter { - @Inject(optional = true) - protected var logger: Logger? = null + @Inject + protected lateinit var logger: Logger @Inject - protected val classExporter: ClassExporter? = null + protected lateinit var classExporter: ClassExporter @Inject protected lateinit var actionContext: ActionContext @@ -202,13 +202,13 @@ open class SuvApiExporter { try { doExportApisFromMethod(requests) } catch (e: Exception) { - logger!!.error("error to export apis:" + e.message) + logger!!.error("Failed to export APIs: " + e.message) logger!!.traceError(e) } } } } catch (e: Throwable) { - logger!!.error("error to export apis:" + e.message) + logger!!.error("Failed to export APIs: " + e.message) logger!!.traceError(e) } } @@ -295,7 +295,7 @@ open class SuvApiExporter { } if (docs.isEmpty()) { - logger!!.info("no api has be found") + logger!!.info("No APIs found") } doExportDocs(docs) @@ -341,10 +341,10 @@ open class SuvApiExporter { private lateinit var postmanSettingsHelper: PostmanSettingsHelper @Inject - private val fileSaveHelper: FileSaveHelper? = null + private lateinit var fileSaveHelper: FileSaveHelper @Inject - private val postmanFormatter: PostmanFormatter? = null + private lateinit var postmanFormatter: PostmanFormatter @Inject private lateinit var project: Project @@ -388,10 +388,10 @@ open class SuvApiExporter { class MarkdownApiExporterAdapter : ApiExporterAdapter() { @Inject - private val fileSaveHelper: FileSaveHelper? = null + private lateinit var fileSaveHelper: FileSaveHelper @Inject - private val markdownFormatter: MarkdownFormatter? = null + private lateinit var markdownFormatter: MarkdownFormatter @Inject private lateinit var markdownSettingsHelper: MarkdownSettingsHelper @@ -423,15 +423,15 @@ open class SuvApiExporter { override fun doExportDocs(docs: MutableList) { try { if (docs.isEmpty()) { - logger!!.info("No api be found to export!") + logger!!.info("No API found in the selected scope") return } logger!!.info("Start parse apis") - val apiInfo = markdownFormatter!!.parseRequests(docs) + val apiInfo = markdownFormatter.parseRequests(docs) docs.clear() actionContext.runAsync { try { - fileSaveHelper!!.saveOrCopy(apiInfo, markdownSettingsHelper.outputCharset(), { + fileSaveHelper.saveOrCopy(apiInfo, markdownSettingsHelper.outputCharset(), { logger!!.info("Exported data are copied to clipboard,you can paste to a md file now") }, { logger!!.info("Apis save success: $it") @@ -482,7 +482,7 @@ open class SuvApiExporter { val requests = docs.filterAs(Request::class) try { if (docs.isEmpty()) { - logger!!.info("No api be found to export!") + logger!!.info("No API found in the selected scope") return } curlExporter.export(requests) @@ -524,7 +524,7 @@ open class SuvApiExporter { val requests = docs.filterAs(Request::class) try { if (docs.isEmpty()) { - logger!!.info("No api be found to export!") + logger!!.info("No API found in the selected scope") return } httpClientExporter.export(requests) @@ -536,7 +536,7 @@ open class SuvApiExporter { private fun doExport(channel: ApiExporterWrapper, requests: List) { if (requests.isEmpty()) { - logger.info("no api has be selected") + logger.info("No API found in the selected scope") return } val adapter = channel.adapter.createInstance() as ApiExporterAdapter diff --git a/idea-plugin/src/test/kotlin/com/itangcent/idea/plugin/api/ClassApiExporterHelperTest.kt b/idea-plugin/src/test/kotlin/com/itangcent/idea/plugin/api/ClassApiExporterHelperTest.kt new file mode 100644 index 00000000..bd7cbfd9 --- /dev/null +++ b/idea-plugin/src/test/kotlin/com/itangcent/idea/plugin/api/ClassApiExporterHelperTest.kt @@ -0,0 +1,102 @@ +package com.itangcent.idea.plugin.api + +import com.google.inject.Inject +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiClassOwner +import com.intellij.psi.PsiFile +import com.itangcent.common.model.Request +import com.itangcent.idea.plugin.api.export.core.ClassExporter +import com.itangcent.idea.plugin.api.export.spring.SpringRequestClassExporter +import com.itangcent.intellij.context.ActionContext +import com.itangcent.intellij.extend.guice.singleton +import com.itangcent.intellij.extend.guice.with +import com.itangcent.test.workAt +import com.itangcent.testFramework.PluginContextLightCodeInsightFixtureTestCase +import org.junit.jupiter.api.Assertions.assertInstanceOf +import java.time.LocalDate +import java.time.LocalDateTime +import java.util.* + +/** + * Test case for [ClassApiExporterHelper] + */ +class ClassApiExporterHelperTest : PluginContextLightCodeInsightFixtureTestCase() { + + @Inject + private lateinit var classApiExporterHelper: ClassApiExporterHelper + + private lateinit var userCtrlPsiClass: PsiClass + private lateinit var userCtrlPsiFile: PsiFile + + + override fun beforeBind() { + super.beforeBind() + loadSource(Object::class) + loadSource(java.lang.Boolean::class) + loadSource(java.lang.String::class) + loadSource(java.lang.Integer::class) + loadSource(java.lang.Long::class) + loadSource(Collection::class) + loadSource(Map::class) + loadSource(List::class) + loadSource(LinkedList::class) + loadSource(LocalDate::class) + loadSource(LocalDateTime::class) + loadSource(HashMap::class) + loadFile("spring/GetMapping.java") + loadFile("spring/PutMapping.java") + loadFile("spring/ModelAttribute.java") + loadFile("spring/PostMapping.java") + loadFile("spring/RequestBody.java") + loadFile("spring/RequestMapping.java") + loadFile("spring/RequestHeader.java") + loadFile("spring/RequestParam.java") + loadFile("spring/RestController.java") + userCtrlPsiFile = loadFile("api/UserCtrl.java")!! + userCtrlPsiClass = (userCtrlPsiFile as? PsiClassOwner)?.classes?.firstOrNull()!! + } + + override fun bind(builder: ActionContext.ActionContextBuilder) { + super.bind(builder) + builder.bind(ClassExporter::class) { it.with(SpringRequestClassExporter::class).singleton() } + builder.workAt(userCtrlPsiFile) + } + + fun testExtractParamComment() { + val method = userCtrlPsiClass.methods.first { it.name == "get" } + val comments = classApiExporterHelper.extractParamComment(method) + + assertNotNull(comments) + assertTrue(comments!!.containsKey("id")) + assertEquals("user id", comments["id"]) + } + + fun testForeachMethod() { + val methods = mutableListOf() + classApiExporterHelper.foreachMethod(userCtrlPsiClass) { method -> + methods.add(method.name()) + } + actionContext.waitComplete() + + assertTrue(methods.contains("create")) + assertTrue(methods.contains("get")) + assertFalse(methods.contains("toString")) + } + + fun testExport() { + val docs = classApiExporterHelper.export() + actionContext.waitComplete() + + assertNotNull(docs) + assertTrue(docs.isNotEmpty()) + + // Verify first API doc + docs[0].let { doc -> + assertInstanceOf(Request::class.java, doc) + doc as Request + assertEquals("say hello", doc.name) + assertEquals("GET", doc.method) + assertEquals("user/greeting", doc.path.toString()) + } + } +} \ No newline at end of file diff --git a/idea-plugin/src/test/kotlin/com/itangcent/idea/plugin/api/call/ApiCallerTest.kt b/idea-plugin/src/test/kotlin/com/itangcent/idea/plugin/api/call/ApiCallerTest.kt index 97c6e23c..02d67d10 100644 --- a/idea-plugin/src/test/kotlin/com/itangcent/idea/plugin/api/call/ApiCallerTest.kt +++ b/idea-plugin/src/test/kotlin/com/itangcent/idea/plugin/api/call/ApiCallerTest.kt @@ -77,7 +77,7 @@ internal abstract class ApiCallerTest : PluginContextLightCodeInsightFixtureTest apiCaller.showCallWindow() actionContext.waitComplete() assertEquals( - "[INFO]\tStart export api...\n" + + "[INFO]\tStarting API export process...\n" + "[INFO]\tNo api be found to call!\n", LoggerCollector.getLog().toUnixString() ) @@ -119,7 +119,7 @@ internal abstract class ApiCallerTest : PluginContextLightCodeInsightFixtureTest apiCaller.showCallWindow() actionContext.waitComplete() assertEquals( - "[INFO]\tStart export api...\n", + "[INFO]\tStarting API export process...\n", LoggerCollector.getLog().replace(Regex("\\d"), "").toUnixString() ) assertEquals(requests, requestListInUI) @@ -147,7 +147,7 @@ internal abstract class ApiCallerTest : PluginContextLightCodeInsightFixtureTest actionContext.instance(ApiCaller::class).showCallWindow() actionContext.waitComplete() assertEquals( - "[INFO]\tStart export api...\n", + "[INFO]\tStarting API export process...\n", LoggerCollector.getLog().replace(Regex("\\d"), "").toUnixString() ) assertEquals(requests, requestListInUI) diff --git a/idea-plugin/src/test/resources/result/com.itangcent.idea.plugin.api.call.ApiCallerTest.ExportFailedApiCallerTest.txt b/idea-plugin/src/test/resources/result/com.itangcent.idea.plugin.api.call.ApiCallerTest.ExportFailedApiCallerTest.txt index c719c626..c13a278c 100644 --- a/idea-plugin/src/test/resources/result/com.itangcent.idea.plugin.api.call.ApiCallerTest.ExportFailedApiCallerTest.txt +++ b/idea-plugin/src/test/resources/result/com.itangcent.idea.plugin.api.call.ApiCallerTest.ExportFailedApiCallerTest.txt @@ -1,4 +1,4 @@ -[INFO] Start export api... +[INFO] Starting API export process... [ERROR] Apis exported failed [TRACE] java.lang.RuntimeException: export time out