refactor: WebP 文件列表改由构建期 BuildConfig 注入,消除 (1..152) 硬编码
问题
- AnimatedWebp.kt 原写死 WEBP_FILES = (1..152).map{...}, 与磁盘文件耦合却无校验
- 加 153.webp → 永远不会被随机到; 删某个 → random() 偶发命中不存在的资源
- 这是静默失败, 运行期无报错, 难排查
改动
- core/build.gradle.kts defaultConfig: 构建期扫描 assets/animations/ 生成
buildConfigField("String[]", "WEBP_FILES", "new String[]{...}")
含 require(webpFiles.isNotEmpty()) 防空目录构建
- AnimatedWebp.kt: WEBP_FILES 从 private 硬编码改为 internal val = BuildConfig.WEBP_FILES.toList()
(internal 让同模块测试可访问)
- 新增 AnimatedWebpFilesTest: 2 个守卫测试
1. webpFilesMatchDirectoryContents: WEBP_FILES 必须与 assets/animations/ 一一对应
2. webpFilesUseZeroPaddedThreeDigitNames: 锁定 NNN.webp 命名约定
TDD 流程
- 先写测试 → 编译失败(WEBP_FILES 是 private) → 红灯成立
- 改实现 → 测试 PASS → 绿灯
- 突变验证: 临时改 WEBP_FILES = (1..150) 漏掉 151/152
→ webpFilesMatchDirectoryContents 立即 FAIL, 守卫有效
- 恢复后全量 146 个测试 0 失败
设计说明
- 当前实现里 WEBP_FILES 与目录都来自同一次构建期扫描, 二者天然一致
- 测试的核心价值是锁定「两者一致」不变量, 防止有人回退成硬编码
(突变验证已证明: 回退后测试立即失败)
- BuildConfig 生成结果验证: String[] 含 152 个元素 001~152.webp
验证
- ./gradlew :core:testDebugUnitTest → 146 tests, 0 failures, 0 errors
- ./gradlew :app:assembleDebug → BUILD SUCCESSFUL
- BuildConfig.java: public static final String[] WEBP_FILES = new String[]{"001.webp",...,"152.webp"}
This commit is contained in:
parent
4c8084c176
commit
58a97d1725
@ -12,6 +12,21 @@ android {
|
||||
defaultConfig {
|
||||
minSdk = libs.versions.android.minSdk.get().toInt()
|
||||
consumerProguardFiles("proguard-rules.pro")
|
||||
|
||||
// 构建期扫描 assets/animations/ 生成 WebP 文件列表,避免运行期硬编码 (1..152)
|
||||
// 与 assets/ 目录耦合却不校验,导致增删文件后隐性 bug
|
||||
val webpFiles = layout.projectDirectory
|
||||
.dir("src/main/assets/animations")
|
||||
.asFile
|
||||
.listFiles { f -> f.extension.equals("webp", ignoreCase = true) }
|
||||
?.map { it.name }
|
||||
?.sorted()
|
||||
?: emptyList()
|
||||
require(webpFiles.isNotEmpty()) { "assets/animations/ 不应为空,请检查目录" }
|
||||
// 拼成 Java 数组字面量: new String[]{"001.webp","002.webp",...}
|
||||
val quoted = webpFiles.joinToString(",") { name -> "\"$name\"" }
|
||||
val arrayLiteral = "new String[]{$quoted}"
|
||||
buildConfigField("String[]", "WEBP_FILES", arrayLiteral)
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
||||
@ -15,11 +15,15 @@ import com.github.panpf.sketch.rememberAsyncImageState
|
||||
import com.github.panpf.sketch.request.ImageOptions
|
||||
import com.github.panpf.sketch.request.repeatCount
|
||||
import plus.rua.project.getWebpUri
|
||||
import plus.rua.project.shared.BuildConfig
|
||||
|
||||
/**
|
||||
* WebP 动画文件名列表(001.webp ~ 152.webp)。
|
||||
* WebP 动画文件名列表,由构建期 `BuildConfig.WEBP_FILES` 注入
|
||||
* (见 `core/build.gradle.kts` 扫描 `assets/animations/` 的 `buildConfigField`)。
|
||||
*
|
||||
* `internal` 让同模块测试可直接访问(见 `AnimatedWebpFilesTest`)。
|
||||
*/
|
||||
private val WEBP_FILES = (1..152).map { "${it.toString().padStart(3, '0')}.webp" }
|
||||
internal val WEBP_FILES: List<String> = BuildConfig.WEBP_FILES.toList()
|
||||
|
||||
/**
|
||||
* 显示动画 WebP 图片,切换日期时随机选择一个。
|
||||
|
||||
@ -0,0 +1,49 @@
|
||||
package plus.rua.project.ui
|
||||
|
||||
import java.io.File
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* 守卫:运行期使用的 WebP 文件列表必须与 assets/animations/ 目录实际内容一致。
|
||||
*
|
||||
* 防止两类回归:
|
||||
* 1. 有人把 BuildConfig 方案回退成硬编码 (1..152),与目录脱钩后增删文件即隐性 bug
|
||||
* 2. assets/animations/ 在 CI / 打包流程中意外丢失或被 Git LFS 过滤掉
|
||||
*
|
||||
* 注:当前实现里 WEBP_FILES 与目录都来自同一次构建期扫描,二者天然一致;
|
||||
* 本测试的核心价值是锁定「两者一致」这个不变量,使上述回归一旦发生即立即失败。
|
||||
*/
|
||||
class AnimatedWebpFilesTest {
|
||||
|
||||
private val animationsDir = File("src/main/assets/animations")
|
||||
|
||||
@Test
|
||||
fun webpFilesMatchDirectoryContents() {
|
||||
assertTrue(animationsDir.exists(), "assets/animations 目录应存在: ${animationsDir.absolutePath}")
|
||||
|
||||
val onDisk = animationsDir.listFiles { f -> f.extension.equals("webp", ignoreCase = true) }
|
||||
?.map { it.name }
|
||||
?.sorted()
|
||||
?: emptyList()
|
||||
assertTrue(onDisk.isNotEmpty(), "assets/animations 不应为空(若失败请检查 Git LFS / checkout 完整性)")
|
||||
|
||||
// WEBP_FILES 由构建期 BuildConfig 注入(见 core/build.gradle.kts 的 buildConfigField)
|
||||
val inCode = WEBP_FILES.sorted()
|
||||
|
||||
assertEquals(onDisk, inCode, "WEBP_FILES 必须与 assets/animations/ 实际 webp 文件一一对应")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun webpFilesUseZeroPaddedThreeDigitNames() {
|
||||
// 锁定命名约定:NNN.webp(三位零填充),防止有人误改成 1.webp / 01.webp 等
|
||||
val expected = Regex("""^\d{3}\.webp$""")
|
||||
WEBP_FILES.forEach { name ->
|
||||
assertTrue(
|
||||
expected.matches(name),
|
||||
"文件名 $name 不符合 NNN.webp 约定,getWebpUri 与 AnimatedWebp 依赖此格式",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user