test(android): JUnit 测试基础设施 + 19 个纯逻辑单测(3a/U7,Q3=A 批准)
仅 testImplementation junit:4.13.2(测试作用域,不进 APK,runtime deps 仍空); versionName→0.1-phase3a。覆盖:WinScancode 映射表(右 Shift 非扩展/UNKNOWN 落 null/ F 键值钉死待 U6)、ConnectErrorText(每类中文非空、编号契约、未知不猜)、 planBackspaces((0,0)/(after>0)/截断 64)。含 mutation 反向验证:断言写反即红。
This commit is contained in:
parent
718b66617f
commit
bb3cf6b09b
@ -11,8 +11,8 @@ android {
|
|||||||
applicationId = "com.verify.remote"
|
applicationId = "com.verify.remote"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 1
|
versionCode = 2
|
||||||
versionName = "0.1-phase1"
|
versionName = "0.1-phase3a"
|
||||||
// 阶段 1 只测真机 arm64;x86_64 留给模拟器回归
|
// 阶段 1 只测真机 arm64;x86_64 留给模拟器回归
|
||||||
ndk {
|
ndk {
|
||||||
abiFilters += listOf("arm64-v8a")
|
abiFilters += listOf("arm64-v8a")
|
||||||
@ -25,6 +25,15 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3a/U7:安卓测试基础设施(Q3=A,用户 2026-10-31 批准)。
|
||||||
|
// **仅测试作用域**:junit 不进 APK,runtime dependencies 保持为空。
|
||||||
|
testOptions {
|
||||||
|
unitTests {
|
||||||
|
// android.jar 桩方法抛 "not mocked"——本工程的单测刻意只测纯逻辑
|
||||||
|
// (不构造 Android 对象),此开关只兜常量引用类的类加载副作用。
|
||||||
|
isReturnDefaultValues = true
|
||||||
|
}
|
||||||
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
targetCompatibility = JavaVersion.VERSION_11
|
||||||
@ -53,4 +62,6 @@ android {
|
|||||||
dependencies {
|
dependencies {
|
||||||
// 阶段 1 刻意零第三方 UI 依赖:纯 View 代码布局,减少变量。
|
// 阶段 1 刻意零第三方 UI 依赖:纯 View 代码布局,减少变量。
|
||||||
// AndroidX 在阶段 2 做正式 UI 时再引入。
|
// AndroidX 在阶段 2 做正式 UI 时再引入。
|
||||||
|
// 3a/U7(Q3=A 批准):仅 JUnit 测试作用域,不进 APK。
|
||||||
|
testImplementation("junit:junit:4.13.2")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,80 @@
|
|||||||
|
package com.verify.remote
|
||||||
|
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertNotEquals
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 3a/U7:连接错误编号→文案映射。
|
||||||
|
* 编号契约的另一端是 Rust(`viewer_core::ConnectErrorKind::code`,
|
||||||
|
* 由 `cargo test -p viewer-core ffi_error_codes_are_stable_and_unique` 锁死),
|
||||||
|
* 这里锁 Kotlin 侧的完整覆盖与文案质量——两端一致才算契约成立。
|
||||||
|
*/
|
||||||
|
class ConnectErrorTextTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun every_known_kind_has_actionable_chinese_text() {
|
||||||
|
val kinds = listOf(
|
||||||
|
ConnectErrorText.KIND_RESOLVE,
|
||||||
|
ConnectErrorText.KIND_TIMEOUT,
|
||||||
|
ConnectErrorText.KIND_REFUSED,
|
||||||
|
ConnectErrorText.KIND_AUTH,
|
||||||
|
ConnectErrorText.KIND_VERSION,
|
||||||
|
ConnectErrorText.KIND_OTHER,
|
||||||
|
ConnectErrorText.KIND_FFI_SELF,
|
||||||
|
)
|
||||||
|
for (k in kinds) {
|
||||||
|
val s = ConnectErrorText.summary(k)
|
||||||
|
assertTrue("kind=$k 必须有文案", !s.isNullOrEmpty())
|
||||||
|
// 项目纪律:用户可见输出中文(文案里至少含一个 CJK 字符)
|
||||||
|
assertTrue("kind=$k 文案必须中文", s!!.any { c -> c.code in 0x4E00..0x9FFF })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun codes_match_ffi_contract() {
|
||||||
|
// 期望值 = android-ffi / viewer-core 的 code() 表(改动必须三处同步)
|
||||||
|
assertEquals(1, ConnectErrorText.KIND_RESOLVE)
|
||||||
|
assertEquals(2, ConnectErrorText.KIND_TIMEOUT)
|
||||||
|
assertEquals(3, ConnectErrorText.KIND_REFUSED)
|
||||||
|
assertEquals(4, ConnectErrorText.KIND_AUTH)
|
||||||
|
assertEquals(5, ConnectErrorText.KIND_VERSION)
|
||||||
|
assertEquals(9, ConnectErrorText.KIND_OTHER)
|
||||||
|
assertEquals(-1, ConnectErrorText.KIND_FFI_SELF)
|
||||||
|
assertEquals(0, ConnectErrorText.KIND_NONE)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun texts_are_distinct_per_failure_mode() {
|
||||||
|
// 五种可区分失败不能给同一句文案(V-A2 的映射前提)
|
||||||
|
val texts = listOf(
|
||||||
|
ConnectErrorText.KIND_RESOLVE,
|
||||||
|
ConnectErrorText.KIND_TIMEOUT,
|
||||||
|
ConnectErrorText.KIND_REFUSED,
|
||||||
|
ConnectErrorText.KIND_AUTH,
|
||||||
|
ConnectErrorText.KIND_VERSION,
|
||||||
|
).map { ConnectErrorText.summary(it)!! }
|
||||||
|
assertEquals(texts.size, texts.distinct().size)
|
||||||
|
for (t in texts) {
|
||||||
|
assertNotEquals("连接失败", t) // 可分类的不得落兜底文案
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun unknown_kind_falls_back_without_claiming_a_cause() {
|
||||||
|
// 编号漂移时不猜语义:兜底文案 + 带未知编号(R-8:宁可少说不可说错)
|
||||||
|
assertNull(ConnectErrorText.summary(42))
|
||||||
|
val line = ConnectErrorText.firstLine(42)
|
||||||
|
assertTrue("实际: $line", line.contains("42"))
|
||||||
|
assertTrue("实际: $line", line.startsWith("连接失败"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun firstLine_uses_summary_for_known_kinds() {
|
||||||
|
for (k in listOf(1, 2, 3, 4, 5, 9, -1)) {
|
||||||
|
assertEquals(ConnectErrorText.summary(k), ConnectErrorText.firstLine(k))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,46 @@
|
|||||||
|
package com.verify.remote
|
||||||
|
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 3a/U7:退格决策纯函数。原逻辑在 `RemoteInputConnection.deleteBackspaces`
|
||||||
|
* (构造链依赖 android.jar 桩,JVM 里不可测),3a 提取为 [planBackspaces] 后
|
||||||
|
* 首次可单测。期望值对照 2b 真机行为(语义不变,见 RemoteInputConnection 注释)。
|
||||||
|
*/
|
||||||
|
class PlanBackspacesTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun zero_zero_is_noop() {
|
||||||
|
assertEquals(BackspaceAction.Noop, planBackspaces(0, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun after_positive_is_unsupported_and_reports_both_counts() {
|
||||||
|
// U3:forward-delete 形态本地无远端真值,必须显式 Unsupported(可见后果),
|
||||||
|
// 不得静默当退格执行——before 值要随形态带出去供 debug 通道显示
|
||||||
|
assertEquals(BackspaceAction.Unsupported(2, 1), planBackspaces(2, 1))
|
||||||
|
assertEquals(BackspaceAction.Unsupported(0, 3), planBackspaces(0, 3))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun before_only_maps_to_backspace_count() {
|
||||||
|
// IME 退格最常见形态(R-9 实测)
|
||||||
|
assertEquals(BackspaceAction.Backspaces(3), planBackspaces(3, 0))
|
||||||
|
assertEquals(BackspaceAction.Backspaces(1), planBackspaces(1, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun count_is_capped_at_limit() {
|
||||||
|
// 防 IME 异常大值刷爆消息队列;截断语义必须发生在计划层(执行层无感知上限)
|
||||||
|
assertEquals(BackspaceAction.Backspaces(64), planBackspaces(64, 0))
|
||||||
|
assertEquals(BackspaceAction.Backspaces(64), planBackspaces(999, 0))
|
||||||
|
assertEquals(BackspaceAction.Backspaces(MAX_BACKSPACE), planBackspaces(1_000_000, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun MAX_BACKSPACE_constant_is_pinned() {
|
||||||
|
// 与 RemoteInputConnection 的 companion 原值一致(提取时不得改数)
|
||||||
|
assertEquals(64, MAX_BACKSPACE)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,82 @@
|
|||||||
|
package com.verify.remote
|
||||||
|
|
||||||
|
import android.view.KeyEvent
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 3a/U7 首批 JVM 单测:扫描码映射表(此前只能真机点验)。
|
||||||
|
*
|
||||||
|
* `KeyEvent.KEYCODE_*` 是 Java 编译期常量(static final int),引用会被内联进
|
||||||
|
* 测试字节码——JVM 单测不触发 android.jar 桩类加载,所以这里不需要 Robolectric。
|
||||||
|
* 期望值来自项目内部权威源 `viewer/src/keymap.rs`(同 R-1:不信记忆,对照项目已有用法)。
|
||||||
|
*/
|
||||||
|
class WinScancodeTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun enter_is_make_0x1C_not_extended() {
|
||||||
|
assertEquals(WinKey(0x1C, false), WinScancode.of(KeyEvent.KEYCODE_ENTER))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun backspace_is_0x0E() {
|
||||||
|
assertEquals(WinKey(0x0E, false), WinScancode.of(KeyEvent.KEYCODE_DEL))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun forward_delete_is_0x53_extended() {
|
||||||
|
assertEquals(WinKey(0x53, true), WinScancode.of(KeyEvent.KEYCODE_FORWARD_DEL))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun arrows_are_extended() {
|
||||||
|
assertEquals(WinKey(0x48, true), WinScancode.of(KeyEvent.KEYCODE_DPAD_UP))
|
||||||
|
assertEquals(WinKey(0x50, true), WinScancode.of(KeyEvent.KEYCODE_DPAD_DOWN))
|
||||||
|
assertEquals(WinKey(0x4B, true), WinScancode.of(KeyEvent.KEYCODE_DPAD_LEFT))
|
||||||
|
assertEquals(WinKey(0x4D, true), WinScancode.of(KeyEvent.KEYCODE_DPAD_RIGHT))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun right_shift_is_NOT_extended_with_its_own_scancode() {
|
||||||
|
// 微软 Keyboard Input Overview:右 Shift 不是扩展键,它有独立扫描码 0x36
|
||||||
|
// (2b 的 D4 偏差修正,回归锁死)
|
||||||
|
assertEquals(WinKey(0x36, false), WinScancode.of(KeyEvent.KEYCODE_SHIFT_RIGHT))
|
||||||
|
assertEquals(WinKey(0x2A, false), WinScancode.of(KeyEvent.KEYCODE_SHIFT_LEFT))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun right_ctrl_and_alt_are_extended_shared_scancodes() {
|
||||||
|
assertEquals(WinKey(0x1D, true), WinScancode.of(KeyEvent.KEYCODE_CTRL_RIGHT))
|
||||||
|
assertEquals(WinKey(0x1D, false), WinScancode.of(KeyEvent.KEYCODE_CTRL_LEFT))
|
||||||
|
assertEquals(WinKey(0x38, true), WinScancode.of(KeyEvent.KEYCODE_ALT_RIGHT))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun letters_and_digits_match_keymap_rs() {
|
||||||
|
// 抽查与 keymap.rs 交集的字母/数字(Set-1 make code)
|
||||||
|
assertEquals(WinKey(0x1E, false), WinScancode.of(KeyEvent.KEYCODE_A))
|
||||||
|
assertEquals(WinKey(0x2C, false), WinScancode.of(KeyEvent.KEYCODE_Z))
|
||||||
|
assertEquals(WinKey(0x02, false), WinScancode.of(KeyEvent.KEYCODE_1))
|
||||||
|
assertEquals(WinKey(0x0B, false), WinScancode.of(KeyEvent.KEYCODE_0))
|
||||||
|
assertEquals(WinKey(0x39, false), WinScancode.of(KeyEvent.KEYCODE_SPACE))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun unmapped_and_unknown_keycodes_return_null() {
|
||||||
|
// KEYCODE_UNKNOWN=0:软键盘可打印字符的形态,必须落 null 让调用方走 Text 通道,
|
||||||
|
// 不能兜底成任意扫描码(2b D13 的根因之一)
|
||||||
|
assertNull(WinScancode.of(KeyEvent.KEYCODE_UNKNOWN))
|
||||||
|
assertNull(WinScancode.of(0))
|
||||||
|
assertNull(WinScancode.of(-1))
|
||||||
|
assertNull(WinScancode.of(9999))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun f_keys_pinned_until_u6_physical_keyboard_verification() {
|
||||||
|
// N5/U6:F1-F10 来自 Set-1 常识与 keymap.rs 交集,未在 Windows 逐一对账;
|
||||||
|
// 这里锁「当前值不变」,防止无声漂移——真机接物理键盘验证后(U6)才允许改期望值
|
||||||
|
assertEquals(WinKey(0x3B, false), WinScancode.of(KeyEvent.KEYCODE_F1))
|
||||||
|
assertEquals(WinKey(0x44, false), WinScancode.of(KeyEvent.KEYCODE_F10))
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user