Skip to content

Commit 5cb10cb

Browse files
committed
Enhance CI workflow and plugin testing capabilities: update GitHub Actions configuration to include optional integration tests and Jacoco test report generation. Refactor build.gradle.kts to introduce a new integration test task, improve task property handling, and ensure proper configuration for test execution. Update README.md to clarify testing requirements and usage instructions. Refactor task properties to use Gradle's Property API for better configuration management.
1 parent 749118f commit 5cb10cb

15 files changed

Lines changed: 355 additions & 253 deletions

File tree

.github/workflows/ci.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@ jobs:
2121
run: chmod +x gradlew
2222

2323
- name: Plugin unit tests
24-
run: ./gradlew :plugin:test :plugin:detekt --no-daemon
24+
run: ./gradlew :plugin:test :plugin:jacocoTestReport :plugin:detekt --no-daemon
25+
26+
- name: Plugin integration tests (optional)
27+
continue-on-error: true
28+
run: ./gradlew :plugin:integrationTest --no-daemon
2529

2630
- name: Sample app assembleDebug
2731
run: ./gradlew :app:assembleDebug --no-daemon

plugin/README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,20 @@
22

33
#### [Wiki](https://github.com/StringCare/AndroidLibrary/wiki)
44

5+
## Requirements (this repo)
6+
7+
- **Gradle** 8.11+ (configuration cache validated on sample `:app:assembleDebug`)
8+
- **JDK** 17
9+
- **Android Gradle Plugin** 8.7+ (as used by the sample app)
10+
- **Troubleshooting**: If host native libs fail to load, set `stringcare` `debug = true` for verbose JNI logs, or set env `STRINGCARE_NATIVE_DEBUG` (see `Stark`).
11+
12+
See repo root [CHANGELOG.md](../CHANGELOG.md), [MIGRATION.md](../MIGRATION.md), [SECURITY.md](../SECURITY.md).
13+
14+
### Tests
15+
16+
- Default `./gradlew :plugin:test` excludes `@Tag("integration")` (network + `git clone` KotlinSample + nested Gradle).
17+
- Full suite: `./gradlew :plugin:integrationTest` (may fail offline or if the sample repo changes).
18+
519

620
License
721
-------

plugin/build.gradle.kts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import org.gradle.api.tasks.SourceSetContainer
2+
import org.gradle.api.tasks.testing.Test
3+
14
plugins {
25
kotlin("jvm") version "2.0.21"
36
kotlin("plugin.serialization") version "2.0.21"
@@ -50,8 +53,32 @@ jacoco {
5053
toolVersion = "0.8.12"
5154
}
5255

56+
tasks.withType<Test>().configureEach {
57+
useJUnitPlatform {
58+
if (name == "test") {
59+
excludeTags("integration")
60+
}
61+
}
62+
}
63+
64+
val sourceSets = extensions.getByType(SourceSetContainer::class.java)
65+
tasks.register<Test>("integrationTest") {
66+
group = "verification"
67+
description =
68+
"Runs @Tag(\"integration\") tests (git clone KotlinSample, nested Gradle). " +
69+
"Optional; default :plugin:test excludes these."
70+
testClassesDirs = sourceSets.named("test").get().output.classesDirs
71+
classpath = sourceSets.named("test").get().runtimeClasspath
72+
useJUnitPlatform {
73+
includeTags("integration")
74+
}
75+
}
76+
5377
tasks.test {
5478
finalizedBy(tasks.jacocoTestReport)
79+
extensions.configure(org.gradle.testing.jacoco.plugins.JacocoTaskExtension::class.java) {
80+
isEnabled = true
81+
}
5582
}
5683

5784
tasks.jacocoTestReport {
@@ -65,8 +92,8 @@ tasks.jacocoTestReport {
6592
detekt {
6693
buildUponDefaultConfig = true
6794
allRules = false
68-
config.setFrom(files("$rootDir/config/detekt/detekt.yml"))
69-
baseline = file("$rootDir/config/detekt/baseline.xml")
95+
config.setFrom(files(layout.projectDirectory.file("config/detekt/detekt.yml")))
96+
baseline = file("config/detekt/baseline.xml")
7097
parallel = true
7198
}
7299

plugin/src/main/kotlin/dev/vyp/stringcare/plugin/internal/Extensions.kt

Lines changed: 20 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -330,34 +330,30 @@ fun Project.registerTask(
330330

331331
this.tasks.register(gradleTaskNameDoctor, SCPreview::class.java) { t ->
332332
(t as Task).usesService(buildService)
333-
(t as SCPreview).stringCareService.set(buildService)
333+
t.stringCareService.set(buildService)
334+
t.module.set(configuration.name)
335+
t.applicationId.set(configuration.applicationId)
336+
t.variantName.set(variantDefault)
337+
t.srcFolders.set(srcJson)
338+
t.stringFiles.set(strJson)
339+
t.assetsFiles.set(assetsJson)
340+
t.mockedFingerprint.set(configuration.mockedFingerprint)
341+
t.debug.set(configuration.debug)
342+
t.skip.set(configuration.skip)
334343
}
335344
this.tasks.register(gradleTaskNameObfuscate, SCTestObfuscation::class.java) { t ->
336345
(t as Task).usesService(buildService)
337-
(t as SCTestObfuscation).stringCareService.set(buildService)
346+
t.stringCareService.set(buildService)
347+
t.module.set(configuration.name)
348+
t.applicationId.set(configuration.applicationId)
349+
t.variantName.set(variantDefault)
350+
t.srcFolders.set(srcJson)
351+
t.stringFiles.set(strJson)
352+
t.assetsFiles.set(assetsJson)
353+
t.mockedFingerprint.set(configuration.mockedFingerprint)
354+
t.debug.set(configuration.debug)
355+
t.skip.set(configuration.skip)
338356
}
339-
340-
val previewTask = this.tasks.getByPath(gradleTaskNameDoctor) as SCPreview
341-
previewTask.module = configuration.name
342-
previewTask.applicationId = configuration.applicationId
343-
previewTask.variantName = variantDefault
344-
previewTask.srcFolders = srcJson
345-
previewTask.stringFiles = strJson
346-
previewTask.assetsFiles = assetsJson
347-
previewTask.mockedFingerprint = configuration.mockedFingerprint
348-
previewTask.debug = configuration.debug
349-
previewTask.skip = configuration.skip
350-
351-
val obfuscateTask = this.tasks.getByPath(gradleTaskNameObfuscate) as SCTestObfuscation
352-
obfuscateTask.module = configuration.name
353-
obfuscateTask.applicationId = configuration.applicationId
354-
obfuscateTask.variantName = variantDefault
355-
obfuscateTask.srcFolders = srcJson
356-
obfuscateTask.stringFiles = strJson
357-
obfuscateTask.assetsFiles = assetsJson
358-
obfuscateTask.mockedFingerprint = configuration.mockedFingerprint
359-
obfuscateTask.debug = configuration.debug
360-
obfuscateTask.skip = configuration.skip
361357
}
362358

363359
fun Process.outputString(): String {

plugin/src/main/kotlin/dev/vyp/stringcare/plugin/internal/Tasks.kt

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,30 +8,35 @@ internal fun signingReportTask(): String =
88
Os.OSX, Os.LINUX -> wrapperOsX
99
}} signingReport"
1010

11-
internal fun gradleWrapper(): String =
11+
/** `gradlew` in the current working directory (e.g. cloned KotlinSample). */
12+
internal fun projectGradleWrapper(): String =
1213
when (getOs()) {
1314
Os.WINDOWS -> wrapperWindows
1415
Os.OSX, Os.LINUX -> wrapperOsX
1516
}
1617

18+
/** Monorepo wrapper when `user.dir` is `:plugin` — root project is parent directory. */
19+
internal fun repoRootGradle(): String =
20+
when (getOs()) {
21+
Os.WINDOWS -> "..\\gradlew.bat -p .."
22+
Os.OSX, Os.LINUX -> "sh ../gradlew -p .."
23+
}
24+
1725
internal fun pluginBuildTask(): String =
18-
"${when (getOs()) {
19-
Os.WINDOWS -> wrapperWindows
20-
Os.OSX, Os.LINUX -> wrapperOsX
21-
}} build --exclude-task test"
26+
"${repoRootGradle()} :plugin:compileKotlin --no-daemon"
2227

28+
/**
29+
* Ensures JNI prebuilts exist on the test runtime classpath under `macos/`, `linux/`, `windows/`
30+
* (same layout as [processResources]). Quotes paths for spaces/special chars in absolute working dirs.
31+
*/
2332
internal val librarySetupTask =
2433
"""
25-
${copyCommand()} src${File.separator}main${File.separator}kotlin${File.separator}dev${File.separator}vyp${File.separator}stringcare${File.separator}plugin${File.separator}internal${File.separator}jni${File.separator}$osxLib out${File.separator}production${File.separator}classes${File.separator}$osxLib &
26-
${copyCommand()} src${File.separator}main${File.separator}kotlin${File.separator}dev${File.separator}vyp${File.separator}stringcare${File.separator}plugin${File.separator}internal${File.separator}jni${File.separator}$linuxLib out${File.separator}production${File.separator}classes${File.separator}$linuxLib &
27-
${copyCommand()} src${File.separator}main${File.separator}kotlin${File.separator}dev${File.separator}vyp${File.separator}stringcare${File.separator}plugin${File.separator}internal${File.separator}jni${File.separator}$linuxLibArm64 out${File.separator}production${File.separator}classes${File.separator}$linuxLibArm64 &
28-
${copyCommand()} src${File.separator}main${File.separator}kotlin${File.separator}dev${File.separator}vyp${File.separator}stringcare${File.separator}plugin${File.separator}internal${File.separator}jni${File.separator}$winLib out${File.separator}production${File.separator}classes${File.separator}$winLib &
29-
${copyCommand()} src${File.separator}main${File.separator}kotlin${File.separator}dev${File.separator}vyp${File.separator}stringcare${File.separator}plugin${File.separator}internal${File.separator}jni${File.separator}$winLibArm64 out${File.separator}production${File.separator}classes${File.separator}$winLibArm64 &
30-
${copyCommand()} src${File.separator}main${File.separator}kotlin${File.separator}dev${File.separator}vyp${File.separator}stringcare${File.separator}plugin${File.separator}internal${File.separator}jni${File.separator}$osxLib build${File.separator}classes${File.separator}kotlin${File.separator}main${File.separator}$osxLib &
31-
${copyCommand()} src${File.separator}main${File.separator}kotlin${File.separator}dev${File.separator}vyp${File.separator}stringcare${File.separator}plugin${File.separator}internal${File.separator}jni${File.separator}$linuxLib build${File.separator}classes${File.separator}kotlin${File.separator}main${File.separator}$linuxLib &
32-
${copyCommand()} src${File.separator}main${File.separator}kotlin${File.separator}dev${File.separator}vyp${File.separator}stringcare${File.separator}plugin${File.separator}internal${File.separator}jni${File.separator}$linuxLibArm64 build${File.separator}classes${File.separator}kotlin${File.separator}main${File.separator}$linuxLibArm64 &
33-
${copyCommand()} src${File.separator}main${File.separator}kotlin${File.separator}dev${File.separator}vyp${File.separator}stringcare${File.separator}plugin${File.separator}internal${File.separator}jni${File.separator}$winLib build${File.separator}classes${File.separator}kotlin${File.separator}main${File.separator}$winLib &
34-
${copyCommand()} src${File.separator}main${File.separator}kotlin${File.separator}dev${File.separator}vyp${File.separator}stringcare${File.separator}plugin${File.separator}internal${File.separator}jni${File.separator}$winLibArm64 build${File.separator}classes${File.separator}kotlin${File.separator}main${File.separator}$winLibArm64
34+
mkdir -p build/resources/main/macos build/resources/main/linux build/resources/main/windows &&
35+
${copyCommand()} "src/main/kotlin/dev/vyp/stringcare/plugin/internal/jni/macos/$osxLib" "build/resources/main/macos/" &&
36+
${copyCommand()} "src/main/kotlin/dev/vyp/stringcare/plugin/internal/jni/linux/$linuxLib" "build/resources/main/linux/" &&
37+
${copyCommand()} "src/main/kotlin/dev/vyp/stringcare/plugin/internal/jni/linux/$linuxLibArm64" "build/resources/main/linux/" &&
38+
${copyCommand()} "src/main/kotlin/dev/vyp/stringcare/plugin/internal/jni/windows/$winLib" "build/resources/main/windows/" &&
39+
${copyCommand()} "src/main/kotlin/dev/vyp/stringcare/plugin/internal/jni/windows/$winLibArm64" "build/resources/main/windows/"
3540
""".trimIndent()
3641

3742
internal fun prepareTask(directory: String): String =
@@ -44,19 +49,19 @@ internal fun prepareTask(directory: String): String =
4449
internal fun buildTask(directory: String): String =
4550
"""
4651
cd $directory &&
47-
${gradleWrapper()} build
52+
${projectGradleWrapper()} build
4853
""".trimIndent()
4954

5055
internal fun basicGradleTask(directory: String): String =
5156
"""
5257
cd $directory &&
53-
${gradleWrapper()} $gradleTaskNameDoctor
58+
${projectGradleWrapper()} $gradleTaskNameDoctor
5459
""".trimIndent()
5560

5661
internal fun obfuscationTestGradleTask(directory: String): String =
5762
"""
5863
cd $directory &&
59-
${gradleWrapper()} $gradleTaskNameObfuscate
64+
${projectGradleWrapper()} $gradleTaskNameObfuscate
6065
""".trimIndent()
6166

6267
internal fun signingReportTask(directory: String): String =

plugin/src/main/kotlin/dev/vyp/stringcare/plugin/internal/VariantApi.kt

Lines changed: 35 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ fun Project.registerVariantObfuscationTasks(
2323

2424
val absoluteProjectPath = buildService.get().absoluteProjectPath()
2525
val moduleNameValue = config.name
26+
val srcJson = toJsonStringList(config.srcFolders)
27+
val strJson = toJsonStringList(config.stringFiles)
28+
val assetsJson = toJsonStringList(config.assetsFiles)
2629
val variantNames = mutableListOf<String>()
2730

2831
androidComponents.onVariants(androidComponents.selector().all()) { variant ->
@@ -37,27 +40,27 @@ fun Project.registerVariantObfuscationTasks(
3740
ObfuscateStringsTask::class.java,
3841
) { t ->
3942
(t as Task).usesService(buildService)
40-
(t as ObfuscateStringsTask).stringCareService.set(buildService)
41-
t.projectPath = absoluteProjectPath
42-
t.moduleName = moduleNameValue
43-
t.variantName = variantName
44-
t.applicationId = applicationId
45-
t.skip = config.skip
46-
t.debug = config.debug
47-
t.mockedFingerprint = config.mockedFingerprint
48-
t.srcFoldersJson = toJsonStringList(config.srcFolders)
49-
t.stringFilesJson = toJsonStringList(config.stringFiles)
50-
t.assetsFilesJson = toJsonStringList(config.assetsFiles)
43+
t.stringCareService.set(buildService)
44+
t.projectPath.set(absoluteProjectPath)
45+
t.moduleName.set(moduleNameValue)
46+
t.variantName.set(variantName)
47+
t.applicationId.set(applicationId)
48+
t.skip.set(config.skip)
49+
t.debug.set(config.debug)
50+
t.mockedFingerprint.set(config.mockedFingerprint)
51+
t.srcFoldersJson.set(srcJson)
52+
t.stringFilesJson.set(strJson)
53+
t.assetsFilesJson.set(assetsJson)
5154
}
5255
tasks.register(
5356
"stringcareAfterMergeResources$variantCapitalized",
5457
RestoreStringsTask::class.java,
5558
) { t ->
5659
(t as Task).usesService(buildService)
57-
(t as RestoreStringsTask).stringCareService.set(buildService)
58-
t.projectPath = absoluteProjectPath
59-
t.moduleName = moduleNameValue
60-
t.skip = config.skip
60+
t.stringCareService.set(buildService)
61+
t.projectPath.set(absoluteProjectPath)
62+
t.moduleName.set(moduleNameValue)
63+
t.skip.set(config.skip)
6164
}
6265
variantNames.add(variantCapitalized)
6366

@@ -66,31 +69,32 @@ fun Project.registerVariantObfuscationTasks(
6669
ObfuscateAssetsTask::class.java,
6770
) { t ->
6871
(t as Task).usesService(buildService)
69-
(t as ObfuscateAssetsTask).stringCareService.set(buildService)
70-
t.projectPath = absoluteProjectPath
71-
t.moduleName = moduleNameValue
72-
t.variantName = variantName
73-
t.applicationId = applicationId
74-
t.skip = config.skip
75-
t.debug = config.debug
76-
t.mockedFingerprint = config.mockedFingerprint
77-
t.srcFoldersJson = toJsonStringList(config.srcFolders)
78-
t.stringFilesJson = toJsonStringList(config.stringFiles)
79-
t.assetsFilesJson = toJsonStringList(config.assetsFiles)
72+
t.stringCareService.set(buildService)
73+
t.projectPath.set(absoluteProjectPath)
74+
t.moduleName.set(moduleNameValue)
75+
t.variantName.set(variantName)
76+
t.applicationId.set(applicationId)
77+
t.skip.set(config.skip)
78+
t.debug.set(config.debug)
79+
t.mockedFingerprint.set(config.mockedFingerprint)
80+
t.srcFoldersJson.set(srcJson)
81+
t.stringFilesJson.set(strJson)
82+
t.assetsFilesJson.set(assetsJson)
8083
}
8184
tasks.register(
8285
"stringcareAfterMergeAssets$variantCapitalized",
8386
RestoreAssetsTask::class.java,
8487
) { t ->
8588
(t as Task).usesService(buildService)
86-
(t as RestoreAssetsTask).stringCareService.set(buildService)
87-
t.projectPath = absoluteProjectPath
88-
t.moduleName = moduleNameValue
89-
t.skip = config.skip
89+
t.stringCareService.set(buildService)
90+
t.projectPath.set(absoluteProjectPath)
91+
t.moduleName.set(moduleNameValue)
92+
t.skip.set(config.skip)
9093
}
9194
}
9295

93-
project.afterEvaluate {
96+
// AGP registers merge/process tasks after variant API callbacks; use afterEvaluate + findByName (nullable).
97+
afterEvaluate {
9498
for (variantCapitalized in variantNames) {
9599
val mergeRes = "merge${variantCapitalized}Resources"
96100
val beforeRes = "stringcareBeforeMergeResources$variantCapitalized"

0 commit comments

Comments
 (0)