mirror of
https://github.com/bitsycore/bitsykonfig-gradle-plugin.git
synced 2026-10-07 20:47:27 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc61526931 | ||
|
|
73b8e449d5 | ||
|
|
9a45658442 | ||
|
|
9e66a86870 |
+5
-1
@@ -3,4 +3,8 @@
|
|||||||
.kotlin
|
.kotlin
|
||||||
.gradle
|
.gradle
|
||||||
local.properties
|
local.properties
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
# Local credential overrides — never commit tokens
|
||||||
|
gradle.properties.local
|
||||||
|
secrets.properties
|
||||||
+56
-4
@@ -48,7 +48,7 @@ dependencies {
|
|||||||
compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin:$embeddedKotlinVersion")
|
compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin:$embeddedKotlinVersion")
|
||||||
compileOnly("com.android.tools.build:gradle:8.0.0")
|
compileOnly("com.android.tools.build:gradle:8.0.0")
|
||||||
testImplementation(kotlin("test"))
|
testImplementation(kotlin("test"))
|
||||||
"functionalTestImplementation"(gradleTestKit())
|
add("functionalTestImplementation", gradleTestKit())
|
||||||
}
|
}
|
||||||
|
|
||||||
val functionalTestTask = tasks.register<Test>("functionalTest") {
|
val functionalTestTask = tasks.register<Test>("functionalTest") {
|
||||||
@@ -60,8 +60,60 @@ val functionalTestTask = tasks.register<Test>("functionalTest") {
|
|||||||
|
|
||||||
tasks.check { dependsOn(functionalTestTask) }
|
tasks.check { dependsOn(functionalTestTask) }
|
||||||
|
|
||||||
|
// ========================================================
|
||||||
|
// MARK: Publishing
|
||||||
|
// ========================================================
|
||||||
|
|
||||||
|
fun prop(name: String): String? =
|
||||||
|
providers.gradleProperty(name).orNull
|
||||||
|
?: System.getenv(name.replace('.', '_').uppercase())
|
||||||
|
|
||||||
publishing {
|
publishing {
|
||||||
repositories {
|
publications {
|
||||||
mavenLocal()
|
create<MavenPublication>("pluginMaven") {
|
||||||
}
|
groupId = project.group.toString()
|
||||||
|
artifactId = providers.gradleProperty("konfig.artifactId").get()
|
||||||
|
version = project.version.toString()
|
||||||
|
|
||||||
|
pom {
|
||||||
|
name = providers.gradleProperty("konfig.pom.name").get()
|
||||||
|
description = providers.gradleProperty("konfig.pom.description").get()
|
||||||
|
url = providers.gradleProperty("konfig.pom.url").get()
|
||||||
|
|
||||||
|
licenses {
|
||||||
|
license {
|
||||||
|
name = "MIT License"
|
||||||
|
url = "https://opensource.org/licenses/MIT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
developers {
|
||||||
|
developer {
|
||||||
|
id = providers.gradleProperty("konfig.pom.developer.id").get()
|
||||||
|
name = providers.gradleProperty("konfig.pom.developer.name").get()
|
||||||
|
url = providers.gradleProperty("konfig.pom.developer.url").get()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scm {
|
||||||
|
connection = providers.gradleProperty("konfig.pom.scm.connection").get()
|
||||||
|
developerConnection = providers.gradleProperty("konfig.pom.scm.developerConnection").get()
|
||||||
|
url = providers.gradleProperty("konfig.pom.scm.url").get()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenLocal()
|
||||||
|
|
||||||
|
maven {
|
||||||
|
name = "GitHubPackages"
|
||||||
|
url = uri(providers.gradleProperty("konfig.publish.url").get())
|
||||||
|
credentials {
|
||||||
|
username = prop("gpr.user")
|
||||||
|
password = prop("gpr.key")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+32
-1
@@ -3,4 +3,35 @@ org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8
|
|||||||
org.gradle.caching=true
|
org.gradle.caching=true
|
||||||
org.gradle.configuration-cache=true
|
org.gradle.configuration-cache=true
|
||||||
|
|
||||||
konfig.version=0.2.0
|
# =========================================================
|
||||||
|
# MARK: Publishing
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
konfig.version=0.5.0
|
||||||
|
konfig.artifactId=konfig-gradle-plugin
|
||||||
|
konfig.publish.url=https://maven.pkg.github.com/bitsycore/bitsykonfig-gradle-plugin
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# MARK: POM
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
konfig.pom.name=BuildKonfig Gradle Plugin
|
||||||
|
konfig.pom.description=Generates a BuildKonfig Kotlin object at build time - like Android BuildConfig, but for any Kotlin project (JVM, Multiplatform, Android).
|
||||||
|
konfig.pom.url=https://github.com/bitsycore/bitsykonfig-gradle-plugin
|
||||||
|
|
||||||
|
konfig.pom.developer.id=bitsycore
|
||||||
|
konfig.pom.developer.name=bitsycore
|
||||||
|
konfig.pom.developer.url=https://github.com/bitsycore
|
||||||
|
|
||||||
|
konfig.pom.scm.connection=scm:git:git://github.com/bitsycore/bitsykonfig-gradle-plugin.git
|
||||||
|
konfig.pom.scm.developerConnection=scm:git:ssh://github.com/bitsycore/bitsykonfig-gradle-plugin.git
|
||||||
|
konfig.pom.scm.url=https://github.com/bitsycore/bitsykonfig-gradle-plugin
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# MARK: Credentials
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
# (keep in ~/.gradle/gradle.properties)
|
||||||
|
|
||||||
|
# gpr.user=YOUR_GITHUB_USERNAME
|
||||||
|
# gpr.key=YOUR_GITHUB_PERSONAL_ACCESS_TOKEN
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
package com.bitsycore.konfig
|
||||||
|
|
||||||
|
import org.gradle.testkit.runner.TaskOutcome
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Functional tests for the basic file-generation behaviour of the plugin:
|
||||||
|
* package derivation, object name, BUILD_TYPE, IS_DEBUG, MODULE_NAME,
|
||||||
|
* and the required file header.
|
||||||
|
*/
|
||||||
|
class BasicGenerationFunctionalTest : FunctionalTestBase() {
|
||||||
|
|
||||||
|
@Test fun `generateKonfig task succeeds`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
""")
|
||||||
|
val result = run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
assertEquals(TaskOutcome.SUCCESS, result.task(":generateKonfig")?.outcome)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `package is derived from group and project name`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
assertTrue(dir.generatedFile().readText().contains("package com.example.test.project"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `default object name is BuildKonfig`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
assertTrue(dir.generatedFile().readText().contains("object BuildKonfig {"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `RELEASE build sets BUILD_TYPE to release`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
assertTrue(dir.generatedFile().readText().contains("""const val BUILD_TYPE: String = "release""""))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `DEBUG build sets BUILD_TYPE to debug`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=DEBUG"))
|
||||||
|
assertTrue(dir.generatedFile().readText().contains("""const val BUILD_TYPE: String = "debug""""))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `RELEASE build emits const IS_DEBUG = false`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
assertTrue(dir.generatedFile().readText().contains("const val IS_DEBUG: Boolean = false"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `DEBUG build emits inline IS_DEBUG getter`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=DEBUG"))
|
||||||
|
assertTrue(dir.generatedFile().readText().contains("inline val IS_DEBUG: Boolean get() = true"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `MODULE_NAME const is present`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
assertTrue(dir.generatedFile().readText().contains("""const val MODULE_NAME: String = """))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `generated file contains RedundantVisibilityModifier suppress header`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
assertTrue(dir.generatedFile().readText().contains("""@file:Suppress("RedundantVisibilityModifier")"""))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `generated file contains DO NOT EDIT comment`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
assertTrue(dir.generatedFile().readText().contains("DO NOT EDIT"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `no konfig block still generates file`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
""")
|
||||||
|
val result = run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
assertEquals(TaskOutcome.SUCCESS, result.task(":generateKonfig")?.outcome)
|
||||||
|
assertTrue(dir.generatedFile().readText().contains("object BuildKonfig"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
package com.bitsycore.konfig
|
||||||
|
|
||||||
|
import org.gradle.testkit.runner.TaskOutcome
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertNotEquals
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Functional tests for Gradle task caching and incremental build behaviour:
|
||||||
|
* UP-TO-DATE on unchanged inputs, re-execution on changed inputs,
|
||||||
|
* and the `-Pkonfig.force` bypass.
|
||||||
|
*/
|
||||||
|
class CachingFunctionalTest : FunctionalTestBase() {
|
||||||
|
|
||||||
|
// ── UP-TO-DATE on unchanged inputs ────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `task is UP-TO-DATE on second run with unchanged inputs`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig { field("FOO", "bar") }
|
||||||
|
""")
|
||||||
|
val args = listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE")
|
||||||
|
run(args)
|
||||||
|
assertEquals(TaskOutcome.UP_TO_DATE, run(args).task(":generateKonfig")?.outcome)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `task is UP-TO-DATE for empty konfig block`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {}
|
||||||
|
""")
|
||||||
|
val args = listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE")
|
||||||
|
run(args)
|
||||||
|
assertEquals(TaskOutcome.UP_TO_DATE, run(args).task(":generateKonfig")?.outcome)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Re-executes when inputs change ────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `task re-runs after build type changes`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
val second = run(listOf("generateKonfig", "-Pkonfig.buildtype=DEBUG"))
|
||||||
|
assertNotEquals(TaskOutcome.UP_TO_DATE, second.task(":generateKonfig")?.outcome)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `task re-runs after field value changes`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig { field("FOO", "first") }
|
||||||
|
""")
|
||||||
|
val args = listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE")
|
||||||
|
run(args)
|
||||||
|
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig { field("FOO", "second") }
|
||||||
|
""")
|
||||||
|
assertNotEquals(TaskOutcome.UP_TO_DATE, run(args).task(":generateKonfig")?.outcome)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `task re-runs after dimension variant property changes`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
dimension("env") {
|
||||||
|
variant("prod") { field("URL", "https://prod.example.com") }
|
||||||
|
variant("dev") { field("URL", "https://dev.example.com") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE", "-Pkonfig.dimension.env=prod"))
|
||||||
|
val second = run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE", "-Pkonfig.dimension.env=dev"))
|
||||||
|
assertNotEquals(TaskOutcome.UP_TO_DATE, second.task(":generateKonfig")?.outcome)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `task re-runs after konfig_properties file changes`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
dimension("env") {
|
||||||
|
variant("prod") { field("URL", "https://prod.example.com") }
|
||||||
|
variant("dev") { field("URL", "https://dev.example.com") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
val propsFile = dir.resolve("konfig.properties")
|
||||||
|
propsFile.writeText("konfig.dimension.env=dev\n")
|
||||||
|
val args = listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE")
|
||||||
|
run(args)
|
||||||
|
|
||||||
|
propsFile.writeText("konfig.dimension.env=prod\n")
|
||||||
|
assertNotEquals(TaskOutcome.UP_TO_DATE, run(args).task(":generateKonfig")?.outcome)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── -Pkonfig.force ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `konfig force prevents UP-TO-DATE on second run`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
""")
|
||||||
|
val args = listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE", "-Pkonfig.force")
|
||||||
|
run(args)
|
||||||
|
assertNotEquals(TaskOutcome.UP_TO_DATE, run(args).task(":generateKonfig")?.outcome)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `without konfig force task stays UP-TO-DATE`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
""")
|
||||||
|
val args = listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE")
|
||||||
|
run(args)
|
||||||
|
assertEquals(TaskOutcome.UP_TO_DATE, run(args).task(":generateKonfig")?.outcome)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `removing konfig force restores UP-TO-DATE behaviour`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
""")
|
||||||
|
// Run once without force to prime the cache
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
// Second run also without force → UP-TO-DATE
|
||||||
|
assertEquals(
|
||||||
|
TaskOutcome.UP_TO_DATE,
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE")).task(":generateKonfig")?.outcome
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Generated output survives subsequent UP-TO-DATE runs ─────────────────
|
||||||
|
|
||||||
|
@Test fun `generated file content is unchanged after UP-TO-DATE run`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig { field("STABLE", "value") }
|
||||||
|
""")
|
||||||
|
val args = listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE")
|
||||||
|
run(args)
|
||||||
|
val textAfterFirst = dir.generatedFile().readText()
|
||||||
|
run(args) // UP-TO-DATE
|
||||||
|
assertEquals(textAfterFirst, dir.generatedFile().readText())
|
||||||
|
assertTrue(textAfterFirst.contains("""const val STABLE: String = "value""""))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
package com.bitsycore.konfig
|
||||||
|
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertFalse
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Functional tests for the `common {}` block inside a dimension.
|
||||||
|
*
|
||||||
|
* The common block provides fallback fields for all variants — a variant field
|
||||||
|
* with the same name must take precedence over the common field.
|
||||||
|
*/
|
||||||
|
class CommonBlockFunctionalTest : FunctionalTestBase() {
|
||||||
|
|
||||||
|
// ── Common field is present when variant has no override ──────────────────
|
||||||
|
|
||||||
|
@Test fun `common field appears when active variant does not define it`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
dimension("env") {
|
||||||
|
common { field("TIMEOUT", 30) }
|
||||||
|
variant("dev") { field("URL", "https://dev.example.com") }
|
||||||
|
variant("prod") { field("URL", "https://prod.example.com") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE", "-Pkonfig.dimension.env=dev"))
|
||||||
|
val text = dir.generatedFile().readText()
|
||||||
|
assertTrue(text.contains("const val TIMEOUT: Int = 30"))
|
||||||
|
assertTrue(text.contains("""const val URL: String = "https://dev.example.com""""))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `common field appears for both variants`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
dimension("env") {
|
||||||
|
common { field("RETRIES", 3) }
|
||||||
|
variant("dev") { field("URL", "https://dev.example.com") }
|
||||||
|
variant("prod") { field("URL", "https://prod.example.com") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
// dev variant
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE", "-Pkonfig.dimension.env=dev"))
|
||||||
|
assertTrue(dir.generatedFile().readText().contains("const val RETRIES: Int = 3"))
|
||||||
|
|
||||||
|
// prod variant
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE", "-Pkonfig.dimension.env=prod"))
|
||||||
|
assertTrue(dir.generatedFile().readText().contains("const val RETRIES: Int = 3"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Variant field overrides common field ─────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `variant field overrides common field with same name`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
dimension("env") {
|
||||||
|
common { field("TIMEOUT", 30) }
|
||||||
|
variant("dev") { field("TIMEOUT", 5) }
|
||||||
|
variant("prod") { /* uses common TIMEOUT */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE", "-Pkonfig.dimension.env=dev"))
|
||||||
|
assertTrue(dir.generatedFile().readText().contains("const val TIMEOUT: Int = 5"))
|
||||||
|
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE", "-Pkonfig.dimension.env=prod"))
|
||||||
|
assertTrue(dir.generatedFile().readText().contains("const val TIMEOUT: Int = 30"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Common block supports build-type scopes ───────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `common debug scope applies for DEBUG build`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
dimension("env") {
|
||||||
|
common {
|
||||||
|
field("TIMEOUT", 30)
|
||||||
|
debug { field("TIMEOUT", 2) }
|
||||||
|
}
|
||||||
|
variant("dev") { field("URL", "https://dev.example.com") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=DEBUG", "-Pkonfig.dimension.env=dev"))
|
||||||
|
assertTrue(dir.generatedFile().readText().contains("const val TIMEOUT: Int = 2"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `common debug scope does not affect RELEASE build`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
dimension("env") {
|
||||||
|
common {
|
||||||
|
field("TIMEOUT", 30)
|
||||||
|
debug { field("TIMEOUT", 2) }
|
||||||
|
}
|
||||||
|
variant("prod") { field("URL", "https://prod.example.com") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE", "-Pkonfig.dimension.env=prod"))
|
||||||
|
assertTrue(dir.generatedFile().readText().contains("const val TIMEOUT: Int = 30"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `common release scope applies for RELEASE build`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
dimension("env") {
|
||||||
|
common {
|
||||||
|
field("LOG", "info")
|
||||||
|
release { field("LOG", "error") }
|
||||||
|
}
|
||||||
|
variant("prod") { field("URL", "https://prod.example.com") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE", "-Pkonfig.dimension.env=prod"))
|
||||||
|
assertTrue(dir.generatedFile().readText().contains("""const val LOG: String = "error""""))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Common-only dimension (no variant-specific fields) ────────────────────
|
||||||
|
|
||||||
|
@Test fun `dimension with only common fields and empty variant generates common fields`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
dimension("env") {
|
||||||
|
common {
|
||||||
|
field("BASE", "https://base.example.com")
|
||||||
|
field("TIMEOUT", 10)
|
||||||
|
}
|
||||||
|
variant("any") { /* intentionally empty */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE", "-Pkonfig.dimension.env=any"))
|
||||||
|
val text = dir.generatedFile().readText()
|
||||||
|
assertTrue(text.contains("""const val BASE: String = "https://base.example.com""""))
|
||||||
|
assertTrue(text.contains("const val TIMEOUT: Int = 10"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Common fields from multiple common{} calls ────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `multiple common blocks merge their fields`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
dimension("env") {
|
||||||
|
common { field("A", "first") }
|
||||||
|
common { field("B", "second") }
|
||||||
|
variant("x") { /* empty */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE", "-Pkonfig.dimension.env=x"))
|
||||||
|
val text = dir.generatedFile().readText()
|
||||||
|
assertTrue(text.contains("""const val A: String = "first""""))
|
||||||
|
assertTrue(text.contains("""const val B: String = "second""""))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Common does not bleed into unrelated dimension ────────────────────────
|
||||||
|
|
||||||
|
@Test fun `common fields in one dimension do not appear in another dimension`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
dimension("env") {
|
||||||
|
common { field("SHARED", "env-shared") }
|
||||||
|
variant("prod") { field("URL", "https://prod.example.com") }
|
||||||
|
}
|
||||||
|
dimension("region", defaultTo = "eu") {
|
||||||
|
variant("eu") { field("CDN", "https://eu-cdn.example.com") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE", "-Pkonfig.dimension.env=prod"))
|
||||||
|
val text = dir.generatedFile().readText()
|
||||||
|
// SHARED should be under Env object
|
||||||
|
val envBlock = text.substringAfter("object Env")
|
||||||
|
val regionBlock = if (text.contains("object Region")) text.substringAfter("object Region") else ""
|
||||||
|
assertTrue(envBlock.contains("SHARED"))
|
||||||
|
assertFalse(regionBlock.contains("SHARED"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,273 @@
|
|||||||
|
package com.bitsycore.konfig
|
||||||
|
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertFalse
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Functional tests for the `dimension()` DSL:
|
||||||
|
* explicit property selection, objectNameOverride, defaultTo fallback,
|
||||||
|
* silent omission when no variant is resolved, variant field overrides,
|
||||||
|
* VARIANT const, missing fields, and multiple dimensions.
|
||||||
|
*/
|
||||||
|
class DimensionsFunctionalTest : FunctionalTestBase() {
|
||||||
|
|
||||||
|
// ── Explicit property selection ───────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `explicit property selects correct variant`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
dimension("env") {
|
||||||
|
variant("prod") { field("SERVER_URL", "https://prod.example.com") }
|
||||||
|
variant("dev") { field("SERVER_URL", "https://dev.example.com") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE", "-Pkonfig.dimension.env=dev"))
|
||||||
|
val text = dir.generatedFile().readText()
|
||||||
|
assertTrue(text.contains("object Env /*env*/"))
|
||||||
|
assertTrue(text.contains("""const val VARIANT: String = "dev""""))
|
||||||
|
assertTrue(text.contains("""const val SERVER_URL: String = "https://dev.example.com""""))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `selecting prod variant generates prod fields`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
dimension("env") {
|
||||||
|
variant("prod") { field("SERVER_URL", "https://prod.example.com") }
|
||||||
|
variant("dev") { field("SERVER_URL", "https://dev.example.com") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE", "-Pkonfig.dimension.env=prod"))
|
||||||
|
val text = dir.generatedFile().readText()
|
||||||
|
assertTrue(text.contains("""const val VARIANT: String = "prod""""))
|
||||||
|
assertTrue(text.contains("""const val SERVER_URL: String = "https://prod.example.com""""))
|
||||||
|
assertFalse(text.contains("dev.example.com"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── objectNameOverride ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `objectNameOverride is used instead of derived name`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
dimension("env", objectNameOverride = "Environment") {
|
||||||
|
variant("prod") { field("X", "y") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE", "-Pkonfig.dimension.env=prod"))
|
||||||
|
val text = dir.generatedFile().readText()
|
||||||
|
assertTrue(text.contains("object Environment /*env*/"))
|
||||||
|
assertFalse(text.contains("object Env /*"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── defaultTo fallback ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `defaultTo is used when no property is set`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
dimension("region", defaultTo = "eu") {
|
||||||
|
variant("eu") { field("BASE_URL", "https://eu.example.com") }
|
||||||
|
variant("us") { field("BASE_URL", "https://us.example.com") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
val text = dir.generatedFile().readText()
|
||||||
|
assertTrue(text.contains("object Region /*region*/"))
|
||||||
|
assertTrue(text.contains("""const val VARIANT: String = "eu""""))
|
||||||
|
assertTrue(text.contains("""const val BASE_URL: String = "https://eu.example.com""""))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `explicit property overrides defaultTo`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
dimension("region", defaultTo = "eu") {
|
||||||
|
variant("eu") { field("BASE_URL", "https://eu.example.com") }
|
||||||
|
variant("us") { field("BASE_URL", "https://us.example.com") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE", "-Pkonfig.dimension.region=us"))
|
||||||
|
val text = dir.generatedFile().readText()
|
||||||
|
assertTrue(text.contains("""const val VARIANT: String = "us""""))
|
||||||
|
assertFalse(text.contains("eu.example.com"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Silent omission ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `dimension without resolved variant is omitted silently`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
dimension("env") {
|
||||||
|
variant("prod") { field("X", "y") }
|
||||||
|
}
|
||||||
|
field("ALWAYS", "here")
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
val text = dir.generatedFile().readText()
|
||||||
|
assertFalse(text.contains("object Env"))
|
||||||
|
assertTrue(text.contains("""const val ALWAYS: String = "here""""))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Variant field debug/release overrides ─────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `variant field debug override applied for DEBUG build`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
dimension("env") {
|
||||||
|
variant("dev") {
|
||||||
|
field("URL", "https://dev.example.com").debug("https://dev-debug.example.com")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=DEBUG", "-Pkonfig.dimension.env=dev"))
|
||||||
|
assertTrue(dir.generatedFile().readText()
|
||||||
|
.contains("""const val URL: String = "https://dev-debug.example.com""""))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `variant field release override applied for RELEASE build`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
dimension("env") {
|
||||||
|
variant("prod") {
|
||||||
|
field("URL", "https://default.example.com").release("https://prod.example.com")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE", "-Pkonfig.dimension.env=prod"))
|
||||||
|
assertTrue(dir.generatedFile().readText()
|
||||||
|
.contains("""const val URL: String = "https://prod.example.com""""))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Missing fields ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `field defined in variant A does not appear when variant B is active`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
dimension("env") {
|
||||||
|
variant("a") { field("ONLY_IN_A", true) }
|
||||||
|
variant("b") { /* no ONLY_IN_A */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE", "-Pkonfig.dimension.env=b"))
|
||||||
|
assertFalse(dir.generatedFile().readText().contains("ONLY_IN_A"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Multiple dimensions ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `two dimensions both appear in output when both resolved`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
dimension("env") {
|
||||||
|
variant("prod") { field("SERVER", "https://prod.example.com") }
|
||||||
|
}
|
||||||
|
dimension("region") {
|
||||||
|
variant("eu") { field("CDN", "https://eu-cdn.example.com") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
run(listOf(
|
||||||
|
"generateKonfig",
|
||||||
|
"-Pkonfig.buildtype=RELEASE",
|
||||||
|
"-Pkonfig.dimension.env=prod",
|
||||||
|
"-Pkonfig.dimension.region=eu"
|
||||||
|
))
|
||||||
|
val text = dir.generatedFile().readText()
|
||||||
|
assertTrue(text.contains("object Env /*env*/"))
|
||||||
|
assertTrue(text.contains("object Region /*region*/"))
|
||||||
|
assertTrue(text.contains("""const val SERVER: String = "https://prod.example.com""""))
|
||||||
|
assertTrue(text.contains("""const val CDN: String = "https://eu-cdn.example.com""""))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `one dimension resolved and one skipped coexist correctly`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
dimension("env", defaultTo = "prod") {
|
||||||
|
variant("prod") { field("SERVER", "https://prod.example.com") }
|
||||||
|
}
|
||||||
|
dimension("region") { // no defaultTo, no property
|
||||||
|
variant("eu") { field("CDN", "https://eu-cdn.example.com") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
val text = dir.generatedFile().readText()
|
||||||
|
assertTrue(text.contains("object Env /*env*/"))
|
||||||
|
assertFalse(text.contains("object Region"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── CamelCase objectName derivation ──────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `hyphenated dimension name becomes CamelCase object name`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
dimension("build-env", defaultTo = "prod") {
|
||||||
|
variant("prod") { field("X", "y") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
assertTrue(dir.generatedFile().readText().contains("object BuildEnv /*build-env*/"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `underscore dimension name becomes CamelCase object name`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
dimension("my_region", defaultTo = "eu") {
|
||||||
|
variant("eu") { field("X", "y") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
assertTrue(dir.generatedFile().readText().contains("object MyRegion /*my_region*/"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── VARIANT const is always present ──────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `VARIANT const is always present in dimension object`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
dimension("env", defaultTo = "staging") {
|
||||||
|
variant("staging") { /* no fields */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
val text = dir.generatedFile().readText()
|
||||||
|
assertTrue(text.contains("""const val VARIANT: String = "staging""""))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
package com.bitsycore.konfig
|
||||||
|
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Functional tests for duplicate-name detection at configuration time.
|
||||||
|
* The plugin must fail fast with a clear error when the same name is
|
||||||
|
* declared more than once for dimensions, global fields, or variant fields.
|
||||||
|
*/
|
||||||
|
class DuplicateDetectionFunctionalTest : FunctionalTestBase() {
|
||||||
|
|
||||||
|
// ── Duplicate dimension name ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `duplicate dimension name fails configuration with clear error`() = withFailingProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
dimension("env") { variant("prod") { field("X", "y") } }
|
||||||
|
dimension("env") { variant("dev") { field("X", "z") } }
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
val result = run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
assertTrue(result.output.contains("env"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Duplicate global field name ───────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `duplicate global field name fails configuration`() = withFailingProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
field("API_URL", "https://prod.example.com")
|
||||||
|
field("API_URL", "https://dev.example.com")
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
val result = run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
assertTrue(result.output.contains("API_URL"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `duplicate global field of different types fails configuration`() = withFailingProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
field("FLAG", true)
|
||||||
|
field("FLAG", "stringNow")
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
val result = run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
assertTrue(result.output.contains("FLAG"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Duplicate variant field name ──────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `duplicate field name within variant fails configuration`() = withFailingProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
dimension("env") {
|
||||||
|
variant("prod") {
|
||||||
|
field("URL", "https://a.example.com")
|
||||||
|
field("URL", "https://b.example.com")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
val result = run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE", "-Pkonfig.dimension.env=prod"))
|
||||||
|
assertTrue(result.output.contains("URL"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `same field name in different variants is allowed`() = withProject { dir, run ->
|
||||||
|
// Different variants may each define the same field name — that is the whole point
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
dimension("env") {
|
||||||
|
variant("prod") { field("URL", "https://prod.example.com") }
|
||||||
|
variant("dev") { field("URL", "https://dev.example.com") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
// Should succeed without any exception
|
||||||
|
val result = run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE", "-Pkonfig.dimension.env=dev"))
|
||||||
|
assertTrue(result.output.contains("generateKonfig"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `same field name in different dimensions is allowed`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
dimension("env", defaultTo = "prod") {
|
||||||
|
variant("prod") { field("URL", "https://prod.example.com") }
|
||||||
|
}
|
||||||
|
dimension("region", defaultTo = "eu") {
|
||||||
|
variant("eu") { field("URL", "https://eu.example.com") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
val result = run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
assertTrue(result.output.contains("generateKonfig"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Duplicate dimension name — error message ──────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `duplicate dimension error message contains dimension name`() = withFailingProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
dimension("my-dim") { variant("a") { field("X", "1") } }
|
||||||
|
dimension("my-dim") { variant("b") { field("X", "2") } }
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
val result = run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
assertTrue(result.output.contains("my-dim"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Duplicate global field — error message ────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `duplicate global field error message contains field name`() = withFailingProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
field("UNIQUE_KEY", "first")
|
||||||
|
field("UNIQUE_KEY", "second")
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
val result = run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
assertTrue(result.output.contains("UNIQUE_KEY"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
package com.bitsycore.konfig
|
||||||
|
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertFalse
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Functional tests for every supported field type emitted by the plugin:
|
||||||
|
* String, Boolean, Int, Long, Float, Double — including correct Kotlin
|
||||||
|
* literal syntax (suffixes, special Float/Double values, escaping).
|
||||||
|
*/
|
||||||
|
class FieldTypesFunctionalTest : FunctionalTestBase() {
|
||||||
|
|
||||||
|
// ── Long ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `Long field emits L suffix`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
field("BIG", 9_000_000_000L)
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
assertTrue(dir.generatedFile().readText().contains("const val BIG: Long = 9000000000L"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `Long zero emits L suffix`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig { field("Z", 0L) }
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
assertTrue(dir.generatedFile().readText().contains("const val Z: Long = 0L"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `Long max value emits correctly`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig { field("MAX", Long.MAX_VALUE) }
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
assertTrue(dir.generatedFile().readText().contains("const val MAX: Long = 9223372036854775807L"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Float ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `Float field emits f suffix`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig { field("RATIO", 1.5f) }
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
val text = dir.generatedFile().readText()
|
||||||
|
assertTrue(text.contains("const val RATIO: Float = "))
|
||||||
|
assertTrue(text.contains("f"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `Float NaN emits Float_NaN`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig { field("NAN", Float.NaN) }
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
assertTrue(dir.generatedFile().readText().contains("const val NAN: Float = Float.NaN"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `Float POSITIVE_INFINITY emits Float_POSITIVE_INFINITY`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig { field("INF", Float.POSITIVE_INFINITY) }
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
assertTrue(dir.generatedFile().readText()
|
||||||
|
.contains("const val INF: Float = Float.POSITIVE_INFINITY"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `Float NEGATIVE_INFINITY emits Float_NEGATIVE_INFINITY`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig { field("NINF", Float.NEGATIVE_INFINITY) }
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
assertTrue(dir.generatedFile().readText()
|
||||||
|
.contains("const val NINF: Float = Float.NEGATIVE_INFINITY"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Double ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `Double field emits no suffix`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig { field("PI", 3.14159) }
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
val text = dir.generatedFile().readText()
|
||||||
|
assertTrue(text.contains("const val PI: Double = 3.14159"))
|
||||||
|
assertFalse(text.contains("3.14159f"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `Double NaN emits Double_NaN`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig { field("NAN", Double.NaN) }
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
assertTrue(dir.generatedFile().readText().contains("const val NAN: Double = Double.NaN"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `Double POSITIVE_INFINITY emits Double_POSITIVE_INFINITY`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig { field("INF", Double.POSITIVE_INFINITY) }
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
assertTrue(dir.generatedFile().readText()
|
||||||
|
.contains("const val INF: Double = Double.POSITIVE_INFINITY"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `Double NEGATIVE_INFINITY emits Double_NEGATIVE_INFINITY`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig { field("NINF", Double.NEGATIVE_INFINITY) }
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
assertTrue(dir.generatedFile().readText()
|
||||||
|
.contains("const val NINF: Double = Double.NEGATIVE_INFINITY"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Boolean (DEBUG inline val vs RELEASE const val) ───────────────────────
|
||||||
|
|
||||||
|
@Test fun `Boolean true in RELEASE is const val`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig { field("B", true) }
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
assertTrue(dir.generatedFile().readText().contains("const val B: Boolean = true"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `Boolean false in DEBUG is inline val getter`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig { field("B", false) }
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=DEBUG"))
|
||||||
|
assertTrue(dir.generatedFile().readText().contains("inline val B: Boolean get() = false"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── All types together ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `all field types appear correctly in a single object`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
field("STR", "hello")
|
||||||
|
field("BOOL", true)
|
||||||
|
field("INT", 42)
|
||||||
|
field("LONG", 99L)
|
||||||
|
field("FLOAT", 2.5f)
|
||||||
|
field("DOUBLE", 1.23)
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
val text = dir.generatedFile().readText()
|
||||||
|
assertTrue(text.contains("""const val STR: String = "hello""""))
|
||||||
|
assertTrue(text.contains("const val BOOL: Boolean = true"))
|
||||||
|
assertTrue(text.contains("const val INT: Int = 42"))
|
||||||
|
assertTrue(text.contains("const val LONG: Long = 99L"))
|
||||||
|
assertTrue(text.contains("const val FLOAT: Float = ") && text.contains("f"))
|
||||||
|
assertTrue(text.contains("const val DOUBLE: Double = 1.23"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Types in dimension fields ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `Long field inside dimension variant is emitted correctly`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
dimension("env", defaultTo = "prod") {
|
||||||
|
variant("prod") { field("TOKEN_TTL", 3_600_000L) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
assertTrue(dir.generatedFile().readText().contains("const val TOKEN_TTL: Long = 3600000L"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `Double field inside dimension variant is emitted correctly`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
dimension("env", defaultTo = "prod") {
|
||||||
|
variant("prod") { field("THRESHOLD", 0.95) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
assertTrue(dir.generatedFile().readText().contains("const val THRESHOLD: Double = 0.95"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package com.bitsycore.konfig
|
||||||
|
|
||||||
|
import org.gradle.testkit.runner.BuildResult
|
||||||
|
import org.gradle.testkit.runner.GradleRunner
|
||||||
|
import java.io.File
|
||||||
|
import kotlin.io.path.createTempDirectory
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for all functional tests.
|
||||||
|
*
|
||||||
|
* Provides:
|
||||||
|
* - [withProject] — creates a temp Gradle project, runs it, then cleans up.
|
||||||
|
* - [File.generatedFile] — walks the output dir to find the generated `.kt` file.
|
||||||
|
* - [File.writeBuildGradle] — shorthand for writing a `build.gradle.kts`.
|
||||||
|
*/
|
||||||
|
abstract class FunctionalTestBase {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a fresh temp project directory, invokes [block] with it, then deletes it.
|
||||||
|
*
|
||||||
|
* @param block receives `(projectDir, run)` where `run(args)` executes Gradle and
|
||||||
|
* returns the [BuildResult] (throws on build failure by default).
|
||||||
|
*/
|
||||||
|
protected fun withProject(block: (projectDir: File, run: (List<String>) -> BuildResult) -> Unit) {
|
||||||
|
val projectDir = createTempDirectory("konfig-ft").toFile()
|
||||||
|
try {
|
||||||
|
projectDir.resolve("settings.gradle.kts")
|
||||||
|
.writeText("""rootProject.name = "test-project"""")
|
||||||
|
val run = { args: List<String> ->
|
||||||
|
GradleRunner.create()
|
||||||
|
.withProjectDir(projectDir)
|
||||||
|
.withPluginClasspath()
|
||||||
|
.withArguments(args)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
block(projectDir, run)
|
||||||
|
} finally {
|
||||||
|
projectDir.deleteRecursively()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Same as [withProject] but Gradle is invoked with `buildAndFail()` so a build
|
||||||
|
* failure does NOT throw — the returned [BuildResult] carries the failed output.
|
||||||
|
*/
|
||||||
|
protected fun withFailingProject(block: (projectDir: File, run: (List<String>) -> BuildResult) -> Unit) {
|
||||||
|
val projectDir = createTempDirectory("konfig-ft-fail").toFile()
|
||||||
|
try {
|
||||||
|
projectDir.resolve("settings.gradle.kts")
|
||||||
|
.writeText("""rootProject.name = "test-project"""")
|
||||||
|
val run = { args: List<String> ->
|
||||||
|
GradleRunner.create()
|
||||||
|
.withProjectDir(projectDir)
|
||||||
|
.withPluginClasspath()
|
||||||
|
.withArguments(args)
|
||||||
|
.buildAndFail()
|
||||||
|
}
|
||||||
|
block(projectDir, run)
|
||||||
|
} finally {
|
||||||
|
projectDir.deleteRecursively()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Finds the single generated `.kt` file inside the project's output tree. */
|
||||||
|
protected fun File.generatedFile(): File =
|
||||||
|
walkTopDown().first { it.isFile && it.extension == "kt" }
|
||||||
|
|
||||||
|
/** Writes a minimal `build.gradle.kts` applying the konfig plugin. */
|
||||||
|
protected fun File.writeBuildGradle(content: String) =
|
||||||
|
resolve("build.gradle.kts").writeText(content.trimIndent())
|
||||||
|
}
|
||||||
@@ -0,0 +1,301 @@
|
|||||||
|
package com.bitsycore.konfig
|
||||||
|
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertFalse
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Functional tests for top-level (global) field declarations:
|
||||||
|
* String, Boolean, Int — defaults, debug overrides, release overrides,
|
||||||
|
* and the debug/release scope block syntax.
|
||||||
|
*/
|
||||||
|
class GlobalFieldsFunctionalTest : FunctionalTestBase() {
|
||||||
|
|
||||||
|
// ── String fields ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `string field is generated with correct const val`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
field("API_URL", "https://api.example.com")
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
assertTrue(dir.generatedFile().readText()
|
||||||
|
.contains("""const val API_URL: String = "https://api.example.com""""))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `string field with quotes is escaped correctly`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
field("APP_NAME", "My \"App\"")
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
assertTrue(dir.generatedFile().readText()
|
||||||
|
.contains("""const val APP_NAME: String = "My \"App\"""""))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `string field with backslash is escaped correctly`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
field("PATH", "C:\\Users\\dev")
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
val text = dir.generatedFile().readText()
|
||||||
|
assertTrue(text.contains("const val PATH: String = "))
|
||||||
|
assertTrue(text.contains("\\\\"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `string field with dollar sign is escaped correctly`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
field("TEMPLATE", "price: ${'$'}99")
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
val text = dir.generatedFile().readText()
|
||||||
|
// Dollar in string literals must be escaped as \$
|
||||||
|
assertTrue(text.contains("""\${'$'}""") || text.contains("""\$"""))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `empty string field is generated`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
field("EMPTY", "")
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
assertTrue(dir.generatedFile().readText().contains("""const val EMPTY: String = """""))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Boolean fields ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `boolean false field emits const val in RELEASE`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
field("LOGGING", false)
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
assertTrue(dir.generatedFile().readText().contains("const val LOGGING: Boolean = false"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `boolean true field emits const val in RELEASE`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
field("FLAG", true)
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
assertTrue(dir.generatedFile().readText().contains("const val FLAG: Boolean = true"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `boolean field emits inline val getter in DEBUG`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
field("LOGGING", false)
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=DEBUG"))
|
||||||
|
assertTrue(dir.generatedFile().readText().contains("inline val LOGGING: Boolean get() = false"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Int fields ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `int field is generated with correct const val`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
field("TIMEOUT", 30)
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
assertTrue(dir.generatedFile().readText().contains("const val TIMEOUT: Int = 30"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `zero int field is generated`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
field("ZERO", 0)
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
assertTrue(dir.generatedFile().readText().contains("const val ZERO: Int = 0"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `negative int field is generated`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
field("NEG", -1)
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
assertTrue(dir.generatedFile().readText().contains("const val NEG: Int = -1"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Debug overrides ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `string debug override is applied for DEBUG build`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
field("BASE_URL", "https://prod.example.com").debug("https://dev.example.com")
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=DEBUG"))
|
||||||
|
assertTrue(dir.generatedFile().readText()
|
||||||
|
.contains("""const val BASE_URL: String = "https://dev.example.com""""))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `string debug override is NOT applied for RELEASE build`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
field("BASE_URL", "https://prod.example.com").debug("https://dev.example.com")
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
val text = dir.generatedFile().readText()
|
||||||
|
assertTrue(text.contains("""const val BASE_URL: String = "https://prod.example.com""""))
|
||||||
|
assertFalse(text.contains("dev.example.com"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `boolean debug override emits inline val getter in DEBUG`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
field("LOGGING", false).debug(true)
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=DEBUG"))
|
||||||
|
assertTrue(dir.generatedFile().readText().contains("inline val LOGGING: Boolean get() = true"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `int debug override is applied for DEBUG build`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
field("TIMEOUT", 30).debug(5)
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=DEBUG"))
|
||||||
|
assertTrue(dir.generatedFile().readText().contains("const val TIMEOUT: Int = 5"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Release overrides ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `string release override is applied for RELEASE build`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
field("URL", "https://default.example.com").release("https://prod.example.com")
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
assertTrue(dir.generatedFile().readText()
|
||||||
|
.contains("""const val URL: String = "https://prod.example.com""""))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `release override is NOT applied for DEBUG build`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
field("URL", "https://default.example.com").release("https://prod.example.com")
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=DEBUG"))
|
||||||
|
assertTrue(dir.generatedFile().readText()
|
||||||
|
.contains("""const val URL: String = "https://default.example.com""""))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── debug {} / release {} block syntax ───────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `debug scope block sets field for DEBUG build`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
debug {
|
||||||
|
field("LOG_LEVEL", "verbose")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=DEBUG"))
|
||||||
|
assertTrue(dir.generatedFile().readText()
|
||||||
|
.contains("""const val LOG_LEVEL: String = "verbose""""))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `debug scope block field is absent for RELEASE build`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
debug {
|
||||||
|
field("DEBUG_ONLY", "yes")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
assertFalse(dir.generatedFile().readText().contains("DEBUG_ONLY"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `release scope block sets field for RELEASE build`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
release {
|
||||||
|
field("LOG_LEVEL", "error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
assertTrue(dir.generatedFile().readText()
|
||||||
|
.contains("""const val LOG_LEVEL: String = "error""""))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `multiple global fields all appear in output`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
field("A", "alpha")
|
||||||
|
field("B", 42)
|
||||||
|
field("C", true)
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
val text = dir.generatedFile().readText()
|
||||||
|
assertTrue(text.contains("""const val A: String = "alpha""""))
|
||||||
|
assertTrue(text.contains("const val B: Int = 42"))
|
||||||
|
assertTrue(text.contains("const val C: Boolean = true"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,643 +0,0 @@
|
|||||||
package com.bitsycore.konfig
|
|
||||||
|
|
||||||
import org.gradle.testkit.runner.BuildResult
|
|
||||||
import org.gradle.testkit.runner.GradleRunner
|
|
||||||
import org.gradle.testkit.runner.TaskOutcome
|
|
||||||
import java.io.File
|
|
||||||
import kotlin.io.path.createTempDirectory
|
|
||||||
import kotlin.test.Test
|
|
||||||
import kotlin.test.assertEquals
|
|
||||||
import kotlin.test.assertFalse
|
|
||||||
import kotlin.test.assertTrue
|
|
||||||
|
|
||||||
@Suppress("FunctionName")
|
|
||||||
class KonfigPluginFunctionalTest {
|
|
||||||
|
|
||||||
private fun withProject(block: (projectDir: File, run: (List<String>) -> BuildResult) -> Unit) {
|
|
||||||
val projectDir = createTempDirectory("konfig-ft").toFile()
|
|
||||||
try {
|
|
||||||
projectDir.resolve("settings.gradle.kts").writeText("""rootProject.name = "test-project"""")
|
|
||||||
val run = { args: List<String> ->
|
|
||||||
GradleRunner.create()
|
|
||||||
.withProjectDir(projectDir)
|
|
||||||
.withPluginClasspath()
|
|
||||||
.withArguments(args)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
block(projectDir, run)
|
|
||||||
} finally {
|
|
||||||
projectDir.deleteRecursively()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun File.generatedBuildKonfig(): File =
|
|
||||||
walkTopDown().first { it.isFile && it.extension == "kt" }
|
|
||||||
|
|
||||||
// ─── Basic generation ─────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
@Test fun `generateKonfig produces file with correct package and BUILD_TYPE`() = withProject { dir, run ->
|
|
||||||
dir.resolve("build.gradle.kts").writeText("""
|
|
||||||
plugins { id("com.bitsycore.konfig") }
|
|
||||||
group = "com.example"
|
|
||||||
""".trimIndent())
|
|
||||||
|
|
||||||
val result = run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
|
||||||
|
|
||||||
assertEquals(TaskOutcome.SUCCESS, result.task(":generateKonfig")?.outcome)
|
|
||||||
|
|
||||||
val text = dir.generatedBuildKonfig().readText()
|
|
||||||
assertTrue(text.contains("package com.example.test.project"))
|
|
||||||
assertTrue(text.contains("object BuildKonfig {"))
|
|
||||||
assertTrue(text.contains("""const val BUILD_TYPE: String = "release""""))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test fun `debug build type generates BUILD_TYPE debug`() = withProject { dir, run ->
|
|
||||||
dir.resolve("build.gradle.kts").writeText("""
|
|
||||||
plugins { id("com.bitsycore.konfig") }
|
|
||||||
group = "com.example"
|
|
||||||
konfig {}
|
|
||||||
""".trimIndent())
|
|
||||||
|
|
||||||
run(listOf("generateKonfig", "-Pkonfig.buildtype=DEBUG"))
|
|
||||||
|
|
||||||
val text = dir.generatedBuildKonfig().readText()
|
|
||||||
assertTrue(text.contains("""const val BUILD_TYPE: String = "debug""""))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Global fields ────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
@Test fun `string fields are generated correctly`() = withProject { dir, run ->
|
|
||||||
dir.resolve("build.gradle.kts").writeText("""
|
|
||||||
plugins { id("com.bitsycore.konfig") }
|
|
||||||
group = "com.example"
|
|
||||||
konfig {
|
|
||||||
field("API_URL", "https://api.example.com")
|
|
||||||
field("APP_NAME", "My \"App\"")
|
|
||||||
}
|
|
||||||
""".trimIndent())
|
|
||||||
|
|
||||||
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
|
||||||
|
|
||||||
val text = dir.generatedBuildKonfig().readText()
|
|
||||||
assertTrue(text.contains("""const val API_URL: String = "https://api.example.com""""))
|
|
||||||
assertTrue(text.contains("""const val APP_NAME: String = "My \"App\"""""))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test fun `boolean and int fields are generated correctly`() = withProject { dir, run ->
|
|
||||||
dir.resolve("build.gradle.kts").writeText("""
|
|
||||||
plugins { id("com.bitsycore.konfig") }
|
|
||||||
group = "com.example"
|
|
||||||
konfig {
|
|
||||||
field("LOGGING", false)
|
|
||||||
field("TIMEOUT", 30)
|
|
||||||
}
|
|
||||||
""".trimIndent())
|
|
||||||
|
|
||||||
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
|
||||||
|
|
||||||
val text = dir.generatedBuildKonfig().readText()
|
|
||||||
assertTrue(text.contains("const val LOGGING: Boolean = false"))
|
|
||||||
assertTrue(text.contains("const val TIMEOUT: Int = 30"))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test fun `global field debug override applied for debug build`() = withProject { dir, run ->
|
|
||||||
dir.resolve("build.gradle.kts").writeText("""
|
|
||||||
plugins { id("com.bitsycore.konfig") }
|
|
||||||
group = "com.example"
|
|
||||||
konfig {
|
|
||||||
field("BASE_URL", "https://prod.example.com").debug("https://dev.example.com")
|
|
||||||
field("LOGGING", false).debug(true)
|
|
||||||
}
|
|
||||||
""".trimIndent())
|
|
||||||
|
|
||||||
run(listOf("generateKonfig", "-Pkonfig.buildtype=DEBUG"))
|
|
||||||
|
|
||||||
val text = dir.generatedBuildKonfig().readText()
|
|
||||||
assertTrue(text.contains("""const val BASE_URL: String = "https://dev.example.com""""))
|
|
||||||
assertTrue(text.contains("inline val LOGGING: Boolean get() = true"))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test fun `release build uses default when no release override`() = withProject { dir, run ->
|
|
||||||
dir.resolve("build.gradle.kts").writeText("""
|
|
||||||
plugins { id("com.bitsycore.konfig") }
|
|
||||||
group = "com.example"
|
|
||||||
konfig {
|
|
||||||
field("BASE_URL", "https://prod.example.com").debug("https://dev.example.com")
|
|
||||||
}
|
|
||||||
""".trimIndent())
|
|
||||||
|
|
||||||
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
|
||||||
|
|
||||||
val text = dir.generatedBuildKonfig().readText()
|
|
||||||
assertTrue(text.contains("""const val BASE_URL: String = "https://prod.example.com""""))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Visibility & naming ──────────────────────────────────────────────────
|
|
||||||
|
|
||||||
@Test fun `internal visibility prefixes object`() = withProject { dir, run ->
|
|
||||||
dir.resolve("build.gradle.kts").writeText("""
|
|
||||||
plugins { id("com.bitsycore.konfig") }
|
|
||||||
group = "com.example"
|
|
||||||
konfig {
|
|
||||||
objectVisibility = com.bitsycore.konfig.Visibility.INTERNAL
|
|
||||||
}
|
|
||||||
""".trimIndent())
|
|
||||||
|
|
||||||
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
|
||||||
|
|
||||||
val text = dir.generatedBuildKonfig().readText()
|
|
||||||
assertTrue(text.contains("internal object BuildKonfig {"))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test fun `custom objectName is used`() = withProject { dir, run ->
|
|
||||||
dir.resolve("build.gradle.kts").writeText("""
|
|
||||||
plugins { id("com.bitsycore.konfig") }
|
|
||||||
group = "com.example"
|
|
||||||
konfig {
|
|
||||||
objectName = "AppConfig"
|
|
||||||
}
|
|
||||||
""".trimIndent())
|
|
||||||
|
|
||||||
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
|
||||||
|
|
||||||
val text = dir.generatedBuildKonfig().readText()
|
|
||||||
assertTrue(text.contains("object AppConfig {"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Dimensions ───────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
@Test fun `dimension with explicit property generates nested object`() = withProject { dir, run ->
|
|
||||||
dir.resolve("build.gradle.kts").writeText("""
|
|
||||||
plugins { id("com.bitsycore.konfig") }
|
|
||||||
group = "com.example"
|
|
||||||
konfig {
|
|
||||||
dimension("env") {
|
|
||||||
variant("prod") {
|
|
||||||
field("SERVER_URL", "https://prod.example.com")
|
|
||||||
}
|
|
||||||
variant("dev") {
|
|
||||||
field("SERVER_URL", "https://dev.example.com")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""".trimIndent())
|
|
||||||
|
|
||||||
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE", "-Pkonfig.dimension.env=dev"))
|
|
||||||
|
|
||||||
val text = dir.generatedBuildKonfig().readText()
|
|
||||||
assertTrue(text.contains("object Env /*env*/"))
|
|
||||||
assertTrue(text.contains("""const val VARIANT: String = "dev""""))
|
|
||||||
assertTrue(text.contains("""const val SERVER_URL: String = "https://dev.example.com""""))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test fun `dimension with objectNameOverride uses override name`() = withProject { dir, run ->
|
|
||||||
dir.resolve("build.gradle.kts").writeText("""
|
|
||||||
plugins { id("com.bitsycore.konfig") }
|
|
||||||
group = "com.example"
|
|
||||||
konfig {
|
|
||||||
dimension("env", objectNameOverride = "Environment") {
|
|
||||||
variant("prod") { field("X", "y") }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""".trimIndent())
|
|
||||||
|
|
||||||
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE", "-Pkonfig.dimension.env=prod"))
|
|
||||||
|
|
||||||
val text = dir.generatedBuildKonfig().readText()
|
|
||||||
assertTrue(text.contains("object Environment /*env*/"))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test fun `dimension with defaultTo uses default variant when no property set`() = withProject { dir, run ->
|
|
||||||
dir.resolve("build.gradle.kts").writeText("""
|
|
||||||
plugins { id("com.bitsycore.konfig") }
|
|
||||||
group = "com.example"
|
|
||||||
konfig {
|
|
||||||
dimension("region", defaultTo = "eu") {
|
|
||||||
variant("eu") { field("BASE_URL", "https://eu.example.com") }
|
|
||||||
variant("us") { field("BASE_URL", "https://us.example.com") }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""".trimIndent())
|
|
||||||
|
|
||||||
// No konfig.dimension.region property — falls back to defaultTo = "eu"
|
|
||||||
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
|
||||||
|
|
||||||
val text = dir.generatedBuildKonfig().readText()
|
|
||||||
assertTrue(text.contains("object Region /*region*/"))
|
|
||||||
assertTrue(text.contains("""const val VARIANT: String = "eu""""))
|
|
||||||
assertTrue(text.contains("""const val BASE_URL: String = "https://eu.example.com""""))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test fun `dimension without active variant is omitted silently`() = withProject { dir, run ->
|
|
||||||
dir.resolve("build.gradle.kts").writeText("""
|
|
||||||
plugins { id("com.bitsycore.konfig") }
|
|
||||||
group = "com.example"
|
|
||||||
konfig {
|
|
||||||
dimension("env") { // no defaultTo
|
|
||||||
variant("prod") { field("X", "y") }
|
|
||||||
}
|
|
||||||
field("ALWAYS", "here")
|
|
||||||
}
|
|
||||||
""".trimIndent())
|
|
||||||
|
|
||||||
// No konfig.dimension.env property → dimension omitted, no crash
|
|
||||||
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
|
||||||
|
|
||||||
val text = dir.generatedBuildKonfig().readText()
|
|
||||||
assertFalse(text.contains("object Env"))
|
|
||||||
assertTrue(text.contains("""const val ALWAYS: String = "here""""))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test fun `variant field debug override applied inside dimension`() = withProject { dir, run ->
|
|
||||||
dir.resolve("build.gradle.kts").writeText("""
|
|
||||||
plugins { id("com.bitsycore.konfig") }
|
|
||||||
group = "com.example"
|
|
||||||
konfig {
|
|
||||||
dimension("env") {
|
|
||||||
variant("dev") {
|
|
||||||
field("URL", "https://dev.example.com").debug("https://dev-debug.example.com")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""".trimIndent())
|
|
||||||
|
|
||||||
run(listOf("generateKonfig", "-Pkonfig.buildtype=DEBUG", "-Pkonfig.dimension.env=dev"))
|
|
||||||
|
|
||||||
val text = dir.generatedBuildKonfig().readText()
|
|
||||||
assertTrue(text.contains("""const val URL: String = "https://dev-debug.example.com""""))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test fun `missing field in variant does not appear in output`() = withProject { dir, run ->
|
|
||||||
dir.resolve("build.gradle.kts").writeText("""
|
|
||||||
plugins { id("com.bitsycore.konfig") }
|
|
||||||
group = "com.example"
|
|
||||||
konfig {
|
|
||||||
dimension("env") {
|
|
||||||
variant("a") { field("ONLY_IN_A", true) }
|
|
||||||
variant("b") { /* ONLY_IN_A not defined here */ }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""".trimIndent())
|
|
||||||
|
|
||||||
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE", "-Pkonfig.dimension.env=b"))
|
|
||||||
|
|
||||||
val text = dir.generatedBuildKonfig().readText()
|
|
||||||
assertFalse(text.contains("ONLY_IN_A"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Caching ─────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
@Test fun `task is UP-TO-DATE on second run`() = withProject { dir, run ->
|
|
||||||
dir.resolve("build.gradle.kts").writeText("""
|
|
||||||
plugins { id("com.bitsycore.konfig") }
|
|
||||||
group = "com.example"
|
|
||||||
konfig { field("FOO", "bar") }
|
|
||||||
""".trimIndent())
|
|
||||||
|
|
||||||
val args = listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE")
|
|
||||||
run(args)
|
|
||||||
val second = run(args)
|
|
||||||
|
|
||||||
assertEquals(TaskOutcome.UP_TO_DATE, second.task(":generateKonfig")?.outcome)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Logging & validation ─────────────────────────────────────────────────
|
|
||||||
|
|
||||||
@Test fun `build type source is logged at lifecycle level`() = withProject { dir, run ->
|
|
||||||
dir.resolve("build.gradle.kts").writeText("""
|
|
||||||
plugins { id("com.bitsycore.konfig") }
|
|
||||||
group = "com.example"
|
|
||||||
konfig {}
|
|
||||||
""".trimIndent())
|
|
||||||
|
|
||||||
val result = run(listOf("generateKonfig", "-Pkonfig.buildtype=DEBUG"))
|
|
||||||
|
|
||||||
assertTrue(result.output.contains("BUILD_TYPE = debug"))
|
|
||||||
assertTrue(result.output.contains("explicit property -Pkonfig.buildtype=DEBUG"))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test fun `dimension active variant and reason are logged`() = withProject { dir, run ->
|
|
||||||
dir.resolve("build.gradle.kts").writeText("""
|
|
||||||
plugins { id("com.bitsycore.konfig") }
|
|
||||||
group = "com.example"
|
|
||||||
konfig {
|
|
||||||
dimension("env") {
|
|
||||||
variant("prod") { field("X", "y") }
|
|
||||||
variant("dev") { field("X", "z") }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""".trimIndent())
|
|
||||||
|
|
||||||
val result = run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE", "-Pkonfig.dimension.env=dev"))
|
|
||||||
|
|
||||||
assertTrue(result.output.contains("dim 'env'"))
|
|
||||||
assertTrue(result.output.contains("'dev'"))
|
|
||||||
assertTrue(result.output.contains("konfig.dimension.env=dev"))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test fun `skipped dimension is logged`() = withProject { dir, run ->
|
|
||||||
dir.resolve("build.gradle.kts").writeText("""
|
|
||||||
plugins { id("com.bitsycore.konfig") }
|
|
||||||
group = "com.example"
|
|
||||||
konfig {
|
|
||||||
dimension("env") { // no defaultTo, no property set
|
|
||||||
variant("prod") { field("X", "y") }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""".trimIndent())
|
|
||||||
|
|
||||||
val result = run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
|
||||||
|
|
||||||
assertTrue(result.output.contains("dim 'env'"))
|
|
||||||
assertTrue(result.output.contains("skipped"))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test fun `unknown variant in property emits a warning`() = withProject { dir, run ->
|
|
||||||
dir.resolve("build.gradle.kts").writeText("""
|
|
||||||
plugins { id("com.bitsycore.konfig") }
|
|
||||||
group = "com.example"
|
|
||||||
konfig {
|
|
||||||
dimension("env") {
|
|
||||||
variant("prod") { field("X", "y") }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""".trimIndent())
|
|
||||||
|
|
||||||
val result = run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE", "-Pkonfig.dimension.env=typo"))
|
|
||||||
|
|
||||||
assertTrue(result.output.contains("typo"))
|
|
||||||
assertTrue(result.output.contains("not a known variant"))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test fun `invalid defaultTo fails build with clear error`() = withProject { dir, run ->
|
|
||||||
dir.resolve("build.gradle.kts").writeText("""
|
|
||||||
plugins { id("com.bitsycore.konfig") }
|
|
||||||
group = "com.example"
|
|
||||||
konfig {
|
|
||||||
dimension("env", defaultTo = "nonexistent") {
|
|
||||||
variant("prod") { field("X", "y") }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""".trimIndent())
|
|
||||||
|
|
||||||
val result = runCatching {
|
|
||||||
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
|
||||||
}
|
|
||||||
|
|
||||||
assertTrue(result.isFailure || result.getOrNull()?.output?.contains("nonexistent") == true)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test fun `invalid field name fails build with clear error`() = withProject { dir, run ->
|
|
||||||
dir.resolve("build.gradle.kts").writeText("""
|
|
||||||
plugins { id("com.bitsycore.konfig") }
|
|
||||||
group = "com.example"
|
|
||||||
konfig {
|
|
||||||
field("INVALID NAME", "oops")
|
|
||||||
}
|
|
||||||
""".trimIndent())
|
|
||||||
|
|
||||||
val result = runCatching {
|
|
||||||
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
|
||||||
}
|
|
||||||
|
|
||||||
assertTrue(result.isFailure)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test fun `generation summary is logged at lifecycle level`() = withProject { dir, run ->
|
|
||||||
dir.resolve("build.gradle.kts").writeText("""
|
|
||||||
plugins { id("com.bitsycore.konfig") }
|
|
||||||
group = "com.example"
|
|
||||||
konfig {
|
|
||||||
dimension("env", defaultTo = "prod") {
|
|
||||||
variant("prod") { field("URL", "https://prod.example.com") }
|
|
||||||
}
|
|
||||||
field("API_KEY", "abc123")
|
|
||||||
}
|
|
||||||
""".trimIndent())
|
|
||||||
|
|
||||||
val result = run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
|
||||||
|
|
||||||
// Summary line always present
|
|
||||||
assertTrue(result.output.contains("generated BuildKonfig.kt"))
|
|
||||||
assertTrue(result.output.contains("1 global field"))
|
|
||||||
assertTrue(result.output.contains("1 dimension"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Generated file header ────────────────────────────────────────────────
|
|
||||||
|
|
||||||
@Test fun `generated file contains Suppress RedundantVisibilityModifier header`() = withProject { dir, run ->
|
|
||||||
dir.resolve("build.gradle.kts").writeText("""
|
|
||||||
plugins { id("com.bitsycore.konfig") }
|
|
||||||
group = "com.example"
|
|
||||||
konfig {}
|
|
||||||
""".trimIndent())
|
|
||||||
|
|
||||||
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
|
||||||
|
|
||||||
val text = dir.generatedBuildKonfig().readText()
|
|
||||||
assertTrue(text.contains("""@file:Suppress("RedundantVisibilityModifier")"""))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── VARIANT visibility ───────────────────────────────────────────────────
|
|
||||||
|
|
||||||
@Test fun `VARIANT Object has visibility prefix when objectVisibility is INTERNAL`() = withProject { dir, run ->
|
|
||||||
dir.resolve("build.gradle.kts").writeText("""
|
|
||||||
plugins { id("com.bitsycore.konfig") }
|
|
||||||
group = "com.example"
|
|
||||||
konfig {
|
|
||||||
objectVisibility = com.bitsycore.konfig.Visibility.INTERNAL
|
|
||||||
dimension("env", defaultTo = "prod") {
|
|
||||||
variant("prod") { field("X", "y") }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""".trimIndent())
|
|
||||||
|
|
||||||
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
|
||||||
|
|
||||||
val text = dir.generatedBuildKonfig().readText()
|
|
||||||
assertTrue(text.contains("internal object Env"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Duplicate name detection ─────────────────────────────────────────────
|
|
||||||
|
|
||||||
@Test fun `duplicate dimension name fails build with clear error`() = withProject { dir, run ->
|
|
||||||
dir.resolve("build.gradle.kts").writeText("""
|
|
||||||
plugins { id("com.bitsycore.konfig") }
|
|
||||||
group = "com.example"
|
|
||||||
konfig {
|
|
||||||
dimension("env") { variant("prod") { field("X", "y") } }
|
|
||||||
dimension("env") { variant("dev") { field("X", "z") } }
|
|
||||||
}
|
|
||||||
""".trimIndent())
|
|
||||||
|
|
||||||
val result = runCatching {
|
|
||||||
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
|
||||||
}
|
|
||||||
|
|
||||||
assertTrue(result.isFailure || result.getOrNull()?.output?.contains("env") == true)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test fun `duplicate global field name fails build with clear error`() = withProject { dir, run ->
|
|
||||||
dir.resolve("build.gradle.kts").writeText("""
|
|
||||||
plugins { id("com.bitsycore.konfig") }
|
|
||||||
group = "com.example"
|
|
||||||
konfig {
|
|
||||||
field("API_URL", "https://prod.example.com")
|
|
||||||
field("API_URL", "https://dev.example.com")
|
|
||||||
}
|
|
||||||
""".trimIndent())
|
|
||||||
|
|
||||||
val result = runCatching {
|
|
||||||
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
|
||||||
}
|
|
||||||
|
|
||||||
assertTrue(result.isFailure)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test fun `duplicate variant field name fails build with clear error`() = withProject { dir, run ->
|
|
||||||
dir.resolve("build.gradle.kts").writeText("""
|
|
||||||
plugins { id("com.bitsycore.konfig") }
|
|
||||||
group = "com.example"
|
|
||||||
konfig {
|
|
||||||
dimension("env") {
|
|
||||||
variant("prod") {
|
|
||||||
field("URL", "https://a.example.com")
|
|
||||||
field("URL", "https://b.example.com")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""".trimIndent())
|
|
||||||
|
|
||||||
val result = runCatching {
|
|
||||||
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE", "-Pkonfig.dimension.env=prod"))
|
|
||||||
}
|
|
||||||
|
|
||||||
assertTrue(result.isFailure)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── konfig.properties file ───────────────────────────────────────────────
|
|
||||||
|
|
||||||
@Test fun `konfig_properties file sets dimension variant as fallback`() = withProject { dir, run ->
|
|
||||||
dir.resolve("build.gradle.kts").writeText("""
|
|
||||||
plugins { id("com.bitsycore.konfig") }
|
|
||||||
group = "com.example"
|
|
||||||
konfig {
|
|
||||||
dimension("env") {
|
|
||||||
variant("prod") { field("SERVER_URL", "https://prod.example.com") }
|
|
||||||
variant("dev") { field("SERVER_URL", "https://dev.example.com") }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""".trimIndent())
|
|
||||||
dir.resolve("konfig.properties").writeText("konfig.dimension.env=dev\n")
|
|
||||||
|
|
||||||
// No -P property — should pick up from konfig.properties
|
|
||||||
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
|
||||||
|
|
||||||
val text = dir.generatedBuildKonfig().readText()
|
|
||||||
assertTrue(text.contains("""const val VARIANT: String = "dev""""))
|
|
||||||
assertTrue(text.contains("""const val SERVER_URL: String = "https://dev.example.com""""))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test fun `gradle property overrides konfig_properties file`() = withProject { dir, run ->
|
|
||||||
dir.resolve("build.gradle.kts").writeText("""
|
|
||||||
plugins { id("com.bitsycore.konfig") }
|
|
||||||
group = "com.example"
|
|
||||||
konfig {
|
|
||||||
dimension("env") {
|
|
||||||
variant("prod") { field("SERVER_URL", "https://prod.example.com") }
|
|
||||||
variant("dev") { field("SERVER_URL", "https://dev.example.com") }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""".trimIndent())
|
|
||||||
dir.resolve("konfig.properties").writeText("konfig.dimension.env=dev\n")
|
|
||||||
|
|
||||||
// -P property takes priority over konfig.properties
|
|
||||||
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE", "-Pkonfig.dimension.env=prod"))
|
|
||||||
|
|
||||||
val text = dir.generatedBuildKonfig().readText()
|
|
||||||
assertTrue(text.contains("""const val VARIANT: String = "prod""""))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test fun `konfig_properties file change invalidates cache`() = withProject { dir, run ->
|
|
||||||
dir.resolve("build.gradle.kts").writeText("""
|
|
||||||
plugins { id("com.bitsycore.konfig") }
|
|
||||||
group = "com.example"
|
|
||||||
konfig {
|
|
||||||
dimension("env") {
|
|
||||||
variant("prod") { field("SERVER_URL", "https://prod.example.com") }
|
|
||||||
variant("dev") { field("SERVER_URL", "https://dev.example.com") }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""".trimIndent())
|
|
||||||
val propsFile = dir.resolve("konfig.properties")
|
|
||||||
propsFile.writeText("konfig.dimension.env=dev\n")
|
|
||||||
|
|
||||||
val args = listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE")
|
|
||||||
run(args)
|
|
||||||
|
|
||||||
propsFile.writeText("konfig.dimension.env=prod\n")
|
|
||||||
val second = run(args)
|
|
||||||
|
|
||||||
// After changing the file, task should NOT be UP-TO-DATE
|
|
||||||
assertFalse(second.task(":generateKonfig")?.outcome == TaskOutcome.UP_TO_DATE)
|
|
||||||
val text = dir.generatedBuildKonfig().readText()
|
|
||||||
assertTrue(text.contains("""const val VARIANT: String = "prod""""))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Validation completeness ──────────────────────────────────────────────
|
|
||||||
|
|
||||||
@Test fun `invalid field name in boolean dimension field fails build`() = withProject { dir, run ->
|
|
||||||
dir.resolve("build.gradle.kts").writeText("""
|
|
||||||
plugins { id("com.bitsycore.konfig") }
|
|
||||||
group = "com.example"
|
|
||||||
konfig {
|
|
||||||
dimension("env") {
|
|
||||||
variant("prod") {
|
|
||||||
field("INVALID NAME", true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""".trimIndent())
|
|
||||||
|
|
||||||
val result = runCatching {
|
|
||||||
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE", "-Pkonfig.dimension.env=prod"))
|
|
||||||
}
|
|
||||||
|
|
||||||
assertTrue(result.isFailure)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Force regen ──────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
@Test fun `konfig force prevents task from being UP-TO-DATE on second run`() = withProject { dir, run ->
|
|
||||||
dir.resolve("build.gradle.kts").writeText("""
|
|
||||||
plugins { id("com.bitsycore.konfig") }
|
|
||||||
group = "com.example"
|
|
||||||
""".trimIndent())
|
|
||||||
|
|
||||||
val args = listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE", "-Pkonfig.force")
|
|
||||||
run(args)
|
|
||||||
val second = run(args)
|
|
||||||
|
|
||||||
assertFalse(
|
|
||||||
second.task(":generateKonfig")?.outcome == TaskOutcome.UP_TO_DATE,
|
|
||||||
"Expected generateKonfig to re-run when -Pkonfig.force is set, but it was UP_TO_DATE"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test fun `without konfig force task is UP-TO-DATE on second run`() = withProject { dir, run ->
|
|
||||||
dir.resolve("build.gradle.kts").writeText("""
|
|
||||||
plugins { id("com.bitsycore.konfig") }
|
|
||||||
group = "com.example"
|
|
||||||
""".trimIndent())
|
|
||||||
|
|
||||||
val args = listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE")
|
|
||||||
run(args)
|
|
||||||
val second = run(args)
|
|
||||||
|
|
||||||
assertEquals(
|
|
||||||
TaskOutcome.UP_TO_DATE,
|
|
||||||
second.task(":generateKonfig")?.outcome,
|
|
||||||
"Expected generateKonfig to be UP_TO_DATE on second run without -Pkonfig.force"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
package com.bitsycore.konfig
|
||||||
|
|
||||||
|
import org.gradle.testkit.runner.TaskOutcome
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertNotEquals
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Functional tests for `konfig.properties` file support:
|
||||||
|
* file-based variant selection, Gradle property priority override,
|
||||||
|
* and cache invalidation when the file changes.
|
||||||
|
*/
|
||||||
|
class KonfigPropertiesFunctionalTest : FunctionalTestBase() {
|
||||||
|
|
||||||
|
private fun buildScript() = """
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
dimension("env") {
|
||||||
|
variant("prod") { field("SERVER_URL", "https://prod.example.com") }
|
||||||
|
variant("dev") { field("SERVER_URL", "https://dev.example.com") }
|
||||||
|
variant("staging") { field("SERVER_URL", "https://staging.example.com") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
// ── Basic file reading ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `konfig_properties sets dimension variant`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle(buildScript())
|
||||||
|
dir.resolve("konfig.properties").writeText("konfig.dimension.env=dev\n")
|
||||||
|
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
|
||||||
|
val text = dir.generatedFile().readText()
|
||||||
|
assertTrue(text.contains("""const val VARIANT: String = "dev""""))
|
||||||
|
assertTrue(text.contains("""const val SERVER_URL: String = "https://dev.example.com""""))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `konfig_properties sets staging variant`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle(buildScript())
|
||||||
|
dir.resolve("konfig.properties").writeText("konfig.dimension.env=staging\n")
|
||||||
|
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
|
||||||
|
assertTrue(dir.generatedFile().readText()
|
||||||
|
.contains("""const val VARIANT: String = "staging""""))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Gradle property takes priority over file ──────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `Gradle property overrides konfig_properties`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle(buildScript())
|
||||||
|
dir.resolve("konfig.properties").writeText("konfig.dimension.env=dev\n")
|
||||||
|
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE", "-Pkonfig.dimension.env=prod"))
|
||||||
|
|
||||||
|
val text = dir.generatedFile().readText()
|
||||||
|
assertTrue(text.contains("""const val VARIANT: String = "prod""""))
|
||||||
|
assertTrue(text.contains("prod.example.com"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── No konfig.properties → falls through to defaultTo or skipped ─────────
|
||||||
|
|
||||||
|
@Test fun `without konfig_properties and no property dimension uses defaultTo`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
dimension("env", defaultTo = "prod") {
|
||||||
|
variant("prod") { field("SERVER_URL", "https://prod.example.com") }
|
||||||
|
variant("dev") { field("SERVER_URL", "https://dev.example.com") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
// no konfig.properties, no -P property
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
|
||||||
|
assertTrue(dir.generatedFile().readText()
|
||||||
|
.contains("""const val VARIANT: String = "prod""""))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Cache invalidation ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `task re-runs after konfig_properties file content changes`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle(buildScript())
|
||||||
|
val propsFile = dir.resolve("konfig.properties")
|
||||||
|
propsFile.writeText("konfig.dimension.env=dev\n")
|
||||||
|
|
||||||
|
val args = listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE")
|
||||||
|
run(args)
|
||||||
|
|
||||||
|
propsFile.writeText("konfig.dimension.env=prod\n")
|
||||||
|
val second = run(args)
|
||||||
|
|
||||||
|
assertNotEquals(TaskOutcome.UP_TO_DATE, second.task(":generateKonfig")?.outcome)
|
||||||
|
assertTrue(dir.generatedFile().readText().contains("""const val VARIANT: String = "prod""""))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `task is UP-TO-DATE when konfig_properties file is unchanged`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle(buildScript())
|
||||||
|
dir.resolve("konfig.properties").writeText("konfig.dimension.env=dev\n")
|
||||||
|
|
||||||
|
val args = listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE")
|
||||||
|
run(args)
|
||||||
|
assertEquals(TaskOutcome.UP_TO_DATE, run(args).task(":generateKonfig")?.outcome)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `adding konfig_properties after first run triggers re-execution`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle(buildScript())
|
||||||
|
// First run: no properties file, dimension skipped
|
||||||
|
val args = listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE")
|
||||||
|
run(args)
|
||||||
|
|
||||||
|
// Now add the file
|
||||||
|
dir.resolve("konfig.properties").writeText("konfig.dimension.env=prod\n")
|
||||||
|
val second = run(args)
|
||||||
|
|
||||||
|
assertNotEquals(TaskOutcome.UP_TO_DATE, second.task(":generateKonfig")?.outcome)
|
||||||
|
assertTrue(dir.generatedFile().readText().contains("""const val VARIANT: String = "prod""""))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Multiple dimensions in properties file ────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `konfig_properties can set multiple dimensions`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
dimension("env") {
|
||||||
|
variant("prod") { field("SERVER", "https://prod.example.com") }
|
||||||
|
variant("dev") { field("SERVER", "https://dev.example.com") }
|
||||||
|
}
|
||||||
|
dimension("region") {
|
||||||
|
variant("eu") { field("CDN", "https://eu-cdn.example.com") }
|
||||||
|
variant("us") { field("CDN", "https://us-cdn.example.com") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
dir.resolve("konfig.properties").writeText(
|
||||||
|
"konfig.dimension.env=dev\nkonfig.dimension.region=us\n"
|
||||||
|
)
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
|
||||||
|
val text = dir.generatedFile().readText()
|
||||||
|
assertTrue(text.contains("""const val SERVER: String = "https://dev.example.com""""))
|
||||||
|
assertTrue(text.contains("""const val CDN: String = "https://us-cdn.example.com""""))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,211 @@
|
|||||||
|
package com.bitsycore.konfig
|
||||||
|
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Functional tests for lifecycle logging and build-time validation errors:
|
||||||
|
* BUILD_TYPE source message, dimension resolution log, skipped-dimension log,
|
||||||
|
* unknown variant warning, invalid defaultTo error, invalid field names,
|
||||||
|
* and generation summary.
|
||||||
|
*/
|
||||||
|
class LoggingAndValidationFunctionalTest : FunctionalTestBase() {
|
||||||
|
|
||||||
|
// ── BUILD_TYPE lifecycle log ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `build type source is logged at lifecycle level`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {}
|
||||||
|
""")
|
||||||
|
val result = run(listOf("generateKonfig", "-Pkonfig.buildtype=DEBUG"))
|
||||||
|
assertTrue(result.output.contains("BUILD_TYPE = debug"))
|
||||||
|
assertTrue(result.output.contains("explicit property -Pkonfig.buildtype=DEBUG"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `build type defaults to release and logs reason`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {}
|
||||||
|
""")
|
||||||
|
// No -Pkonfig.buildtype → should fall back to RELEASE
|
||||||
|
val result = run(listOf("generateKonfig"))
|
||||||
|
assertTrue(result.output.contains("BUILD_TYPE = release"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Dimension resolution log ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `active dimension variant and reason are logged`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
dimension("env") {
|
||||||
|
variant("prod") { field("X", "y") }
|
||||||
|
variant("dev") { field("X", "z") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
val result = run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE", "-Pkonfig.dimension.env=dev"))
|
||||||
|
assertTrue(result.output.contains("dim 'env'"))
|
||||||
|
assertTrue(result.output.contains("'dev'"))
|
||||||
|
assertTrue(result.output.contains("konfig.dimension.env=dev"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Skipped dimension log ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `skipped dimension is logged at lifecycle level`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
dimension("env") {
|
||||||
|
variant("prod") { field("X", "y") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
val result = run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
assertTrue(result.output.contains("dim 'env'"))
|
||||||
|
assertTrue(result.output.contains("skipped"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Unknown variant warning ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `unknown variant in property emits a warning`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
dimension("env") {
|
||||||
|
variant("prod") { field("X", "y") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
val result = run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE", "-Pkonfig.dimension.env=typo"))
|
||||||
|
assertTrue(result.output.contains("typo"))
|
||||||
|
assertTrue(result.output.contains("not a known variant"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Generation summary ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `generation summary is logged with field and dimension counts`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
dimension("env", defaultTo = "prod") {
|
||||||
|
variant("prod") { field("URL", "https://prod.example.com") }
|
||||||
|
}
|
||||||
|
field("API_KEY", "abc123")
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
val result = run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
assertTrue(result.output.contains("generated BuildKonfig.kt"))
|
||||||
|
assertTrue(result.output.contains("1 global field"))
|
||||||
|
assertTrue(result.output.contains("1 dimension"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `generation summary says no global fields when none declared`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {}
|
||||||
|
""")
|
||||||
|
val result = run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
assertTrue(result.output.contains("no global fields"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `generation summary says no dimensions when none declared`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig { field("K", "v") }
|
||||||
|
""")
|
||||||
|
val result = run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
assertTrue(result.output.contains("no dimensions"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `generation summary counts multiple global fields correctly`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
field("A", "1")
|
||||||
|
field("B", "2")
|
||||||
|
field("C", "3")
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
val result = run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
assertTrue(result.output.contains("3 global field"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Validation errors ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `invalid global field name fails build`() = withFailingProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
field("INVALID NAME", "oops")
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
val result = run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
assertTrue(result.output.contains("INVALID NAME") || result.output.contains("not a valid Kotlin identifier"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `invalid field name starting with digit fails build`() = withFailingProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
field("1INVALID", "oops")
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
val result = run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
assertTrue(result.output.contains("1INVALID") || result.output.contains("not a valid Kotlin identifier"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `invalid field name in dimension variant fails build`() = withFailingProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
dimension("env") {
|
||||||
|
variant("prod") {
|
||||||
|
field("INVALID NAME", true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
val result = run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE", "-Pkonfig.dimension.env=prod"))
|
||||||
|
assertTrue(result.output.contains("INVALID NAME") || result.output.contains("not a valid Kotlin identifier"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `invalid defaultTo variant fails build with error message`() = withFailingProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
dimension("env", defaultTo = "nonexistent") {
|
||||||
|
variant("prod") { field("X", "y") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
val result = run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
assertTrue(result.output.contains("nonexistent"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `invalid objectName fails build`() = withFailingProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
objectName = "123Invalid"
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
val result = run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
assertTrue(result.output.contains("123Invalid") || result.output.contains("not a valid Kotlin identifier"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
package com.bitsycore.konfig
|
||||||
|
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertFalse
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Functional tests for object visibility and custom naming:
|
||||||
|
* [objectVisibility] (PUBLIC / INTERNAL), [objectName], [objectPackage],
|
||||||
|
* and the interaction between visibility and nested dimension objects.
|
||||||
|
*/
|
||||||
|
class VisibilityAndNamingFunctionalTest : FunctionalTestBase() {
|
||||||
|
|
||||||
|
// ── Default visibility (PUBLIC) ───────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `default visibility emits public object`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
assertTrue(dir.generatedFile().readText().contains("public object BuildKonfig {"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── INTERNAL visibility ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `INTERNAL visibility prefixes top-level object`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
objectVisibility = com.bitsycore.konfig.types.Visibility.INTERNAL
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
assertTrue(dir.generatedFile().readText().contains("internal object BuildKonfig {"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `INTERNAL visibility also prefixes nested dimension object`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
objectVisibility = com.bitsycore.konfig.types.Visibility.INTERNAL
|
||||||
|
dimension("env", defaultTo = "prod") {
|
||||||
|
variant("prod") { field("X", "y") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
assertTrue(dir.generatedFile().readText().contains("internal object Env"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `INTERNAL visibility does not appear twice for top-level object`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
objectVisibility = com.bitsycore.konfig.types.Visibility.INTERNAL
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
val text = dir.generatedFile().readText()
|
||||||
|
// "internal object BuildKonfig" must appear exactly once
|
||||||
|
val count = text.split("internal object BuildKonfig").size - 1
|
||||||
|
assertTrue(count == 1, "Expected exactly one 'internal object BuildKonfig', found $count")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Custom objectName ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `custom objectName replaces BuildKonfig`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
objectName = "AppConfig"
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
val text = dir.generatedFile().readText()
|
||||||
|
assertTrue(text.contains("object AppConfig {"))
|
||||||
|
assertFalse(text.contains("object BuildKonfig"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `custom objectName is reflected in generated file name`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
objectName = "Cfg"
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
val file = dir.generatedFile()
|
||||||
|
assertTrue(file.name == "Cfg.kt", "Expected file name 'Cfg.kt', got '${file.name}'")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `custom objectName combined with INTERNAL visibility`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
objectName = "MyConf"
|
||||||
|
objectVisibility = com.bitsycore.konfig.types.Visibility.INTERNAL
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
assertTrue(dir.generatedFile().readText().contains("internal object MyConf {"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Custom objectPackage ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `custom objectPackage overrides derived package`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
objectPackage = "org.myapp.generated"
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
assertTrue(dir.generatedFile().readText().contains("package org.myapp.generated"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `custom objectPackage is reflected in output directory path`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
objectPackage = "io.test.pkg"
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
val file = dir.generatedFile()
|
||||||
|
// The file's parent path should reflect the package segments
|
||||||
|
assertTrue(file.path.contains("io") && file.path.contains("test") && file.path.contains("pkg"),
|
||||||
|
"Expected package path in '${file.path}'")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── generation summary log ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `generation summary references correct object name`() = withProject { dir, run ->
|
||||||
|
dir.writeBuildGradle("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
konfig {
|
||||||
|
objectName = "MyConfig"
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
val result = run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
assertTrue(result.output.contains("MyConfig.kt"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.bitsycore.konfig
|
package com.bitsycore.konfig
|
||||||
|
|
||||||
|
import com.bitsycore.konfig.types.BuildType
|
||||||
|
import com.bitsycore.konfig.types.Visibility
|
||||||
import org.gradle.api.DefaultTask
|
import org.gradle.api.DefaultTask
|
||||||
import org.gradle.api.GradleException
|
import org.gradle.api.GradleException
|
||||||
import org.gradle.api.file.DirectoryProperty
|
import org.gradle.api.file.DirectoryProperty
|
||||||
@@ -156,7 +158,9 @@ abstract class GenerateKonfigTask : DefaultTask() {
|
|||||||
logger.info("konfig [$mod]: output -> ${outFile.absolutePath}")
|
logger.info("konfig [$mod]: output -> ${outFile.absolutePath}")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Validation ────────────────────────────────────────────────────────────
|
// ==============================================================================
|
||||||
|
// MARK: Validation
|
||||||
|
// ==============================================================================
|
||||||
|
|
||||||
private fun validate(mod: String, objName: String, pkg: String) {
|
private fun validate(mod: String, objName: String, pkg: String) {
|
||||||
val errors = mutableListOf<String>()
|
val errors = mutableListOf<String>()
|
||||||
@@ -193,7 +197,9 @@ abstract class GenerateKonfigTask : DefaultTask() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Logging ───────────────────────────────────────────────────────────────
|
// ==============================================================================
|
||||||
|
// MARK: Loggings
|
||||||
|
// ==============================================================================
|
||||||
|
|
||||||
private fun logResolution(mod: String, btVal: BuildType, objName: String, pkg: String) {
|
private fun logResolution(mod: String, btVal: BuildType, objName: String, pkg: String) {
|
||||||
logger.lifecycle("konfig [$mod]: BUILD_TYPE = ${btVal.name.lowercase()} (${buildTypeSource.get()})")
|
logger.lifecycle("konfig [$mod]: BUILD_TYPE = ${btVal.name.lowercase()} (${buildTypeSource.get()})")
|
||||||
@@ -223,7 +229,9 @@ abstract class GenerateKonfigTask : DefaultTask() {
|
|||||||
logger.info("konfig [$mod]: object = $pkg.$objName")
|
logger.info("konfig [$mod]: object = $pkg.$objName")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Code-generation helpers ───────────────────────────────────────────────
|
// ==============================================================================
|
||||||
|
// MARK: Helpers
|
||||||
|
// ==============================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Appends `const val` lines for each entry in [fields].
|
* Appends `const val` lines for each entry in [fields].
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
package com.bitsycore.konfig
|
package com.bitsycore.konfig
|
||||||
|
|
||||||
|
import com.bitsycore.konfig.configs.BuildTypedFieldDeclScope
|
||||||
|
import com.bitsycore.konfig.configs.DimensionConfig
|
||||||
|
import com.bitsycore.konfig.configs.FieldConfig
|
||||||
|
import com.bitsycore.konfig.configs.VariantConfig
|
||||||
|
import com.bitsycore.konfig.types.KonfigDsl
|
||||||
|
import com.bitsycore.konfig.types.Visibility
|
||||||
import org.gradle.api.file.DirectoryProperty
|
import org.gradle.api.file.DirectoryProperty
|
||||||
import org.gradle.api.model.ObjectFactory
|
import org.gradle.api.model.ObjectFactory
|
||||||
import org.gradle.api.provider.Property
|
import org.gradle.api.provider.Property
|
||||||
@@ -8,18 +14,20 @@ import javax.inject.Inject
|
|||||||
|
|
||||||
@KonfigDsl
|
@KonfigDsl
|
||||||
abstract class KonfigExtension @Inject constructor(
|
abstract class KonfigExtension @Inject constructor(
|
||||||
private val objects: ObjectFactory,
|
objects: ObjectFactory,
|
||||||
) {
|
) {
|
||||||
// ── Backing Gradle properties (used by the plugin for lazy wiring) ────────
|
|
||||||
|
// ==============================================================================
|
||||||
|
// MARK: Settings Internal
|
||||||
|
// ==============================================================================
|
||||||
|
|
||||||
internal val objectPackageProp: Property<String> = objects.property(String::class.java)
|
internal val objectPackageProp: Property<String> = objects.property(String::class.java)
|
||||||
internal val objectNameProp: Property<String> = objects.property(String::class.java)
|
internal val objectNameProp: Property<String> = objects.property(String::class.java)
|
||||||
internal val objectVisibilityProp: Property<Visibility> = objects.property(Visibility::class.java)
|
internal val objectVisibilityProp: Property<Visibility> = objects.property(Visibility::class.java)
|
||||||
|
|
||||||
/** Output directory — kept as [DirectoryProperty] for full Gradle lazy semantics. */
|
// ==============================================================================
|
||||||
val outputDir: DirectoryProperty = objects.directoryProperty()
|
// MARK: User facing Settings
|
||||||
|
// ==============================================================================
|
||||||
// ── User-facing var properties ────────────────────────────────────────────
|
|
||||||
|
|
||||||
/** Package for the generated object (e.g. `"com.example.app"`). */
|
/** Package for the generated object (e.g. `"com.example.app"`). */
|
||||||
var objectPackage: String
|
var objectPackage: String
|
||||||
@@ -36,18 +44,25 @@ abstract class KonfigExtension @Inject constructor(
|
|||||||
get() = objectVisibilityProp.get()
|
get() = objectVisibilityProp.get()
|
||||||
set(value) = objectVisibilityProp.set(value)
|
set(value) = objectVisibilityProp.set(value)
|
||||||
|
|
||||||
// ── DSL internals ─────────────────────────────────────────────────────────
|
/** Output directory — kept as [DirectoryProperty] for full Gradle lazy semantics. */
|
||||||
|
val outputDir: DirectoryProperty = objects.directoryProperty()
|
||||||
|
|
||||||
|
// ==============================================================================
|
||||||
|
// MARK: DSL Internal
|
||||||
|
// ==============================================================================
|
||||||
|
|
||||||
@PublishedApi
|
@PublishedApi
|
||||||
internal val dimensions: MutableList<DimensionConfig> = mutableListOf()
|
internal val dimensions: MutableList<DimensionConfig> = mutableListOf()
|
||||||
|
|
||||||
/** Backing store for global fields — reuses [VariantConfig] for its field/debug/release logic. */
|
/** Backing store for global fields — reuses [com.bitsycore.konfig.configs.VariantConfig] for its field/debug/release logic. */
|
||||||
@PublishedApi
|
@PublishedApi
|
||||||
internal val globalScope: VariantConfig = VariantConfig("\$global")
|
internal val globalScope: VariantConfig = VariantConfig("\$global")
|
||||||
|
|
||||||
internal val globalFields: List<FieldConfig<*>> get() = globalScope.fields
|
internal val globalFields: List<FieldConfig<*>> get() = globalScope.fields
|
||||||
|
|
||||||
// ─── Dimension DSL ────────────────────────────────────────────────────────
|
// ==============================================================================
|
||||||
|
// MARK: Top Level DSL
|
||||||
|
// ==============================================================================
|
||||||
|
|
||||||
fun dimension(
|
fun dimension(
|
||||||
name: String,
|
name: String,
|
||||||
@@ -63,8 +78,6 @@ abstract class KonfigExtension @Inject constructor(
|
|||||||
dimensions.add(d)
|
dimensions.add(d)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Global field DSL — delegates to globalScope ──────────────────────────
|
|
||||||
|
|
||||||
inline fun <reified T : Any> field(
|
inline fun <reified T : Any> field(
|
||||||
name: String,
|
name: String,
|
||||||
default: T,
|
default: T,
|
||||||
|
|||||||
@@ -3,6 +3,10 @@
|
|||||||
package com.bitsycore.konfig
|
package com.bitsycore.konfig
|
||||||
|
|
||||||
import com.android.build.api.dsl.CommonExtension
|
import com.android.build.api.dsl.CommonExtension
|
||||||
|
import com.bitsycore.konfig.configs.DimensionConfig
|
||||||
|
import com.bitsycore.konfig.configs.FieldConfig
|
||||||
|
import com.bitsycore.konfig.types.BuildType
|
||||||
|
import com.bitsycore.konfig.types.Visibility
|
||||||
import org.gradle.api.Plugin
|
import org.gradle.api.Plugin
|
||||||
import org.gradle.api.Project
|
import org.gradle.api.Project
|
||||||
import org.gradle.api.provider.Provider
|
import org.gradle.api.provider.Provider
|
||||||
@@ -360,7 +364,9 @@ class KonfigPlugin : Plugin<Project> {
|
|||||||
return field.fieldName to encoded
|
return field.fieldName to encoded
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Merge helpers ─────────────────────────────────────────────────────────
|
// ==============================================================================
|
||||||
|
// MARK: Fields Merge
|
||||||
|
// ==============================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a merged list of fields: [commonFields] as the base, with any field in
|
* Returns a merged list of fields: [commonFields] as the base, with any field in
|
||||||
@@ -375,7 +381,9 @@ class KonfigPlugin : Plugin<Project> {
|
|||||||
return commonFields.filter { it.fieldName !in variantNames } + variantFields
|
return commonFields.filter { it.fieldName !in variantNames } + variantFields
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Package-name helpers ──────────────────────────────────────────────────
|
// ==============================================================================
|
||||||
|
// MARK: Package Utilities
|
||||||
|
// ==============================================================================
|
||||||
|
|
||||||
private fun defaultPackageName(projectName: String, projectGroup: String): String {
|
private fun defaultPackageName(projectName: String, projectGroup: String): String {
|
||||||
val group = projectGroup
|
val group = projectGroup
|
||||||
|
|||||||
+7
-3
@@ -1,4 +1,6 @@
|
|||||||
package com.bitsycore.konfig
|
package com.bitsycore.konfig.configs
|
||||||
|
|
||||||
|
import com.bitsycore.konfig.types.KonfigDsl
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DSL scope for a dimension declaration.
|
* DSL scope for a dimension declaration.
|
||||||
@@ -35,9 +37,11 @@ class DimensionConfig @PublishedApi internal constructor(
|
|||||||
* Shared fields that apply to **all** variants as a fallback.
|
* Shared fields that apply to **all** variants as a fallback.
|
||||||
* A variant field with the same name takes precedence over a common field.
|
* A variant field with the same name takes precedence over a common field.
|
||||||
*/
|
*/
|
||||||
internal val commonConfig: VariantConfig = VariantConfig("\$common")
|
internal val commonConfig: VariantConfig = VariantConfig($$"$common")
|
||||||
|
|
||||||
// ── DSL ──────────────────────────────────────────────────────────────────
|
// ==============================================================================
|
||||||
|
// MARK: DSL
|
||||||
|
// ==============================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Declares a named variant. Can be called multiple times for the same name
|
* Declares a named variant. Can be called multiple times for the same name
|
||||||
+9
-5
@@ -1,6 +1,10 @@
|
|||||||
package com.bitsycore.konfig
|
package com.bitsycore.konfig.configs
|
||||||
|
|
||||||
|
import com.bitsycore.konfig.types.BuildType
|
||||||
|
import org.gradle.api.Transformer
|
||||||
import org.gradle.api.provider.Provider
|
import org.gradle.api.provider.Provider
|
||||||
|
import org.gradle.api.specs.Spec
|
||||||
|
import java.util.function.BiFunction
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Holds the resolved configuration for a single generated field.
|
* Holds the resolved configuration for a single generated field.
|
||||||
@@ -53,14 +57,14 @@ private class ConstantProvider<T : Any>(private val value: T) : Provider<T> {
|
|||||||
override fun getOrElse(defaultValue: T): T = value
|
override fun getOrElse(defaultValue: T): T = value
|
||||||
override fun isPresent(): Boolean = true
|
override fun isPresent(): Boolean = true
|
||||||
|
|
||||||
override fun <S : Any> map(transformer: org.gradle.api.Transformer<out S?, in T>): Provider<S> =
|
override fun <S : Any> map(transformer: Transformer<out S?, in T>): Provider<S> =
|
||||||
ConstantProvider(transformer.transform(value)!!)
|
ConstantProvider(transformer.transform(value)!!)
|
||||||
|
|
||||||
override fun <S : Any> flatMap(transformer: org.gradle.api.Transformer<out Provider<out S>?, in T>): Provider<S> =
|
override fun <S : Any> flatMap(transformer: Transformer<out Provider<out S>?, in T>): Provider<S> =
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
(transformer.transform(value) as Provider<S>)
|
(transformer.transform(value) as Provider<S>)
|
||||||
|
|
||||||
override fun filter(spec: org.gradle.api.specs.Spec<in T>): Provider<T> =
|
override fun filter(spec: Spec<in T>): Provider<T> =
|
||||||
if (spec.isSatisfiedBy(value)) this else throw NoSuchElementException("Provider has no value")
|
if (spec.isSatisfiedBy(value)) this else throw NoSuchElementException("Provider has no value")
|
||||||
|
|
||||||
override fun orElse(value: T): Provider<T> = this
|
override fun orElse(value: T): Provider<T> = this
|
||||||
@@ -71,6 +75,6 @@ private class ConstantProvider<T : Any>(private val value: T) : Provider<T> {
|
|||||||
|
|
||||||
override fun <B : Any, R : Any> zip(
|
override fun <B : Any, R : Any> zip(
|
||||||
right: Provider<B>,
|
right: Provider<B>,
|
||||||
combiner: java.util.function.BiFunction<in T, in B, out R?>
|
combiner: BiFunction<in T, in B, out R?>
|
||||||
): Provider<R> = ConstantProvider(combiner.apply(value, right.get())!!)
|
): Provider<R> = ConstantProvider(combiner.apply(value, right.get())!!)
|
||||||
}
|
}
|
||||||
+16
-9
@@ -1,8 +1,12 @@
|
|||||||
package com.bitsycore.konfig
|
package com.bitsycore.konfig.configs
|
||||||
|
|
||||||
|
import com.bitsycore.konfig.types.BuildType
|
||||||
|
import com.bitsycore.konfig.types.KonfigDsl
|
||||||
import org.gradle.api.provider.Provider
|
import org.gradle.api.provider.Provider
|
||||||
|
|
||||||
// ── Fluent handle returned by top-level field() declarations ─────────────────
|
// ==============================================================================
|
||||||
|
// MARK: FieldHandle with modifier
|
||||||
|
// ==============================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returned by `field(name, default)` when called at the top level of a [VariantConfig].
|
* Returned by `field(name, default)` when called at the top level of a [VariantConfig].
|
||||||
@@ -14,17 +18,18 @@ import org.gradle.api.provider.Provider
|
|||||||
* ```
|
* ```
|
||||||
* Not available inside `debug {}` / `release {}` blocks — those return [Unit].
|
* Not available inside `debug {}` / `release {}` blocks — those return [Unit].
|
||||||
*/
|
*/
|
||||||
@KonfigDsl
|
|
||||||
class FieldHandle<T : Any> @PublishedApi internal constructor(
|
class FieldHandle<T : Any> @PublishedApi internal constructor(
|
||||||
@PublishedApi internal val field: FieldConfig<T>
|
@PublishedApi internal val field: FieldConfig<T>
|
||||||
) {
|
) {
|
||||||
fun debug(value: T): Unit { field.debug(value) }
|
fun debug(value: T): Unit = field.debug(value)
|
||||||
fun debug(value: Provider<T>): Unit { field.debug(value) }
|
fun debug(value: Provider<T>): Unit = field.debug(value)
|
||||||
fun release(value: T): Unit { field.release(value) }
|
fun release(value: T): Unit = field.release(value)
|
||||||
fun release(value: Provider<T>): Unit { field.release(value) }
|
fun release(value: Provider<T>): Unit = field.release(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Scope for fields declared inside debug {} / release {} blocks ─────────────
|
// ==============================================================================
|
||||||
|
// MARK: BuildType Scope
|
||||||
|
// ==============================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Receiver of `debug { ... }` and `release { ... }` blocks inside [VariantConfig].
|
* Receiver of `debug { ... }` and `release { ... }` blocks inside [VariantConfig].
|
||||||
@@ -46,7 +51,9 @@ class BuildTypedFieldDeclScope @PublishedApi internal constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Main variant / common scope ───────────────────────────────────────────────
|
// ==============================================================================
|
||||||
|
// MARK: Variant
|
||||||
|
// ==============================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DSL scope for a variant (or the `common {}` block inside a dimension).
|
* DSL scope for a variant (or the `common {}` block inside a dimension).
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package com.bitsycore.konfig
|
package com.bitsycore.konfig.types
|
||||||
|
|
||||||
import kotlin.enums.enumEntries
|
import kotlin.enums.enumEntries
|
||||||
|
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package com.bitsycore.konfig
|
package com.bitsycore.konfig.types
|
||||||
|
|
||||||
@DslMarker
|
@DslMarker
|
||||||
annotation class KonfigDsl
|
annotation class KonfigDsl
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package com.bitsycore.konfig
|
package com.bitsycore.konfig.types
|
||||||
|
|
||||||
enum class Visibility {
|
enum class Visibility {
|
||||||
PUBLIC, INTERNAL
|
PUBLIC, INTERNAL
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
package com.bitsycore.konfig
|
||||||
|
|
||||||
|
import com.bitsycore.konfig.types.BuildType
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertNull
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full coverage for [BuildType.resolve].
|
||||||
|
* Covers exact enum-name matching, lowercase keywords, typical Android / KMP task
|
||||||
|
* names, ambiguous inputs, false-positive substring guards, edge cases, and the
|
||||||
|
* [BuildType.value] property.
|
||||||
|
*/
|
||||||
|
class BuildTypeExtendedTest {
|
||||||
|
|
||||||
|
// ── Enum name resolution ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `exact uppercase DEBUG resolves to DEBUG`() {
|
||||||
|
assertEquals(BuildType.DEBUG, BuildType.resolve("DEBUG"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `exact uppercase RELEASE resolves to RELEASE`() {
|
||||||
|
assertEquals(BuildType.RELEASE, BuildType.resolve("RELEASE"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `mixed case DeBuG does not match enum name and falls through to regex`() {
|
||||||
|
// Not an exact enum name match; regex will catch it via "Debug" pattern
|
||||||
|
assertNull(BuildType.resolve("DeBuG"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Lowercase keyword ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `standalone lowercase debug resolves DEBUG`() {
|
||||||
|
assertEquals(BuildType.DEBUG, BuildType.resolve("debug"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `standalone lowercase release resolves RELEASE`() {
|
||||||
|
assertEquals(BuildType.RELEASE, BuildType.resolve("release"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Typical Android task names ───────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `assembleDebug resolves DEBUG`() {
|
||||||
|
assertEquals(BuildType.DEBUG, BuildType.resolve("assembleDebug"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `assembleRelease resolves RELEASE`() {
|
||||||
|
assertEquals(BuildType.RELEASE, BuildType.resolve("assembleRelease"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `compileDebugKotlin resolves DEBUG`() {
|
||||||
|
assertEquals(BuildType.DEBUG, BuildType.resolve("compileDebugKotlin"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `compileReleaseKotlin resolves RELEASE`() {
|
||||||
|
assertEquals(BuildType.RELEASE, BuildType.resolve("compileReleaseKotlin"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `bundleReleaseAar resolves RELEASE`() {
|
||||||
|
assertEquals(BuildType.RELEASE, BuildType.resolve("bundleReleaseAar"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `generateDebugSources resolves DEBUG`() {
|
||||||
|
assertEquals(BuildType.DEBUG, BuildType.resolve("generateDebugSources"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `processDebugResources resolves DEBUG`() {
|
||||||
|
assertEquals(BuildType.DEBUG, BuildType.resolve("processDebugResources"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `processReleaseResources resolves RELEASE`() {
|
||||||
|
assertEquals(BuildType.RELEASE, BuildType.resolve("processReleaseResources"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `mergeDebugAssets resolves DEBUG`() {
|
||||||
|
assertEquals(BuildType.DEBUG, BuildType.resolve("mergeDebugAssets"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `mergeReleaseAssets resolves RELEASE`() {
|
||||||
|
assertEquals(BuildType.RELEASE, BuildType.resolve("mergeReleaseAssets"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `packageDebug resolves DEBUG`() {
|
||||||
|
assertEquals(BuildType.DEBUG, BuildType.resolve("packageDebug"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `packageRelease resolves RELEASE`() {
|
||||||
|
assertEquals(BuildType.RELEASE, BuildType.resolve("packageRelease"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── KMP / JVM task names ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `compileKotlinJvm returns null`() {
|
||||||
|
assertNull(BuildType.resolve("compileKotlinJvm"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `compileKotlin returns null`() {
|
||||||
|
assertNull(BuildType.resolve("compileKotlin"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `jar returns null`() {
|
||||||
|
assertNull(BuildType.resolve("jar"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `test returns null`() {
|
||||||
|
assertNull(BuildType.resolve("test"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Ambiguous task names (both keywords present) ─────────────────────────
|
||||||
|
|
||||||
|
@Test fun `debugRelease is ambiguous returns null`() {
|
||||||
|
assertNull(BuildType.resolve("debugRelease"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `releaseDebug is ambiguous returns null`() {
|
||||||
|
assertNull(BuildType.resolve("releaseDebug"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `compileDebugReleaseKotlin is ambiguous returns null`() {
|
||||||
|
assertNull(BuildType.resolve("compileDebugReleaseKotlin"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── False-positive guards (words that contain debug/release as substring) ─
|
||||||
|
|
||||||
|
@Test fun `debuggable does not resolve DEBUG`() {
|
||||||
|
// "debuggable" has lowercase 'g' immediately after "debug" → regex rejects it
|
||||||
|
assertNull(BuildType.resolve("debuggable"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `prereleased does not resolve RELEASE`() {
|
||||||
|
// "release" is preceded by lowercase 'e' in "prere[lease]" — but "release"
|
||||||
|
// starts after "prere", let's verify actual behaviour via the regex:
|
||||||
|
// lookbehind (?<![a-z]) fails because char before 'r' of "release" is 'e'
|
||||||
|
assertNull(BuildType.resolve("prereleased"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `released does not resolve RELEASE`() {
|
||||||
|
// "released" — 'd' after "release" is NOT a lowercase letter... wait,
|
||||||
|
// actually the regex checks (?![a-z]) so 'd' fails the lookahead.
|
||||||
|
assertNull(BuildType.resolve("released"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `debugMode resolves DEBUG`() {
|
||||||
|
// 'M' after "debug" — uppercase, not [a-z], so lookahead passes
|
||||||
|
assertEquals(BuildType.DEBUG, BuildType.resolve("debugMode"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Empty / whitespace ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `empty string returns null`() {
|
||||||
|
assertNull(BuildType.resolve(""))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `blank string returns null`() {
|
||||||
|
assertNull(BuildType.resolve(" "))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Value property ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `DEBUG value is debug`() {
|
||||||
|
assertEquals("debug", BuildType.DEBUG.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `RELEASE value is release`() {
|
||||||
|
assertEquals("release", BuildType.RELEASE.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Enum entries count ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `exactly two build types exist`() {
|
||||||
|
assertEquals(2, BuildType.entries.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
package com.bitsycore.konfig
|
||||||
|
|
||||||
|
import com.bitsycore.konfig.configs.BuildTypedFieldDeclScope
|
||||||
|
import com.bitsycore.konfig.configs.VariantConfig
|
||||||
|
import com.bitsycore.konfig.configs.constantProvider
|
||||||
|
import com.bitsycore.konfig.types.BuildType
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertNull
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unit tests for [BuildTypedFieldDeclScope].
|
||||||
|
*
|
||||||
|
* This scope is the receiver inside `debug { }` / `release { }` blocks on a
|
||||||
|
* [VariantConfig]. It pins the [BuildType] and delegates to
|
||||||
|
* [VariantConfig.getOrCreateField] so overrides land on the correct entry.
|
||||||
|
*/
|
||||||
|
class BuildTypedFieldDeclScopeTest {
|
||||||
|
|
||||||
|
private fun makeScope(buildType: BuildType, variantName: String = "v"): Pair<BuildTypedFieldDeclScope, VariantConfig> {
|
||||||
|
val owner = VariantConfig(variantName)
|
||||||
|
val scope = BuildTypedFieldDeclScope(buildType, owner)
|
||||||
|
return scope to owner
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── buildType is stored ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `DEBUG scope stores DEBUG build type`() {
|
||||||
|
val (scope, _) = makeScope(BuildType.DEBUG)
|
||||||
|
assertEquals(BuildType.DEBUG, scope.buildType)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `RELEASE scope stores RELEASE build type`() {
|
||||||
|
val (scope, _) = makeScope(BuildType.RELEASE)
|
||||||
|
assertEquals(BuildType.RELEASE, scope.buildType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── owner is stored ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `owner is the supplied VariantConfig`() {
|
||||||
|
val owner = VariantConfig("myVariant")
|
||||||
|
val scope = BuildTypedFieldDeclScope(BuildType.DEBUG, owner)
|
||||||
|
assertEquals(owner, scope.owner)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── field(name, literal) in DEBUG scope ──────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `field in DEBUG scope creates override for DEBUG`() {
|
||||||
|
val (scope, owner) = makeScope(BuildType.DEBUG)
|
||||||
|
scope.field("LOG", "verbose")
|
||||||
|
val fc = owner.fields.first { it.fieldName == "LOG" }
|
||||||
|
assertEquals("verbose", fc.resolve(BuildType.DEBUG)!!.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `field in DEBUG scope does not set RELEASE override`() {
|
||||||
|
val (scope, owner) = makeScope(BuildType.DEBUG)
|
||||||
|
scope.field("LOG", "verbose")
|
||||||
|
val fc = owner.fields.first { it.fieldName == "LOG" }
|
||||||
|
assertNull(fc.resolve(BuildType.RELEASE))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── field(name, literal) in RELEASE scope ────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `field in RELEASE scope creates override for RELEASE`() {
|
||||||
|
val (scope, owner) = makeScope(BuildType.RELEASE)
|
||||||
|
scope.field("LOG", "error")
|
||||||
|
val fc = owner.fields.first { it.fieldName == "LOG" }
|
||||||
|
assertEquals("error", fc.resolve(BuildType.RELEASE)!!.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `field in RELEASE scope does not set DEBUG override`() {
|
||||||
|
val (scope, owner) = makeScope(BuildType.RELEASE)
|
||||||
|
scope.field("LOG", "error")
|
||||||
|
val fc = owner.fields.first { it.fieldName == "LOG" }
|
||||||
|
assertNull(fc.resolve(BuildType.DEBUG))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── field(name, provider) ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `field with provider in DEBUG scope stores provider`() {
|
||||||
|
val (scope, owner) = makeScope(BuildType.DEBUG)
|
||||||
|
scope.field("KEY", constantProvider("devKey"))
|
||||||
|
val fc = owner.fields.first { it.fieldName == "KEY" }
|
||||||
|
assertEquals("devKey", fc.resolve(BuildType.DEBUG)!!.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `field with provider in RELEASE scope stores provider`() {
|
||||||
|
val (scope, owner) = makeScope(BuildType.RELEASE)
|
||||||
|
scope.field("KEY", constantProvider("prodKey"))
|
||||||
|
val fc = owner.fields.first { it.fieldName == "KEY" }
|
||||||
|
assertEquals("prodKey", fc.resolve(BuildType.RELEASE)!!.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── getOrCreateField reuse ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `same field name in debug and release scopes shares one FieldConfig`() {
|
||||||
|
val owner = VariantConfig("v")
|
||||||
|
val dbgScope = BuildTypedFieldDeclScope(BuildType.DEBUG, owner)
|
||||||
|
val relScope = BuildTypedFieldDeclScope(BuildType.RELEASE, owner)
|
||||||
|
dbgScope.field("LOG", "verbose")
|
||||||
|
relScope.field("LOG", "error")
|
||||||
|
// Only one FieldConfig should exist
|
||||||
|
assertEquals(1, owner.fields.size)
|
||||||
|
val fc = owner.fields[0]
|
||||||
|
assertEquals("verbose", fc.resolve(BuildType.DEBUG)!!.get())
|
||||||
|
assertEquals("error", fc.resolve(BuildType.RELEASE)!!.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Multiple fields in one scope block ───────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `multiple fields in debug scope are all created`() {
|
||||||
|
val (scope, owner) = makeScope(BuildType.DEBUG)
|
||||||
|
scope.field("A", "alpha")
|
||||||
|
scope.field("B", 42)
|
||||||
|
scope.field("C", true)
|
||||||
|
assertEquals(3, owner.fields.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `multiple fields in release scope are independent`() {
|
||||||
|
val (scope, owner) = makeScope(BuildType.RELEASE)
|
||||||
|
scope.field("X", 100)
|
||||||
|
scope.field("Y", 200)
|
||||||
|
assertEquals(100, owner.fields.first { it.fieldName == "X" }.resolve(BuildType.RELEASE)!!.get())
|
||||||
|
assertEquals(200, owner.fields.first { it.fieldName == "Y" }.resolve(BuildType.RELEASE)!!.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Int / Boolean / Long scoped fields ───────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `Int field in debug scope resolves correctly`() {
|
||||||
|
val (scope, owner) = makeScope(BuildType.DEBUG)
|
||||||
|
scope.field("TIMEOUT", 5)
|
||||||
|
assertEquals(5, owner.fields[0].resolve(BuildType.DEBUG)!!.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `Boolean field in release scope resolves correctly`() {
|
||||||
|
val (scope, owner) = makeScope(BuildType.RELEASE)
|
||||||
|
scope.field("FLAG", false)
|
||||||
|
assertEquals(false, owner.fields[0].resolve(BuildType.RELEASE)!!.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `Long field in debug scope resolves correctly`() {
|
||||||
|
val (scope, owner) = makeScope(BuildType.DEBUG)
|
||||||
|
scope.field("BIG", 123_456_789_000L)
|
||||||
|
assertEquals(123_456_789_000L, owner.fields[0].resolve(BuildType.DEBUG)!!.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Scope used via VariantConfig.debug{} / release{} ─────────────────────
|
||||||
|
|
||||||
|
@Test fun `VariantConfig debug block reaches correct scope`() {
|
||||||
|
val v = VariantConfig("test")
|
||||||
|
v.debug { field("M", "debugMsg") }
|
||||||
|
assertEquals("debugMsg", v.fields.first { it.fieldName == "M" }.resolve(BuildType.DEBUG)!!.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `VariantConfig release block reaches correct scope`() {
|
||||||
|
val v = VariantConfig("test")
|
||||||
|
v.release { field("M", "releaseMsg") }
|
||||||
|
assertEquals("releaseMsg", v.fields.first { it.fieldName == "M" }.resolve(BuildType.RELEASE)!!.get())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
package com.bitsycore.konfig
|
||||||
|
|
||||||
|
import com.bitsycore.konfig.configs.constantProvider
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertNotNull
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for [constantProvider] and the internal [ConstantProvider] it creates.
|
||||||
|
*
|
||||||
|
* The [ConstantProvider] class is private; we access its behaviour through the
|
||||||
|
* public [constantProvider] factory and the [org.gradle.api.provider.Provider] interface.
|
||||||
|
*/
|
||||||
|
class ConstantProviderTest {
|
||||||
|
|
||||||
|
// ── get / getOrNull / isPresent ──────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `String provider get returns value`() {
|
||||||
|
val p = constantProvider("hello")
|
||||||
|
assertEquals("hello", p.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `Int provider get returns value`() {
|
||||||
|
val p = constantProvider(42)
|
||||||
|
assertEquals(42, p.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `Boolean provider get returns true`() {
|
||||||
|
val p = constantProvider(true)
|
||||||
|
assertEquals(true, p.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `Boolean provider get returns false`() {
|
||||||
|
val p = constantProvider(false)
|
||||||
|
assertEquals(false, p.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `Long provider get returns value`() {
|
||||||
|
val p = constantProvider(Long.MAX_VALUE)
|
||||||
|
assertEquals(Long.MAX_VALUE, p.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `Double provider get returns value`() {
|
||||||
|
val p = constantProvider(3.14)
|
||||||
|
assertEquals(3.14, p.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `Float provider get returns value`() {
|
||||||
|
val p = constantProvider(1.5f)
|
||||||
|
assertEquals(1.5f, p.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `isPresent is always true`() {
|
||||||
|
assertTrue(constantProvider("anything").isPresent)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `getOrNull returns value not null`() {
|
||||||
|
val p = constantProvider("value")
|
||||||
|
assertNotNull(p.getOrNull())
|
||||||
|
assertEquals("value", p.getOrNull())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `getOrElse returns stored value ignoring default`() {
|
||||||
|
val p = constantProvider("stored")
|
||||||
|
assertEquals("stored", p.getOrElse("fallback"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── map ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `map transforms value`() {
|
||||||
|
val p = constantProvider("hello")
|
||||||
|
val mapped = p.map { it.uppercase() }
|
||||||
|
assertEquals("HELLO", mapped.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `map on Int transforms to String`() {
|
||||||
|
val p = constantProvider(7)
|
||||||
|
val mapped = p.map { "num=$it" }
|
||||||
|
assertEquals("num=7", mapped.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `nested map chains correctly`() {
|
||||||
|
val p = constantProvider(2)
|
||||||
|
val result = p.map { it * 3 }.map { it + 1 }
|
||||||
|
assertEquals(7, result.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── flatMap ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `flatMap returns inner provider value`() {
|
||||||
|
val p = constantProvider("base")
|
||||||
|
val result = p.flatMap { constantProvider("flat:$it") }
|
||||||
|
assertEquals("flat:base", result.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── filter ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `filter passes when spec is satisfied`() {
|
||||||
|
val p = constantProvider(10)
|
||||||
|
val filtered = p.filter { it > 5 }
|
||||||
|
assertEquals(10, filtered.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `filter throws when spec is not satisfied`() {
|
||||||
|
val p = constantProvider(3)
|
||||||
|
var threw = false
|
||||||
|
try {
|
||||||
|
p.filter { it > 5 }.get()
|
||||||
|
} catch (_: NoSuchElementException) {
|
||||||
|
threw = true
|
||||||
|
}
|
||||||
|
assertTrue(threw)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── orElse ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `orElse with value returns stored value`() {
|
||||||
|
val p = constantProvider("original")
|
||||||
|
assertEquals("original", p.orElse("other").get())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `orElse with provider returns stored value`() {
|
||||||
|
val p = constantProvider("original")
|
||||||
|
assertEquals("original", p.orElse(constantProvider("other")).get())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── zip ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `zip combines two providers`() {
|
||||||
|
val p1 = constantProvider("Hello")
|
||||||
|
val p2 = constantProvider("World")
|
||||||
|
val zipped = p1.zip(p2) { a, b -> "$a $b" }
|
||||||
|
assertEquals("Hello World", zipped.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `zip with Int providers produces sum`() {
|
||||||
|
val p1 = constantProvider(3)
|
||||||
|
val p2 = constantProvider(4)
|
||||||
|
val sum = p1.zip(p2) { a, b -> a + b }
|
||||||
|
assertEquals(7, sum.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── identity / equality ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `two providers with same value each return that value independently`() {
|
||||||
|
val p1 = constantProvider("same")
|
||||||
|
val p2 = constantProvider("same")
|
||||||
|
assertEquals(p1.get(), p2.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `provider wrapping empty string returns empty string`() {
|
||||||
|
val p = constantProvider("")
|
||||||
|
assertEquals("", p.get())
|
||||||
|
assertTrue(p.isPresent)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
package com.bitsycore.konfig
|
||||||
|
|
||||||
|
import com.bitsycore.konfig.configs.DimensionConfig
|
||||||
|
import com.bitsycore.konfig.types.BuildType
|
||||||
|
import kotlin.test.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unit tests for [DimensionConfig].
|
||||||
|
*
|
||||||
|
* Covers:
|
||||||
|
* - Construction: dimensionName, objectNameOverride, defaultVariant
|
||||||
|
* - objectName() derivation (camelCase, override, separators)
|
||||||
|
* - variant() DSL: creation, retrieval, merging
|
||||||
|
* - common() block: shared fallback fields
|
||||||
|
* - Interaction between common and variant fields (variant wins)
|
||||||
|
*/
|
||||||
|
class DimensionConfigTest {
|
||||||
|
|
||||||
|
private fun makeDim(
|
||||||
|
name: String,
|
||||||
|
objectNameOverride: String? = null,
|
||||||
|
defaultTo: String? = null,
|
||||||
|
) = DimensionConfig(name, objectNameOverride, defaultTo)
|
||||||
|
|
||||||
|
// ── Construction ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `dimensionName is stored`() {
|
||||||
|
val d = makeDim("env")
|
||||||
|
assertEquals("env", d.dimensionName)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `objectNameOverride is stored when provided`() {
|
||||||
|
val d = makeDim("env", objectNameOverride = "Environment")
|
||||||
|
assertEquals("Environment", d.objectNameOverride)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `objectNameOverride is null when not provided`() {
|
||||||
|
val d = makeDim("env")
|
||||||
|
assertNull(d.objectNameOverride)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `defaultVariant is stored`() {
|
||||||
|
val d = makeDim("env", defaultTo = "prod")
|
||||||
|
assertEquals("prod", d.defaultVariant)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `defaultVariant is null when not provided`() {
|
||||||
|
val d = makeDim("env")
|
||||||
|
assertNull(d.defaultVariant)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── objectName() ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `objectName returns override when set`() {
|
||||||
|
val d = makeDim("env", objectNameOverride = "MyEnv")
|
||||||
|
assertEquals("MyEnv", d.objectName())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `objectName capitalises simple lowercase name`() {
|
||||||
|
val d = makeDim("env")
|
||||||
|
assertEquals("Env", d.objectName())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `objectName capitalises each part split by dash`() {
|
||||||
|
val d = makeDim("build-env")
|
||||||
|
assertEquals("BuildEnv", d.objectName())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `objectName capitalises each part split by underscore`() {
|
||||||
|
val d = makeDim("my_env")
|
||||||
|
assertEquals("MyEnv", d.objectName())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `objectName capitalises each part split by space`() {
|
||||||
|
val d = makeDim("my env")
|
||||||
|
assertEquals("MyEnv", d.objectName())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `objectName handles mixed separators`() {
|
||||||
|
val d = makeDim("build_my-env")
|
||||||
|
assertEquals("BuildMyEnv", d.objectName())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `objectName for single uppercase word keeps first letter uppercase`() {
|
||||||
|
val d = makeDim("REGION")
|
||||||
|
assertEquals("Region", d.objectName())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `objectName for already camel-case single word`() {
|
||||||
|
val d = makeDim("region")
|
||||||
|
assertEquals("Region", d.objectName())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── variant() ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `variant creates new VariantConfig`() {
|
||||||
|
val d = makeDim("env")
|
||||||
|
d.variant("prod") { field("URL", "https://prod.example.com") }
|
||||||
|
assertEquals(1, d.variants.size)
|
||||||
|
assertNotNull(d.variants["prod"])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `variant stores correct variantName`() {
|
||||||
|
val d = makeDim("env")
|
||||||
|
d.variant("dev") {}
|
||||||
|
assertEquals("dev", d.variants["dev"]!!.variantName)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `multiple variants are stored`() {
|
||||||
|
val d = makeDim("env")
|
||||||
|
d.variant("prod") {}
|
||||||
|
d.variant("dev") {}
|
||||||
|
d.variant("staging") {}
|
||||||
|
assertEquals(3, d.variants.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `calling variant with same name merges fields`() {
|
||||||
|
val d = makeDim("env")
|
||||||
|
d.variant("prod") { field("URL", "https://prod.example.com") }
|
||||||
|
d.variant("prod") { field("KEY", "secret") }
|
||||||
|
val v = d.variants["prod"]!!
|
||||||
|
// Same VariantConfig instance reused — two fields total
|
||||||
|
assertEquals(2, v.fields.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `variant fields are accessible via VariantConfig`() {
|
||||||
|
val d = makeDim("env")
|
||||||
|
d.variant("prod") { field("URL", "https://prod.example.com") }
|
||||||
|
val url = d.variants["prod"]!!.fields[0].resolve(BuildType.RELEASE)!!.get()
|
||||||
|
assertEquals("https://prod.example.com", url)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── common() ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `common block adds fields to commonConfig`() {
|
||||||
|
val d = makeDim("env")
|
||||||
|
d.common { field("TIMEOUT", 30) }
|
||||||
|
assertEquals(1, d.commonConfig.fields.size)
|
||||||
|
assertEquals("TIMEOUT", d.commonConfig.fields[0].fieldName)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `common block can be called multiple times and merges`() {
|
||||||
|
val d = makeDim("env")
|
||||||
|
d.common { field("TIMEOUT", 30) }
|
||||||
|
d.common { field("RETRIES", 3) }
|
||||||
|
assertEquals(2, d.commonConfig.fields.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `common block supports debug scope`() {
|
||||||
|
val d = makeDim("env")
|
||||||
|
d.common {
|
||||||
|
field("TIMEOUT", 30)
|
||||||
|
debug { field("TIMEOUT", 5) }
|
||||||
|
}
|
||||||
|
val fc = d.commonConfig.fields.first { it.fieldName == "TIMEOUT" }
|
||||||
|
assertEquals(5, fc.resolve(BuildType.DEBUG)!!.get())
|
||||||
|
assertEquals(30, fc.resolve(BuildType.RELEASE)!!.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `common block supports release scope`() {
|
||||||
|
val d = makeDim("env")
|
||||||
|
d.common {
|
||||||
|
field("LOG", "info")
|
||||||
|
release { field("LOG", "error") }
|
||||||
|
}
|
||||||
|
val fc = d.commonConfig.fields.first { it.fieldName == "LOG" }
|
||||||
|
assertEquals("info", fc.resolve(BuildType.DEBUG)!!.get())
|
||||||
|
assertEquals("error", fc.resolve(BuildType.RELEASE)!!.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Fresh dimension has empty collections ─────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `fresh DimensionConfig has no variants`() {
|
||||||
|
val d = makeDim("env")
|
||||||
|
assertTrue(d.variants.isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `fresh DimensionConfig commonConfig has no fields`() {
|
||||||
|
val d = makeDim("env")
|
||||||
|
assertTrue(d.commonConfig.fields.isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Dimension with only common (no variants) ──────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `dimension with only common block and no variants is valid`() {
|
||||||
|
val d = makeDim("env")
|
||||||
|
d.common { field("BASE_URL", "https://default.example.com") }
|
||||||
|
assertEquals(0, d.variants.size)
|
||||||
|
assertEquals(1, d.commonConfig.fields.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── objectName for dimension used as nested object ────────────────────────
|
||||||
|
|
||||||
|
@Test fun `objectName is consistent on repeated calls`() {
|
||||||
|
val d = makeDim("my-env")
|
||||||
|
assertEquals(d.objectName(), d.objectName())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
package com.bitsycore.konfig
|
||||||
|
|
||||||
|
import com.bitsycore.konfig.configs.FieldConfig
|
||||||
|
import com.bitsycore.konfig.configs.constantProvider
|
||||||
|
import com.bitsycore.konfig.types.BuildType
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertNotNull
|
||||||
|
import kotlin.test.assertNull
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unit tests for [FieldConfig]: construction, default resolution,
|
||||||
|
* build-type overrides (literal and provider), and resolve() priority.
|
||||||
|
*/
|
||||||
|
class FieldConfigTest {
|
||||||
|
|
||||||
|
// ── Construction ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `fieldName is stored correctly`() {
|
||||||
|
val fc = FieldConfig("MY_FIELD", String::class.java, null)
|
||||||
|
assertEquals("MY_FIELD", fc.fieldName)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `type is stored as javaObjectType`() {
|
||||||
|
val fc = FieldConfig("F", Int::class.javaObjectType, null)
|
||||||
|
assertEquals(Int::class.javaObjectType, fc.type)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `default provider is null when not supplied`() {
|
||||||
|
val fc = FieldConfig("F", String::class.java, null)
|
||||||
|
// resolve with no overrides and no default → null
|
||||||
|
assertNull(fc.resolve(BuildType.DEBUG))
|
||||||
|
assertNull(fc.resolve(BuildType.RELEASE))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Default resolution ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `resolve returns default when no override for DEBUG`() {
|
||||||
|
val fc = FieldConfig("F", String::class.java, constantProvider("default"))
|
||||||
|
val resolved = fc.resolve(BuildType.DEBUG)
|
||||||
|
assertNotNull(resolved)
|
||||||
|
assertEquals("default", resolved.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `resolve returns default when no override for RELEASE`() {
|
||||||
|
val fc = FieldConfig("F", String::class.java, constantProvider("default"))
|
||||||
|
val resolved = fc.resolve(BuildType.RELEASE)
|
||||||
|
assertNotNull(resolved)
|
||||||
|
assertEquals("default", resolved.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── BuildType override via literal value ─────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `debug(literal) overrides for DEBUG`() {
|
||||||
|
val fc = FieldConfig("F", String::class.java, constantProvider("default"))
|
||||||
|
fc.debug("debugValue")
|
||||||
|
assertEquals("debugValue", fc.resolve(BuildType.DEBUG)!!.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `debug(literal) does not affect RELEASE`() {
|
||||||
|
val fc = FieldConfig("F", String::class.java, constantProvider("default"))
|
||||||
|
fc.debug("debugValue")
|
||||||
|
assertEquals("default", fc.resolve(BuildType.RELEASE)!!.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `release(literal) overrides for RELEASE`() {
|
||||||
|
val fc = FieldConfig("F", String::class.java, constantProvider("default"))
|
||||||
|
fc.release("releaseValue")
|
||||||
|
assertEquals("releaseValue", fc.resolve(BuildType.RELEASE)!!.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `release(literal) does not affect DEBUG`() {
|
||||||
|
val fc = FieldConfig("F", String::class.java, constantProvider("default"))
|
||||||
|
fc.release("releaseValue")
|
||||||
|
assertEquals("default", fc.resolve(BuildType.DEBUG)!!.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `both debug and release overrides are stored independently`() {
|
||||||
|
val fc = FieldConfig("F", String::class.java, constantProvider("default"))
|
||||||
|
fc.debug("d")
|
||||||
|
fc.release("r")
|
||||||
|
assertEquals("d", fc.resolve(BuildType.DEBUG)!!.get())
|
||||||
|
assertEquals("r", fc.resolve(BuildType.RELEASE)!!.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── BuildType override via Provider ──────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `debug(provider) stores provider for DEBUG`() {
|
||||||
|
val fc = FieldConfig("F", String::class.java, constantProvider("default"))
|
||||||
|
fc.debug(constantProvider("fromProvider"))
|
||||||
|
assertEquals("fromProvider", fc.resolve(BuildType.DEBUG)!!.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `release(provider) stores provider for RELEASE`() {
|
||||||
|
val fc = FieldConfig("F", String::class.java, constantProvider("default"))
|
||||||
|
fc.release(constantProvider("releaseProvider"))
|
||||||
|
assertEquals("releaseProvider", fc.resolve(BuildType.RELEASE)!!.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Override replaces previous override ─────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `second debug call overwrites first`() {
|
||||||
|
val fc = FieldConfig("F", String::class.java, constantProvider("default"))
|
||||||
|
fc.debug("first")
|
||||||
|
fc.debug("second")
|
||||||
|
assertEquals("second", fc.resolve(BuildType.DEBUG)!!.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `second release call overwrites first`() {
|
||||||
|
val fc = FieldConfig("F", String::class.java, constantProvider("default"))
|
||||||
|
fc.release("first")
|
||||||
|
fc.release("second")
|
||||||
|
assertEquals("second", fc.resolve(BuildType.RELEASE)!!.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Int / Boolean / Long field types ────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `Int field resolves default correctly`() {
|
||||||
|
val fc = FieldConfig("TIMEOUT", Int::class.javaObjectType, constantProvider(30))
|
||||||
|
assertEquals(30, fc.resolve(BuildType.RELEASE)!!.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `Int field debug override resolves correctly`() {
|
||||||
|
val fc = FieldConfig("TIMEOUT", Int::class.javaObjectType, constantProvider(30))
|
||||||
|
fc.debug(5)
|
||||||
|
assertEquals(5, fc.resolve(BuildType.DEBUG)!!.get())
|
||||||
|
assertEquals(30, fc.resolve(BuildType.RELEASE)!!.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `Boolean field default resolves correctly`() {
|
||||||
|
val fc = FieldConfig("FLAG", Boolean::class.javaObjectType, constantProvider(true))
|
||||||
|
assertEquals(true, fc.resolve(BuildType.DEBUG)!!.get())
|
||||||
|
assertEquals(true, fc.resolve(BuildType.RELEASE)!!.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `Boolean field release override resolves correctly`() {
|
||||||
|
val fc = FieldConfig("FLAG", Boolean::class.javaObjectType, constantProvider(true))
|
||||||
|
fc.release(false)
|
||||||
|
assertEquals(true, fc.resolve(BuildType.DEBUG)!!.get())
|
||||||
|
assertEquals(false, fc.resolve(BuildType.RELEASE)!!.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `Long field default resolves correctly`() {
|
||||||
|
val fc = FieldConfig("BIG", Long::class.javaObjectType, constantProvider(9_999_999_999L))
|
||||||
|
assertEquals(9_999_999_999L, fc.resolve(BuildType.RELEASE)!!.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `Double field default resolves correctly`() {
|
||||||
|
val fc = FieldConfig("PI", Double::class.javaObjectType, constantProvider(3.14))
|
||||||
|
assertEquals(3.14, fc.resolve(BuildType.DEBUG)!!.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `Float field default resolves correctly`() {
|
||||||
|
val fc = FieldConfig("F", Float::class.javaObjectType, constantProvider(1.5f))
|
||||||
|
assertEquals(1.5f, fc.resolve(BuildType.DEBUG)!!.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Override map is initially empty ─────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `buildTypeOverrides is empty on fresh FieldConfig`() {
|
||||||
|
val fc = FieldConfig("F", String::class.java, null)
|
||||||
|
assertEquals(0, fc.buildTypeOverrides.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `buildTypeOverrides grows as overrides are set`() {
|
||||||
|
val fc = FieldConfig("F", String::class.java, constantProvider("x"))
|
||||||
|
fc.debug("d")
|
||||||
|
assertEquals(1, fc.buildTypeOverrides.size)
|
||||||
|
fc.release("r")
|
||||||
|
assertEquals(2, fc.buildTypeOverrides.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
package com.bitsycore.konfig
|
||||||
|
|
||||||
|
import com.bitsycore.konfig.configs.FieldConfig
|
||||||
|
import com.bitsycore.konfig.configs.FieldHandle
|
||||||
|
import com.bitsycore.konfig.configs.constantProvider
|
||||||
|
import com.bitsycore.konfig.types.BuildType
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertNull
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unit tests for [FieldHandle].
|
||||||
|
*
|
||||||
|
* [FieldHandle] is a thin wrapper that delegates directly to the underlying
|
||||||
|
* [FieldConfig]. These tests verify that every overload correctly mutates the
|
||||||
|
* backing [FieldConfig] and that the resolver sees those changes.
|
||||||
|
*/
|
||||||
|
class FieldHandleTest {
|
||||||
|
|
||||||
|
private fun stringHandle(default: String = "default"): Pair<FieldHandle<String>, FieldConfig<String>> {
|
||||||
|
val fc = FieldConfig("F", String::class.java, constantProvider(default))
|
||||||
|
return FieldHandle(fc) to fc
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun intHandle(default: Int = 0): Pair<FieldHandle<Int>, FieldConfig<Int>> {
|
||||||
|
val fc = FieldConfig("N", Int::class.javaObjectType, constantProvider(default))
|
||||||
|
return FieldHandle(fc) to fc
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── debug(literal) ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `debug literal sets DEBUG override`() {
|
||||||
|
val (handle, fc) = stringHandle()
|
||||||
|
handle.debug("devValue")
|
||||||
|
assertEquals("devValue", fc.resolve(BuildType.DEBUG)!!.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `debug literal does not touch RELEASE`() {
|
||||||
|
val (handle, fc) = stringHandle("default")
|
||||||
|
handle.debug("devValue")
|
||||||
|
assertEquals("default", fc.resolve(BuildType.RELEASE)!!.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `debug literal on Int handle sets DEBUG override`() {
|
||||||
|
val (handle, fc) = intHandle(30)
|
||||||
|
handle.debug(5)
|
||||||
|
assertEquals(5, fc.resolve(BuildType.DEBUG)!!.get())
|
||||||
|
assertEquals(30, fc.resolve(BuildType.RELEASE)!!.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── release(literal) ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `release literal sets RELEASE override`() {
|
||||||
|
val (handle, fc) = stringHandle()
|
||||||
|
handle.release("prodValue")
|
||||||
|
assertEquals("prodValue", fc.resolve(BuildType.RELEASE)!!.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `release literal does not touch DEBUG`() {
|
||||||
|
val (handle, fc) = stringHandle("default")
|
||||||
|
handle.release("prodValue")
|
||||||
|
assertEquals("default", fc.resolve(BuildType.DEBUG)!!.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── debug(provider) ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `debug provider sets DEBUG override`() {
|
||||||
|
val (handle, fc) = stringHandle()
|
||||||
|
handle.debug(constantProvider("fromProvider"))
|
||||||
|
assertEquals("fromProvider", fc.resolve(BuildType.DEBUG)!!.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `debug provider does not touch RELEASE`() {
|
||||||
|
val (handle, fc) = stringHandle("default")
|
||||||
|
handle.debug(constantProvider("fromProvider"))
|
||||||
|
assertEquals("default", fc.resolve(BuildType.RELEASE)!!.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── release(provider) ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `release provider sets RELEASE override`() {
|
||||||
|
val (handle, fc) = stringHandle()
|
||||||
|
handle.release(constantProvider("relProv"))
|
||||||
|
assertEquals("relProv", fc.resolve(BuildType.RELEASE)!!.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `release provider does not touch DEBUG`() {
|
||||||
|
val (handle, fc) = stringHandle("default")
|
||||||
|
handle.release(constantProvider("relProv"))
|
||||||
|
assertEquals("default", fc.resolve(BuildType.DEBUG)!!.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Both overrides set ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `debug and release both set resolve independently`() {
|
||||||
|
val (handle, fc) = stringHandle("default")
|
||||||
|
handle.debug("d")
|
||||||
|
handle.release("r")
|
||||||
|
assertEquals("d", fc.resolve(BuildType.DEBUG)!!.get())
|
||||||
|
assertEquals("r", fc.resolve(BuildType.RELEASE)!!.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Handle wraps correct FieldConfig ─────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `handle field property points to the same FieldConfig`() {
|
||||||
|
val fc = FieldConfig("F", String::class.java, constantProvider("x"))
|
||||||
|
val handle = FieldHandle(fc)
|
||||||
|
// Mutation through handle should be visible via fc
|
||||||
|
handle.debug("changed")
|
||||||
|
assertEquals("changed", fc.resolve(BuildType.DEBUG)!!.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── No-override scenarios ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `handle with no debug or release set returns null for scope-only field`() {
|
||||||
|
val fc = FieldConfig<String>("F", String::class.java, null)
|
||||||
|
@Suppress("UNUSED_VARIABLE") val handle = FieldHandle(fc)
|
||||||
|
// No overrides set — resolve returns null
|
||||||
|
assertNull(fc.resolve(BuildType.DEBUG))
|
||||||
|
assertNull(fc.resolve(BuildType.RELEASE))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Override replacement ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `second debug call replaces first override`() {
|
||||||
|
val (handle, fc) = stringHandle()
|
||||||
|
handle.debug("first")
|
||||||
|
handle.debug("second")
|
||||||
|
assertEquals("second", fc.resolve(BuildType.DEBUG)!!.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `second release call replaces first override`() {
|
||||||
|
val (handle, fc) = stringHandle()
|
||||||
|
handle.release("first")
|
||||||
|
handle.release("second")
|
||||||
|
assertEquals("second", fc.resolve(BuildType.RELEASE)!!.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Provider and literal mixed ────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `literal debug then provider debug - provider wins`() {
|
||||||
|
val (handle, fc) = stringHandle()
|
||||||
|
handle.debug("literal")
|
||||||
|
handle.debug(constantProvider("provider"))
|
||||||
|
assertEquals("provider", fc.resolve(BuildType.DEBUG)!!.get())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
package com.bitsycore.konfig
|
|
||||||
|
|
||||||
import kotlin.test.Test
|
|
||||||
import kotlin.test.assertEquals
|
|
||||||
import kotlin.test.assertNull
|
|
||||||
|
|
||||||
class KonfigTypeTest {
|
|
||||||
|
|
||||||
@Test fun `exact enum name resolves DEBUG`() {
|
|
||||||
assertEquals(BuildType.DEBUG, BuildType.resolve("DEBUG"))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test fun `exact enum name resolves RELEASE`() {
|
|
||||||
assertEquals(BuildType.RELEASE, BuildType.resolve("RELEASE"))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test fun `task name containing debug resolves DEBUG`() {
|
|
||||||
assertEquals(BuildType.DEBUG, BuildType.resolve("compileDebugKotlin"))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test fun `task name containing release resolves RELEASE`() {
|
|
||||||
assertEquals(BuildType.RELEASE, BuildType.resolve("assembleRelease"))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test fun `ambiguous input returns null`() {
|
|
||||||
assertNull(BuildType.resolve("debugRelease"))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test fun `unrelated string returns null`() {
|
|
||||||
assertNull(BuildType.resolve("compileKotlinJvm"))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test fun `lowercase debug resolves DEBUG`() {
|
|
||||||
assertEquals(BuildType.DEBUG, BuildType.resolve("debug"))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test fun `lowercase release resolves RELEASE`() {
|
|
||||||
assertEquals(BuildType.RELEASE, BuildType.resolve("release"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
package com.bitsycore.konfig
|
||||||
|
|
||||||
|
import com.bitsycore.konfig.configs.VariantConfig
|
||||||
|
import com.bitsycore.konfig.configs.constantProvider
|
||||||
|
import com.bitsycore.konfig.types.BuildType
|
||||||
|
import kotlin.test.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unit tests for [VariantConfig].
|
||||||
|
*
|
||||||
|
* Covers:
|
||||||
|
* - Basic field declaration (literal and provider default)
|
||||||
|
* - Duplicate field guard
|
||||||
|
* - [FieldHandle] fluent API wired through the config
|
||||||
|
* - debug {} / release {} block scopes
|
||||||
|
* - getOrCreateField (creates on first call, retrieves on subsequent calls)
|
||||||
|
* - Multiple fields coexist without interference
|
||||||
|
*/
|
||||||
|
class VariantConfigTest {
|
||||||
|
|
||||||
|
private fun makeVariant(name: String = "test") = VariantConfig(name)
|
||||||
|
|
||||||
|
// ── variantName ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `variantName is stored`() {
|
||||||
|
val v = makeVariant("prod")
|
||||||
|
assertEquals("prod", v.variantName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── field with literal default ────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `field with String default is added to fields list`() {
|
||||||
|
val v = makeVariant()
|
||||||
|
v.field("URL", "https://example.com")
|
||||||
|
assertEquals(1, v.fields.size)
|
||||||
|
assertEquals("URL", v.fields[0].fieldName)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `field with Int default is added`() {
|
||||||
|
val v = makeVariant()
|
||||||
|
v.field("TIMEOUT", 30)
|
||||||
|
assertEquals("TIMEOUT", v.fields[0].fieldName)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `field with Boolean default is added`() {
|
||||||
|
val v = makeVariant()
|
||||||
|
v.field("LOGS_ENABLED", true)
|
||||||
|
assertEquals("LOGS_ENABLED", v.fields[0].fieldName)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `field with Long default is added`() {
|
||||||
|
val v = makeVariant()
|
||||||
|
v.field("BIG_NUM", 9_000_000_000L)
|
||||||
|
assertEquals(9_000_000_000L, v.fields[0].resolve(BuildType.RELEASE)!!.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `field with Double default is added`() {
|
||||||
|
val v = makeVariant()
|
||||||
|
v.field("PI", 3.14159)
|
||||||
|
assertEquals(3.14159, v.fields[0].resolve(BuildType.DEBUG)!!.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `field with Float default is added`() {
|
||||||
|
val v = makeVariant()
|
||||||
|
v.field("F", 2.0f)
|
||||||
|
assertEquals(2.0f, v.fields[0].resolve(BuildType.DEBUG)!!.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── field with Provider default ───────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `field with provider default stores provider`() {
|
||||||
|
val v = makeVariant()
|
||||||
|
v.field("API_KEY", constantProvider("secret"))
|
||||||
|
val resolved = v.fields[0].resolve(BuildType.RELEASE)
|
||||||
|
assertNotNull(resolved)
|
||||||
|
assertEquals("secret", resolved.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── FieldHandle returned by field() ──────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `field returns non-null FieldHandle`() {
|
||||||
|
val v = makeVariant()
|
||||||
|
val handle = v.field("X", "val")
|
||||||
|
assertNotNull(handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `FieldHandle debug sets debug override`() {
|
||||||
|
val v = makeVariant()
|
||||||
|
v.field("URL", "prod").debug("dev")
|
||||||
|
val fc = v.fields[0]
|
||||||
|
assertEquals("dev", fc.resolve(BuildType.DEBUG)!!.get())
|
||||||
|
assertEquals("prod", fc.resolve(BuildType.RELEASE)!!.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `FieldHandle release sets release override`() {
|
||||||
|
val v = makeVariant()
|
||||||
|
v.field("URL", "default").release("rel")
|
||||||
|
val fc = v.fields[0]
|
||||||
|
assertEquals("default", fc.resolve(BuildType.DEBUG)!!.get())
|
||||||
|
assertEquals("rel", fc.resolve(BuildType.RELEASE)!!.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `FieldHandle debug and release can be chained independently`() {
|
||||||
|
val v = makeVariant()
|
||||||
|
val handle = v.field("LOG", "info")
|
||||||
|
handle.debug("verbose")
|
||||||
|
handle.release("error")
|
||||||
|
val fc = v.fields[0]
|
||||||
|
assertEquals("verbose", fc.resolve(BuildType.DEBUG)!!.get())
|
||||||
|
assertEquals("error", fc.resolve(BuildType.RELEASE)!!.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Duplicate field guard ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `declaring duplicate field name throws IllegalArgumentException`() {
|
||||||
|
val v = makeVariant()
|
||||||
|
v.field("DUPE", "first")
|
||||||
|
assertFailsWith<IllegalArgumentException> {
|
||||||
|
v.field("DUPE", "second")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `duplicate guard error message contains field name`() {
|
||||||
|
val v = makeVariant("myVariant")
|
||||||
|
v.field("DUPE", "first")
|
||||||
|
val ex = assertFailsWith<IllegalArgumentException> {
|
||||||
|
v.field("DUPE", "second")
|
||||||
|
}
|
||||||
|
assertTrue(ex.message!!.contains("DUPE"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `duplicate guard error message contains variant name`() {
|
||||||
|
val v = makeVariant("myVariant")
|
||||||
|
v.field("DUPE", "first")
|
||||||
|
val ex = assertFailsWith<IllegalArgumentException> {
|
||||||
|
v.field("DUPE", "second")
|
||||||
|
}
|
||||||
|
assertTrue(ex.message!!.contains("myVariant"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `fields with different names do not trigger duplicate error`() {
|
||||||
|
val v = makeVariant()
|
||||||
|
v.field("A", "1")
|
||||||
|
v.field("B", "2")
|
||||||
|
assertEquals(2, v.fields.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── debug {} block ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `debug block sets override for DEBUG build type`() {
|
||||||
|
val v = makeVariant()
|
||||||
|
v.debug { field("LOG", "verbose") }
|
||||||
|
val fc = v.fields.first { it.fieldName == "LOG" }
|
||||||
|
assertEquals("verbose", fc.resolve(BuildType.DEBUG)!!.get())
|
||||||
|
assertNull(fc.resolve(BuildType.RELEASE))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `debug block with multiple fields stores all`() {
|
||||||
|
val v = makeVariant()
|
||||||
|
v.debug {
|
||||||
|
field("A", "alpha")
|
||||||
|
field("B", 99)
|
||||||
|
}
|
||||||
|
assertEquals(2, v.fields.size)
|
||||||
|
assertEquals("alpha", v.fields.first { it.fieldName == "A" }.resolve(BuildType.DEBUG)!!.get())
|
||||||
|
assertEquals(99, v.fields.first { it.fieldName == "B" }.resolve(BuildType.DEBUG)!!.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── release {} block ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `release block sets override for RELEASE build type`() {
|
||||||
|
val v = makeVariant()
|
||||||
|
v.release { field("LOG", "error") }
|
||||||
|
val fc = v.fields.first { it.fieldName == "LOG" }
|
||||||
|
assertEquals("error", fc.resolve(BuildType.RELEASE)!!.get())
|
||||||
|
assertNull(fc.resolve(BuildType.DEBUG))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `release block with provider stores provider`() {
|
||||||
|
val v = makeVariant()
|
||||||
|
v.release { field("KEY", constantProvider("relProv")) }
|
||||||
|
val fc = v.fields.first { it.fieldName == "KEY" }
|
||||||
|
assertEquals("relProv", fc.resolve(BuildType.RELEASE)!!.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── debug + release blocks for same field name ───────────────────────────
|
||||||
|
|
||||||
|
@Test fun `field can appear in both debug and release blocks`() {
|
||||||
|
val v = makeVariant()
|
||||||
|
v.debug { field("LOG", "verbose") }
|
||||||
|
v.release { field("LOG", "error") }
|
||||||
|
// getOrCreateField reuses existing FieldConfig
|
||||||
|
assertEquals(1, v.fields.size)
|
||||||
|
val fc = v.fields[0]
|
||||||
|
assertEquals("verbose", fc.resolve(BuildType.DEBUG)!!.get())
|
||||||
|
assertEquals("error", fc.resolve(BuildType.RELEASE)!!.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Multiple independent fields ──────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `multiple fields are independent`() {
|
||||||
|
val v = makeVariant()
|
||||||
|
v.field("URL", "https://api.example.com")
|
||||||
|
v.field("TIMEOUT", 30)
|
||||||
|
v.field("DEBUG_MODE", false)
|
||||||
|
|
||||||
|
assertEquals(3, v.fields.size)
|
||||||
|
assertEquals("https://api.example.com", v.fields[0].resolve(BuildType.RELEASE)!!.get())
|
||||||
|
assertEquals(30, v.fields[1].resolve(BuildType.RELEASE)!!.get())
|
||||||
|
assertEquals(false, v.fields[2].resolve(BuildType.RELEASE)!!.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Empty variant ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `fresh VariantConfig has no fields`() {
|
||||||
|
val v = makeVariant()
|
||||||
|
assertTrue(v.fields.isEmpty())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package com.bitsycore.konfig
|
||||||
|
|
||||||
|
import com.bitsycore.konfig.types.Visibility
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertNotEquals
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for the [Visibility] enum.
|
||||||
|
*/
|
||||||
|
class VisibilityTest {
|
||||||
|
|
||||||
|
@Test fun `enum contains exactly two entries`() {
|
||||||
|
assertEquals(2, Visibility.entries.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `PUBLIC entry exists`() {
|
||||||
|
assertEquals(Visibility.PUBLIC, Visibility.valueOf("PUBLIC"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `INTERNAL entry exists`() {
|
||||||
|
assertEquals(Visibility.INTERNAL, Visibility.valueOf("INTERNAL"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `PUBLIC and INTERNAL are distinct`() {
|
||||||
|
assertNotEquals(Visibility.PUBLIC, Visibility.INTERNAL)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `ordinal of PUBLIC is 0`() {
|
||||||
|
assertEquals(0, Visibility.PUBLIC.ordinal)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `ordinal of INTERNAL is 1`() {
|
||||||
|
assertEquals(1, Visibility.INTERNAL.ordinal)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `name of PUBLIC is PUBLIC`() {
|
||||||
|
assertEquals("PUBLIC", Visibility.PUBLIC.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `name of INTERNAL is INTERNAL`() {
|
||||||
|
assertEquals("INTERNAL", Visibility.INTERNAL.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `entries list order is PUBLIC then INTERNAL`() {
|
||||||
|
val entries = Visibility.entries
|
||||||
|
assertEquals(Visibility.PUBLIC, entries[0])
|
||||||
|
assertEquals(Visibility.INTERNAL, entries[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `valueOf is case-sensitive and throws on lowercase`() {
|
||||||
|
var threw = false
|
||||||
|
try {
|
||||||
|
Visibility.valueOf("public")
|
||||||
|
} catch (_: IllegalArgumentException) {
|
||||||
|
threw = true
|
||||||
|
}
|
||||||
|
assertTrue(threw)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user