From c407de70a56732ae0b7b80906f8b77aec0aee960 Mon Sep 17 00:00:00 2001 From: tangcent Date: Sun, 15 Dec 2024 20:55:27 +0800 Subject: [PATCH] test: add unit tests for ClassApiExporterHelper --- .../idea/plugin/api/ClassApiExporterHelper.kt | 57 +++++----- .../plugin/api/export/suv/SuvApiExporter.kt | 40 +++---- .../plugin/api/ClassApiExporterHelperTest.kt | 102 ++++++++++++++++++ 3 files changed, 149 insertions(+), 50 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 6a1eeeb4e..37ec563e9 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 4b7d21d1b..06165d3c4 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 @@ -77,7 +77,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,7 +388,7 @@ open class SuvApiExporter { class YapiApiExporterAdapter : ApiExporterAdapter() { @Inject - protected val yapiSettingsHelper: YapiSettingsHelper? = null + protected lateinit var yapiSettingsHelper: YapiSettingsHelper override fun actionName(): String { return "YapiExportAction" @@ -429,7 +429,7 @@ open class SuvApiExporter { } override fun beforeExport(next: () -> Unit) { - val serverFound = yapiSettingsHelper!!.getServer(false).notNullOrBlank() + val serverFound = yapiSettingsHelper.getServer(false).notNullOrBlank() if (serverFound) { next() } @@ -441,7 +441,7 @@ open class SuvApiExporter { try { docs.forEach { suvYapiApiExporter.exportDoc(it) } } catch (e: Exception) { - logger!!.error("Apis export failed") + logger!!.error("Failed to export APIs to YAPI") logger!!.traceError(e) } } @@ -490,10 +490,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 @@ -525,15 +525,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") @@ -584,7 +584,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) @@ -626,7 +626,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) @@ -638,7 +638,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 000000000..8cae9d350 --- /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.intellij.testFramework.assertInstanceOf +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 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(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