Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions build-logic/convention/build.gradle.kts
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,5 +45,9 @@ gradlePlugin {
id = "flipcash.kmp.library"
implementationClass = "KmpLibraryConventionPlugin"
}
register("kmpTestFixtures") {
id = "flipcash.kmp.test.fixtures"
implementationClass = "KmpTestFixturesConventionPlugin"
}
}
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
import com.getcode.buildlogic.testfixtures.GenerateTestFixtures
import com.getcode.buildlogic.testfixtures.TestFixturesExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.create
import org.gradle.kotlin.dsl.register
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension

/**
* Compiles a KMP module's `src/commonTest/resources` into a generated `TestFixtures.kt` on
* `commonTest`, so the same fixtures are readable from every target (Kotlin/Native test binaries
* ship no resource bundle, so a resource-based loader only ever runs on the JVM).
*
* Usage in a module's `build.gradle.kts`:
* ```
* plugins {
* alias(libs.plugins.flipcash.kmp.test.fixtures)
* }
*
* testFixtures {
* packageName = "com.getcode.vendor"
* }
* ```
*
* The generated directory is registered on `commonTest` and the AGP lint tasks are made to depend
* on the generator -- adding the source directory only carries the dependency to the Kotlin compile
* tasks, while lint reads the same directories straight off disk and Gradle then fails the build
* over an undeclared dependency on generated sources.
*/
class KmpTestFixturesConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
val extension = extensions.create<TestFixturesExtension>("testFixtures")
extension.fixtures.convention(layout.projectDirectory.dir("src/commonTest/resources"))

val generateTestFixtures = tasks.register<GenerateTestFixtures>("generateTestFixtures") {
packageName.set(extension.packageName)
fixtures.set(extension.fixtures)
outputDirectory.set(layout.buildDirectory.dir("generated/testFixtures"))
}

tasks.matching { it.name.startsWith("lint") || it.name.endsWith("LintModel") }
.configureEach { dependsOn(generateTestFixtures) }

pluginManager.withPlugin("org.jetbrains.kotlin.multiplatform") {
extensions.configure<KotlinMultiplatformExtension> {
sourceSets.named("commonTest") { kotlin.srcDir(generateTestFixtures) }
}
}
}
}
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
package com.getcode.buildlogic.testfixtures

import org.gradle.api.DefaultTask
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.TaskAction

/**
* Compiles the cross-platform fixtures into `commonTest` as Kotlin constants.
*
* The parity gate is only worth something if it runs on *both* platforms, and Kotlin/Native test
* binaries ship no resource bundle -- `NSBundle.pathForResource` finds nothing there, so a
* resource-based loader quietly only ever runs on the JVM. Generating a source file instead makes
* the same fixtures readable from every target with no platform code at all.
*/
abstract class GenerateTestFixtures : DefaultTask() {

/** Package the generated `TestFixtures.kt` is declared in. */
@get:Input
abstract val packageName: Property<String>

@get:InputDirectory
abstract val fixtures: DirectoryProperty

@get:OutputDirectory
abstract val outputDirectory: DirectoryProperty

@TaskAction
fun generate() {
val target = packageName.get()
val files = fixtures.get().asFile.listFiles().orEmpty().sortedBy { it.name }
val destination = outputDirectory.get().asFile
.resolve(target.replace('.', '/'))
.resolve("TestFixtures.kt")
destination.parentFile.mkdirs()

destination.writeText(
buildString {
appendLine("package $target")
appendLine()
appendLine("// Generated from src/commonTest/resources -- do not edit.")
appendLine()
appendLine("private val FIXTURES: Map<String, String> = mapOf(")
files.forEach { file ->
append(" \"").append(file.name).append("\" to \"")
append(file.readText().escapeForKotlin())
appendLine("\",")
}
appendLine(")")
appendLine()
appendLine("/** Reads a fixture compiled in from `src/commonTest/resources/`. */")
appendLine("fun readTestResource(name: String): String =")
append(" requireNotNull(FIXTURES[name]) { \"unknown fixture '")
appendLine("\$name'\" }")
}
)
}

private fun String.escapeForKotlin(): String = buildString(length) {
this@escapeForKotlin.forEach { character ->
when (character) {
'\\' -> append("\\\\")
'"' -> append("\\\"")
'$' -> append("\\$")
'\n' -> append("\\n")
'\r' -> append("\\r")
'\t' -> append("\\t")
else -> append(character)
}
}
}
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
package com.getcode.buildlogic.testfixtures

import org.gradle.api.file.DirectoryProperty
import org.gradle.api.provider.Property

/**
* Configures [GenerateTestFixtures] for the module.
*
* ```
* testFixtures {
* packageName = "com.getcode.vendor"
* }
* ```
*/
abstract class TestFixturesExtension {

/** Package the generated `TestFixtures.kt` is declared in. Required. */
abstract val packageName: Property<String>

/** Directory of fixture files to compile in. Defaults to `src/commonTest/resources`. */
abstract val fixtures: DirectoryProperty
}
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -335,6 +335,7 @@ flipcash-android-library-compose = { id = "flipcash.android.library.compose" }
flipcash-android-feature = { id = "flipcash.android.feature" }
flipcash-android-ed25519-shadow = { id = "flipcash.android.ed25519.shadow" }
flipcash-kmp-library = { id = "flipcash.kmp.library" }
flipcash-kmp-test-fixtures = { id = "flipcash.kmp.test.fixtures" }
android-application = { id = "com.android.application", version.ref = "agp" }
android-library = { id = "com.android.library", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
Expand Down
75 changes: 5 additions & 70 deletions libs/codes/kikcode/build.gradle.kts
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,15 @@
plugins {
kotlin("multiplatform")
id("com.android.kotlin.multiplatform.library")
alias(libs.plugins.flipcash.kmp.test.fixtures)
}

/**
* Compiles the cross-platform fixtures into `commonTest` as Kotlin constants.
*
* The parity gate is only worth something if it runs on *both* platforms, and Kotlin/Native test
* binaries ship no resource bundle -- `NSBundle.pathForResource` finds nothing there, so a
* resource-based loader quietly only ever runs on the JVM. Generating a source file instead makes
* the same fixtures readable from every target with no platform code at all.
*/
abstract class GenerateTestFixtures : DefaultTask() {

@get:InputDirectory
abstract val fixtures: DirectoryProperty

@get:OutputDirectory
abstract val outputDirectory: DirectoryProperty

@TaskAction
fun generate() {
val files = fixtures.get().asFile.listFiles().orEmpty().sortedBy { it.name }
val destination = outputDirectory.get().asFile
.resolve("com/getcode/codes/kikcode/TestFixtures.kt")
destination.parentFile.mkdirs()

destination.writeText(
buildString {
appendLine("package com.getcode.codes.kikcode")
appendLine()
appendLine("// Generated from src/commonTest/resources -- do not edit.")
appendLine()
appendLine("private val FIXTURES: Map<String, String> = mapOf(")
files.forEach { file ->
append(" \"").append(file.name).append("\" to \"")
append(file.readText().escapeForKotlin())
appendLine("\",")
}
appendLine(")")
appendLine()
appendLine("/** Reads a fixture compiled in from `src/commonTest/resources/`. */")
appendLine("fun readTestResource(name: String): String =")
append(" requireNotNull(FIXTURES[name]) { \"unknown fixture '")
appendLine("\$name'\" }")
}
)
}

private fun String.escapeForKotlin(): String = buildString(length) {
this@escapeForKotlin.forEach { character ->
when (character) {
'\\' -> append("\\\\")
'"' -> append("\\\"")
'$' -> append("\\$")
'\n' -> append("\\n")
'\r' -> append("\\r")
'\t' -> append("\\t")
else -> append(character)
}
}
}
// Compiles `src/commonTest/resources` into a generated `TestFixtures.kt` on `commonTest`, readable
// from every target -- see the `flipcash.kmp.test.fixtures` convention plugin.
testFixtures {
packageName = "com.getcode.codes.kikcode"
}

val generateTestFixtures = tasks.register<GenerateTestFixtures>("generateTestFixtures") {
fixtures.set(layout.projectDirectory.dir("src/commonTest/resources"))
outputDirectory.set(layout.buildDirectory.dir("generated/testFixtures"))
}

// `srcDir(taskProvider)` below carries the task dependency to the Kotlin compile tasks only; AGP's
// lint tasks read the same source directories straight off disk, so Gradle fails the build over an
// undeclared dependency on the generated fixtures. Wire it up by hand.
tasks.matching { it.name.startsWith("lint") || it.name.endsWith("LintModel") }
.configureEach { dependsOn(generateTestFixtures) }

kotlin {
android {
namespace = "com.getcode.codes.kikcode"
Expand All@@ -91,7 +27,6 @@ kotlin {
// Pure Kotlin -- geometry + string building, no platform APIs.
}
commonTest {
kotlin.srcDir(generateTestFixtures)
dependencies {
implementation(kotlin("test"))
implementation(libs.kotlinx.serialization.json)
Expand Down
75 changes: 5 additions & 70 deletions libs/encryption/base58/build.gradle.kts
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,15 @@
plugins {
kotlin("multiplatform")
id("com.android.kotlin.multiplatform.library")
alias(libs.plugins.flipcash.kmp.test.fixtures)
}

/**
* Compiles the cross-platform fixtures into `commonTest` as Kotlin constants.
*
* The parity gate is only worth something if it runs on *both* platforms, and Kotlin/Native test
* binaries ship no resource bundle -- `NSBundle.pathForResource` finds nothing there, so a
* resource-based loader quietly only ever runs on the JVM. Generating a source file instead makes
* the same fixtures readable from every target with no platform code at all.
*/
abstract class GenerateTestFixtures : DefaultTask() {

@get:InputDirectory
abstract val fixtures: DirectoryProperty

@get:OutputDirectory
abstract val outputDirectory: DirectoryProperty

@TaskAction
fun generate() {
val files = fixtures.get().asFile.listFiles().orEmpty().sortedBy { it.name }
val destination = outputDirectory.get().asFile
.resolve("com/getcode/vendor/TestFixtures.kt")
destination.parentFile.mkdirs()

destination.writeText(
buildString {
appendLine("package com.getcode.vendor")
appendLine()
appendLine("// Generated from src/commonTest/resources -- do not edit.")
appendLine()
appendLine("private val FIXTURES: Map<String, String> = mapOf(")
files.forEach { file ->
append(" \"").append(file.name).append("\" to \"")
append(file.readText().escapeForKotlin())
appendLine("\",")
}
appendLine(")")
appendLine()
appendLine("/** Reads a fixture compiled in from `src/commonTest/resources/`. */")
appendLine("fun readTestResource(name: String): String =")
append(" requireNotNull(FIXTURES[name]) { \"unknown fixture '")
appendLine("\$name'\" }")
}
)
}

private fun String.escapeForKotlin(): String = buildString(length) {
this@escapeForKotlin.forEach { character ->
when (character) {
'\\' -> append("\\\\")
'"' -> append("\\\"")
'$' -> append("\\$")
'\n' -> append("\\n")
'\r' -> append("\\r")
'\t' -> append("\\t")
else -> append(character)
}
}
}
// Compiles `src/commonTest/resources` into a generated `TestFixtures.kt` on `commonTest`, readable
// from every target -- see the `flipcash.kmp.test.fixtures` convention plugin.
testFixtures {
packageName = "com.getcode.vendor"
}

val generateTestFixtures = tasks.register<GenerateTestFixtures>("generateTestFixtures") {
fixtures.set(layout.projectDirectory.dir("src/commonTest/resources"))
outputDirectory.set(layout.buildDirectory.dir("generated/testFixtures"))
}

// `srcDir(taskProvider)` below carries the task dependency to the Kotlin compile tasks only; AGP's
// lint tasks read the same source directories straight off disk, so Gradle fails the build over an
// undeclared dependency on the generated fixtures. Wire it up by hand.
tasks.matching { it.name.startsWith("lint") || it.name.endsWith("LintModel") }
.configureEach { dependsOn(generateTestFixtures) }

kotlin {
android {
namespace = "com.getcode.encryption.base58"
Expand All@@ -94,7 +30,6 @@ kotlin {
// MessageDigest + BigInteger -- JDK only; no extra Gradle deps.
}
commonTest {
kotlin.srcDir(generateTestFixtures)
dependencies {
implementation(kotlin("test"))
implementation(libs.kotlinx.serialization.json)
Expand Down
Loading