Skip to content

Commit fc6bd16

Browse files
committed
Add test suite for services and MCP tools
Unit tests (plain JUnit 4, no IDE): - ModuleConfigurationValidationTest — validate() rules and custom validators - ModuleTemplateResolveTest — ${} and {{}} variable resolution in paths/content Platform tests (BasePlatformTestCase, headless IDE): - FtlTemplateServicePlatformTest — FreeMarker rendering, template loading/parsing - TemplateServiceTest — template discovery, getTemplate, createConfiguration - ModuleGeneratorServiceTest — file generation on disk, preview - KmpMcpToolsetTest — listTemplates returns DTOs, generateModule success/errors as McpExpectedError
1 parent cb3ef3d commit fc6bd16

14 files changed

Lines changed: 602 additions & 5 deletions

File tree

build.gradle.kts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,21 +33,18 @@ repositories {
3333
dependencies {
3434
implementation(libs.freemarker)
3535
compileOnly(libs.kotlinx.serialization.json)
36+
testImplementation(libs.junit)
3637

3738
// IntelliJ Platform Gradle Plugin Dependencies Extension - read more: https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-dependencies-extension.html
3839
intellijPlatform {
39-
create(providers.gradleProperty("platformType"), providers.gradleProperty("platformVersion"))
40+
intellijIdea(providers.gradleProperty("platformVersion"))
4041

4142
// Plugin Dependencies. Uses `platformBundledPlugins` property from the gradle.properties file for bundled IntelliJ Platform plugins.
4243
bundledPlugins(providers.gradleProperty("platformBundledPlugins").map { it.split(',') })
4344

4445
// Plugin Dependencies. Uses `platformPlugins` property from the gradle.properties file for plugin from JetBrains Marketplace.
4546
plugins(providers.gradleProperty("platformPlugins").map { it.split(',') })
4647

47-
// Optional dependency: JetBrains MCP Server (available since IDEA 2025.2, build 252)
48-
// Used only for compilation; at runtime loaded only when the plugin is present (optional dependency in plugin.xml)
49-
plugin("com.intellij.mcpServer:252.23892.364")
50-
5148
// Module Dependencies. Uses `platformBundledModules` property from the gradle.properties file for bundled IntelliJ Platform modules.
5249
bundledModules(providers.gradleProperty("platformBundledModules").map { it.split(',') })
5350

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package com.github.nonoxys.kmpmodulegenerator.platform
2+
3+
import com.github.nonoxys.kmpmodulegenerator.services.FtlTemplateService
4+
5+
class FtlTemplateServicePlatformTest : KmpPluginTestCase() {
6+
7+
private lateinit var service: FtlTemplateService
8+
9+
override fun setUp() {
10+
super.setUp()
11+
service = FtlTemplateService.getInstance(project)
12+
}
13+
14+
// region processTemplate
15+
16+
fun `test processTemplate substitutes single variable`() {
17+
val result = service.processTemplate("Hello \${name}!", mapOf("name" to "World"))
18+
assertEquals("Hello World!", result)
19+
}
20+
21+
fun `test processTemplate substitutes multiple variables`() {
22+
val result = service.processTemplate(
23+
"package \${pkg}\n\nclass \${cls} {}",
24+
mapOf("pkg" to "com.example", "cls" to "MyClass")
25+
)
26+
assertEquals("package com.example\n\nclass MyClass {}", result)
27+
}
28+
29+
fun `test processTemplate returns static content unchanged`() {
30+
val result = service.processTemplate("No variables here.", emptyMap())
31+
assertEquals("No variables here.", result)
32+
}
33+
34+
// endregion
35+
36+
// region loadTemplate
37+
38+
fun `test loadTemplate parses minimal-template correctly`() {
39+
val template = service.loadTemplate(fixturesDir().resolve("minimal-template"))!!
40+
41+
assertEquals("minimal-template", template.id)
42+
assertEquals("Minimal Template", template.name)
43+
assertEquals(1, template.variables.size)
44+
assertEquals("moduleName", template.variables[0].name)
45+
assertTrue(template.variables[0].required)
46+
}
47+
48+
fun `test loadTemplate parses full-template with all variable types`() {
49+
val template = service.loadTemplate(fixturesDir().resolve("full-template"))!!
50+
51+
assertEquals("full-template", template.id)
52+
assertEquals(3, template.variables.size)
53+
val dropdown = template.variables.first { it.name == "platform" }
54+
assertEquals(listOf("android", "kmp"), dropdown.options)
55+
}
56+
57+
fun `test loadTemplate detects build gradle path in full-template`() {
58+
val template = service.loadTemplate(fixturesDir().resolve("full-template"))!!
59+
assertTrue(template.modulePaths.any { it.contains("build.gradle.kts") })
60+
}
61+
62+
fun `test loadTemplate returns null when template xml is missing`() {
63+
val emptyDir = tempDir("kmp-no-config")
64+
try {
65+
assertNull(service.loadTemplate(emptyDir))
66+
} finally {
67+
emptyDir.deleteRecursively()
68+
}
69+
}
70+
71+
// endregion
72+
73+
// region getAvailableTemplates
74+
75+
fun `test getAvailableTemplates finds all valid fixture templates`() {
76+
useFixturesAsTemplateFolder()
77+
val ids = service.getAvailableTemplates().map { it.id }
78+
79+
assertTrue(ids.contains("minimal-template"))
80+
assertTrue(ids.contains("full-template"))
81+
}
82+
83+
fun `test getAvailableTemplates skips directory without template xml`() {
84+
useFixturesAsTemplateFolder()
85+
val ids = service.getAvailableTemplates().map { it.id }
86+
87+
assertFalse(ids.contains("no-config-dir"))
88+
}
89+
90+
override fun tearDown() {
91+
resetTemplateFolder()
92+
super.tearDown()
93+
}
94+
95+
// endregion
96+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package com.github.nonoxys.kmpmodulegenerator.platform
2+
3+
import com.github.nonoxys.kmpmodulegenerator.mcp.KmpMcpToolset
4+
import com.github.nonoxys.kmpmodulegenerator.services.TemplateService
5+
import com.github.nonoxys.kmpmodulegenerator.settings.TemplateSettings
6+
import com.intellij.mcpserver.McpExpectedError
7+
import com.intellij.openapi.components.ComponentManagerEx
8+
import kotlinx.coroutines.runBlocking
9+
import java.io.File
10+
import kotlin.io.path.createTempDirectory
11+
12+
class KmpMcpToolsetTest : KmpPluginTestCase() {
13+
14+
private lateinit var toolset: KmpMcpToolset
15+
private lateinit var outputDir: File
16+
17+
override fun setUp() {
18+
super.setUp()
19+
useFixturesAsTemplateFolder()
20+
TemplateService.getInstance(project).reloadTemplates()
21+
toolset = KmpMcpToolset()
22+
outputDir = tempDir("kmp-mcp-test")
23+
}
24+
25+
override fun tearDown() {
26+
outputDir.deleteRecursively()
27+
resetTemplateFolder()
28+
super.tearDown()
29+
}
30+
31+
private fun <T> mcp(block: suspend KmpMcpToolset.() -> T): T {
32+
val scope = (project as ComponentManagerEx).getCoroutineScope()
33+
return runBlocking(scope.coroutineContext) { toolset.block() }
34+
}
35+
36+
private fun mcpError(block: suspend KmpMcpToolset.() -> Unit): McpExpectedError {
37+
try {
38+
mcp(block)
39+
fail("Expected McpExpectedError to be thrown")
40+
} catch (e: McpExpectedError) {
41+
return e
42+
}
43+
error("unreachable")
44+
}
45+
46+
// region listTemplates
47+
48+
fun `test listTemplates returns dto list with fixture templates`() {
49+
val result = mcp { listTemplates() }
50+
val ids = result.map { it.id }
51+
assertTrue(ids.contains("minimal-template"))
52+
assertTrue(ids.contains("full-template"))
53+
}
54+
55+
fun `test listTemplates dto contains variables`() {
56+
val result = mcp { listTemplates() }
57+
val minimal = result.first { it.id == "minimal-template" }
58+
assertEquals(1, minimal.variables.size)
59+
assertEquals("moduleName", minimal.variables[0].name)
60+
assertTrue(minimal.variables[0].required)
61+
}
62+
63+
fun `test listTemplates throws McpExpectedError when no templates`() {
64+
val emptyDir = tempDir("kmp-empty")
65+
try {
66+
TemplateSettings.setCustomTemplateFolder(project, emptyDir)
67+
TemplateService.getInstance(project).reloadTemplates()
68+
mcpError { listTemplates() }
69+
} finally {
70+
emptyDir.deleteRecursively()
71+
useFixturesAsTemplateFolder()
72+
TemplateService.getInstance(project).reloadTemplates()
73+
}
74+
}
75+
76+
// endregion
77+
78+
// region generateModule
79+
80+
fun `test generateModule returns dto with created files on success`() {
81+
val targetDir = File(outputDir, "auth")
82+
val result = mcp { generateModule("minimal-template", targetDir.absolutePath, """{"moduleName":"Auth"}""") }
83+
84+
assertTrue(result.moduleName.isNotBlank())
85+
assertTrue(result.files.isNotEmpty())
86+
assertTrue(result.warnings.isEmpty())
87+
assertTrue(File(targetDir, "Module.kt").exists())
88+
}
89+
90+
fun `test generateModule throws McpExpectedError for unknown template id`() {
91+
val error = mcpError { generateModule("nonexistent", outputDir.absolutePath) }
92+
assertTrue(error.message!!.contains("nonexistent"))
93+
}
94+
95+
fun `test generateModule throws McpExpectedError for invalid variables json`() {
96+
mcpError { generateModule("minimal-template", outputDir.absolutePath, "not-json") }
97+
}
98+
99+
fun `test generateModule throws McpExpectedError when required variable is missing`() {
100+
mcpError { generateModule("minimal-template", outputDir.absolutePath, "{}") }
101+
}
102+
103+
// endregion
104+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.github.nonoxys.kmpmodulegenerator.platform
2+
3+
import com.github.nonoxys.kmpmodulegenerator.settings.TemplateSettings
4+
import com.intellij.testFramework.fixtures.BasePlatformTestCase
5+
import java.io.File
6+
import kotlin.io.path.createTempDirectory
7+
8+
abstract class KmpPluginTestCase : BasePlatformTestCase() {
9+
10+
protected fun fixturesDir(): File {
11+
val url = javaClass.classLoader.getResource("fixtures/templates")
12+
?: error("fixtures/templates not found in test resources")
13+
return File(url.toURI())
14+
}
15+
16+
protected fun tempDir(prefix: String = "kmp-test"): File =
17+
createTempDirectory(prefix).toFile()
18+
19+
protected fun useFixturesAsTemplateFolder() {
20+
TemplateSettings.setCustomTemplateFolder(project, fixturesDir())
21+
}
22+
23+
protected fun resetTemplateFolder() {
24+
TemplateSettings.setCustomTemplateFolder(project, null)
25+
}
26+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package com.github.nonoxys.kmpmodulegenerator.platform
2+
3+
import com.github.nonoxys.kmpmodulegenerator.models.GenerationResult
4+
import com.github.nonoxys.kmpmodulegenerator.services.ModuleGeneratorService
5+
import com.github.nonoxys.kmpmodulegenerator.services.TemplateService
6+
import com.intellij.openapi.application.ApplicationManager
7+
import java.io.File
8+
9+
class ModuleGeneratorServiceTest : KmpPluginTestCase() {
10+
11+
private lateinit var templateService: TemplateService
12+
private lateinit var generatorService: ModuleGeneratorService
13+
private lateinit var outputDir: File
14+
15+
override fun setUp() {
16+
super.setUp()
17+
useFixturesAsTemplateFolder()
18+
templateService = TemplateService.getInstance(project)
19+
templateService.reloadTemplates()
20+
generatorService = ModuleGeneratorService.getInstance(project)
21+
outputDir = tempDir("kmp-gen-test")
22+
}
23+
24+
override fun tearDown() {
25+
outputDir.deleteRecursively()
26+
resetTemplateFolder()
27+
super.tearDown()
28+
}
29+
30+
private fun generate(moduleName: String, targetDir: File = File(outputDir, moduleName)): GenerationResult {
31+
val template = templateService.getTemplate("minimal-template")!!
32+
val config = templateService.createConfiguration(
33+
template, mapOf("moduleName" to moduleName), targetDir.absolutePath
34+
)
35+
var result: GenerationResult? = null
36+
ApplicationManager.getApplication().invokeAndWait { result = generatorService.generateModule(config) }
37+
return result!!
38+
}
39+
40+
// region generateModule
41+
42+
fun `test generateModule returns success for valid input`() {
43+
val result = generate("Auth")
44+
assertTrue(result is GenerationResult.Success || result is GenerationResult.Warning)
45+
}
46+
47+
fun `test generateModule creates files on disk`() {
48+
val targetDir = File(outputDir, "login")
49+
generate("Login", targetDir)
50+
assertTrue(File(targetDir, "Module.kt").exists())
51+
}
52+
53+
fun `test generateModule file content contains module name`() {
54+
val targetDir = File(outputDir, "profile")
55+
generate("Profile", targetDir)
56+
assertTrue(File(targetDir, "Module.kt").readText().contains("Profile"))
57+
}
58+
59+
fun `test generateModule returns failure when required variable is missing`() {
60+
val template = templateService.getTemplate("minimal-template")!!
61+
val config = templateService.createConfiguration(template, emptyMap(), outputDir.absolutePath)
62+
var result: GenerationResult? = null
63+
ApplicationManager.getApplication().invokeAndWait { result = generatorService.generateModule(config) }
64+
assertTrue(result is GenerationResult.Failure)
65+
}
66+
67+
// endregion
68+
69+
// region generatePreview
70+
71+
fun `test generatePreview lists expected output files`() {
72+
val template = templateService.getTemplate("minimal-template")!!
73+
val config = templateService.createConfiguration(
74+
template, mapOf("moduleName" to "Auth"), outputDir.absolutePath
75+
)
76+
val preview = generatorService.generatePreview(config)
77+
78+
assertFalse(preview.files.isEmpty())
79+
assertTrue(preview.files.any { it.path.contains("Module.kt") })
80+
}
81+
82+
// endregion
83+
}

0 commit comments

Comments
 (0)