test(android): 光标映射 8 例 + 帧头拆包 golden bytes 6 例
4b/V-D1 的单元侧判据(跨端契约 + 映射语义): - CursorMarkerTest:中心/满量程/(0,0) 合法/1:1 恒等映射;null、尺寸未就绪、 越界全分支返回 null。origin_maps_not_treated_as_none 锁最危险的错—— 把左上角误当无光标(协议用 Option 的全部理由) - RustBridgeTest:goldenHeader 逐字节手抄(与 android-ffi 写端测试同一组常数对拍), 锁 [25]/[26..28]/[28..30] 三个新偏移 + 有符号 i16 + 截断缓冲防御 + 空载荷边界 - R-8 反向验证:读偏移 26→27 后 4 例坐标测试确实红(已改回); CursorMarker 边界 >= 改 <= 后 origin 用例确实红(已改回) JVM 全量 60 passed / 0 failed(--rerun-tasks 新鲜跑,XML 时间戳核对)。
This commit is contained in:
parent
9a6f6962d2
commit
efcaeaad26
@ -0,0 +1,115 @@
|
|||||||
|
package com.verify.remote
|
||||||
|
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertNotNull
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 4b/V-D1:光标标记映射的纯逻辑(协议 v5)。
|
||||||
|
*
|
||||||
|
* 这些断言对应验收判据,不是「跑通就算」:
|
||||||
|
* - V-D1「标记位置=映射值」→ [center_maps_to_view_center] / [full_extent_linear_map]
|
||||||
|
* - 「(0,0) 是合法位置」→ [origin_maps_not_treated_as_none](协议用 Option 的全部理由)
|
||||||
|
* - 跨信任边界自证 → 越界与负值一律 null(Linux 后端未实现/被篡改时的防线)
|
||||||
|
*/
|
||||||
|
class CursorMarkerTest {
|
||||||
|
|
||||||
|
// ---- 核心等比映射 ----
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 横屏典型几何:桌面 1920x1080,视图 1080x607(满宽 letterbox)。
|
||||||
|
* 桌面中心应映射到视图中心。这是 V-D1 的锚点用例——真机 proof 就是把
|
||||||
|
* SetCursorPos 打到桌面中心,再在截图里断言标记簇心在视图中心 ±6px。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun center_maps_to_view_center() {
|
||||||
|
val p = CursorMarker.mapToView(
|
||||||
|
desktopW = 1920, desktopH = 1080,
|
||||||
|
cursor = 960 to 540,
|
||||||
|
viewW = 1080, viewH = 607,
|
||||||
|
)
|
||||||
|
assertNotNull(p)
|
||||||
|
assertEquals(540f, p!!.x, 1f)
|
||||||
|
assertEquals(303.5f, p.y, 1f)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 满量程线性映射:桌面右下角 (1919,1079) → 视图右下角附近,
|
||||||
|
* 且不越过视图边界(映射后必须仍在画面内可画)。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun full_extent_linear_map() {
|
||||||
|
val p = CursorMarker.mapToView(1920, 1080, 1919 to 1079, 1080, 607)
|
||||||
|
assertNotNull(p)
|
||||||
|
assertEquals(1080f * 1919 / 1920f, p!!.x, 0.5f)
|
||||||
|
assertEquals(607f * 1079 / 1080f, p.y, 0.5f)
|
||||||
|
assertTrue("映射 x 不得越过视图右边界", p.x < 1080f)
|
||||||
|
assertTrue("映射 y 不得越过视图下边界", p.y < 607f)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (0,0) 必须映射成视图左上角,**而不是 null**。
|
||||||
|
* 这条锁住最危险的错误:实现把「左上角」和「没有光标」混为一谈
|
||||||
|
* (比如用 (0,0) 当缺省值再判 ==0 返回 null)。协议专门用 Option 区分二者,
|
||||||
|
* 这层区分在 Kotlin 侧不能被抹平。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun origin_maps_not_treated_as_none() {
|
||||||
|
val p = CursorMarker.mapToView(1920, 1080, 0 to 0, 1080, 607)
|
||||||
|
assertNotNull("左上角是合法光标位置,不得当成无光标丢弃")
|
||||||
|
assertEquals(0f, p!!.x, 0.001f)
|
||||||
|
assertEquals(0f, p.y, 0.001f)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 1:1 场景(视图尺寸恰等于桌面):坐标原样透传。 */
|
||||||
|
@Test
|
||||||
|
fun identity_when_sizes_match() {
|
||||||
|
val p = CursorMarker.mapToView(1280, 720, 640 to 360, 1280, 720)
|
||||||
|
assertEquals(640f, p!!.x, 0.001f)
|
||||||
|
assertEquals(360f, p.y, 0.001f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- null 分支:一律「不画」而非画到别处 ----
|
||||||
|
|
||||||
|
/** cursor=null(隐藏/离屏/后端未实现)→ null。 */
|
||||||
|
@Test
|
||||||
|
fun null_cursor_returns_null() {
|
||||||
|
assertNull(CursorMarker.mapToView(1920, 1080, null, 1080, 607))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任一尺寸未就绪(0 或负)→ null,绝不按 0 除得到 NaN/Infinity。
|
||||||
|
* 覆盖四种:视图未测量(width 0,letterbox onMeasure 前)、
|
||||||
|
* 桌面尺寸还没从 AuthOk 取到、以及负值这种异常。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun not_ready_or_degenerate_dimensions_return_null() {
|
||||||
|
assertNull(CursorMarker.mapToView(0, 1080, 10 to 10, 1080, 607)) // 桌面宽未就绪
|
||||||
|
assertNull(CursorMarker.mapToView(1920, 0, 10 to 10, 1080, 607)) // 桌面高未就绪
|
||||||
|
assertNull(CursorMarker.mapToView(1920, 1080, 10 to 10, 0, 607)) // 视图未测量
|
||||||
|
assertNull(CursorMarker.mapToView(1920, 1080, 10 to 10, 1080, 0)) // 视图高 0
|
||||||
|
assertNull(CursorMarker.mapToView(-5, 1080, 10 to 10, 1080, 607)) // 异常负尺寸
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 越界坐标 → null(不画)。这是跨信任边界的自证:被控端 `fold_cursor` 本应折叠掉
|
||||||
|
* 越界值,但协议把对端字节按不可信输入处理——若折叠漏网或被篡改,这里兜住,
|
||||||
|
* 否则标记会画到画面外(甚至映射出一个「看似合理」的错位置)。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun out_of_desktop_bounds_returns_null() {
|
||||||
|
assertNull(CursorMarker.mapToView(1920, 1080, 1920 to 500, 1080, 607)) // 右边界外一列
|
||||||
|
assertNull(CursorMarker.mapToView(1920, 1080, 500 to 1080, 1080, 607)) // 下边界外一行
|
||||||
|
assertNull(CursorMarker.mapToView(1920, 1080, -1 to 500, 1080, 607)) // 负 X(副屏穿透)
|
||||||
|
assertNull(CursorMarker.mapToView(1920, 1080, 500 to -1, 1080, 607)) // 负 Y
|
||||||
|
assertNull(CursorMarker.mapToView(1920, 1080, 3000 to 500, 1080, 607)) // 远超桌面
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 恰好界内 (desktopW-1, desktopH-1) 必须画(与上一条配对,钉死 >= 还是 > 的边界)。 */
|
||||||
|
@Test
|
||||||
|
fun exactly_in_bounds_draws() {
|
||||||
|
assertNotNull(CursorMarker.mapToView(1920, 1080, 1919 to 1079, 1080, 607))
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,106 @@
|
|||||||
|
package com.verify.remote
|
||||||
|
|
||||||
|
import org.junit.Assert.assertArrayEquals
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 4b:[FrameCodec] 帧头拆包的**跨端字节契约**测试(golden bytes)。
|
||||||
|
*
|
||||||
|
* 为什么值这个钱:android-ffi/src/lib.rs 按 buf[0]/[1..9]/[9..17]/[17..21]/[21..25]/
|
||||||
|
* [25]/[26..28]/[28..30] 写字节,Kotlin 按同样的偏移读。**任何一侧挪一位**(尤其 v5
|
||||||
|
* 从 25→30 这次),Rust 编译过、Kotlin 也编译过、真机能连上——只是标记画到错误位置
|
||||||
|
* 或永远不显示,静默失败(全局规则:核过 Rust 一端不等于核过 Kotlin 另一端)。
|
||||||
|
*
|
||||||
|
* 期望值用 goldenHeader 按 Rust 侧写入布局**手算**生成(`to_le_bytes()` 的逐字节结果)。
|
||||||
|
* Rust 侧若改偏移或改字节序,这里的对拍就红。这是唯一能在 JVM 里锁住 JNI 二进制布局的
|
||||||
|
* 手段(真机跑不了单测)。拆包逻辑之所以住在顶层 [FrameCodec] 而不是 RustBridge object,
|
||||||
|
* 正因为后者带 loadLibrary 的 init,测试里不可达。
|
||||||
|
*/
|
||||||
|
class RustBridgeTest {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 「IDR、seq=0x0102030405060708、ts=0x0011223344556677、桌面 1920x1080、
|
||||||
|
* 光标 has=x/y」的 30 字节头,逐字节 = android-ffi 的写入布局。
|
||||||
|
*/
|
||||||
|
private fun goldenHeader(hasCursor: Int, x: Int, y: Int): ByteArray {
|
||||||
|
val bytes = listOf(
|
||||||
|
0x01, // [0] keyframe = true
|
||||||
|
// [1..9] seq = 0x0102030405060708 LE
|
||||||
|
0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01,
|
||||||
|
// [9..17] ts = 0x0011223344556677 LE
|
||||||
|
0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x00,
|
||||||
|
// [17..21] width = 1920 (0x0780) LE
|
||||||
|
0x80, 0x07, 0x00, 0x00,
|
||||||
|
// [21..25] height = 1080 (0x0438) LE
|
||||||
|
0x38, 0x04, 0x00, 0x00,
|
||||||
|
// [25] has_cursor
|
||||||
|
hasCursor,
|
||||||
|
// [26..28] cursor.x i16 LE
|
||||||
|
x and 0xFF, (x shr 8) and 0xFF,
|
||||||
|
// [28..30] cursor.y i16 LE
|
||||||
|
y and 0xFF, (y shr 8) and 0xFF,
|
||||||
|
)
|
||||||
|
return bytes.map { it.toByte() }.toByteArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 光标 Some((960,540)):逐字段核对,锁死 [25]/[26..28]/[28..30] 三个新偏移。 */
|
||||||
|
@Test
|
||||||
|
fun parses_v5_header_with_cursor() {
|
||||||
|
val payload = byteArrayOf(0xAA.toInt().toByte(), 0xBB.toInt().toByte(), 0xCC.toInt().toByte())
|
||||||
|
val raw = goldenHeader(hasCursor = 1, x = 960, y = 540) + payload
|
||||||
|
val f = FrameCodec.parse(raw)!!
|
||||||
|
assertEquals(true, f.keyframe)
|
||||||
|
assertEquals(0x0102030405060708L, f.seq)
|
||||||
|
assertEquals(0x0011223344556677L, f.tsMs)
|
||||||
|
assertEquals(1920, f.width)
|
||||||
|
assertEquals(1080, f.height)
|
||||||
|
assertEquals(960 to 540, f.cursor)
|
||||||
|
assertArrayEquals(payload, f.payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* has_cursor=0 时**即便坐标字节是全 1 位模式(0xFFFF = -1)也必须读成 null**。
|
||||||
|
* 这条锁住 `if (hasCursor)` 的判断:若误判成「非零即有」,(-1,-1) 会穿透成一个
|
||||||
|
* 看似合法的错位置——正是协议用独立 has 字节而非「0 表示无」的原因。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun has_cursor_zero_means_none_regardless_of_coord_bytes() {
|
||||||
|
// 故意把 x/y 字节填成 -1 的位模式(0xFFFF)
|
||||||
|
val raw = goldenHeader(hasCursor = 0, x = 0xFFFF, y = 0xFFFF) + byteArrayOf(0x00)
|
||||||
|
val f = FrameCodec.parse(raw)!!
|
||||||
|
assertNull("has=0 时坐标字节无意义,必须 null", f.cursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 负光标坐标(有符号 i16)必须正确读回——agent 折叠后不该出现,但拆包层要忠实。 */
|
||||||
|
@Test
|
||||||
|
fun reads_signed_cursor_coords() {
|
||||||
|
val raw = goldenHeader(hasCursor = 1, x = (-1) and 0xFFFF, y = (-32768) and 0xFFFF) + byteArrayOf(0)
|
||||||
|
val f = FrameCodec.parse(raw)!!
|
||||||
|
assertEquals(-1 to -32768, f.cursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 光标坐标取 i16 两端边界 (32767, -32768) 往返,证明 readI16 符号扩展正确。 */
|
||||||
|
@Test
|
||||||
|
fun cursor_i16_extremes() {
|
||||||
|
val raw = goldenHeader(hasCursor = 1, x = 32767, y = -32768) + byteArrayOf(0)
|
||||||
|
val f = FrameCodec.parse(raw)!!
|
||||||
|
assertEquals(32767 to -32768, f.cursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 短于 30 字节的缓冲(旧 25 字节布局或截断帧)必须返回 null,不能切片越界崩溃。 */
|
||||||
|
@Test
|
||||||
|
fun rejects_truncated_buffer() {
|
||||||
|
assertNull(FrameCodec.parse(ByteArray(29)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 恰好 30 字节(空载荷)是合法的最小帧;载荷长度为 0 而非 null。 */
|
||||||
|
@Test
|
||||||
|
fun accepts_exact_header_with_empty_payload() {
|
||||||
|
val raw = goldenHeader(hasCursor = 1, x = 100, y = 200)
|
||||||
|
val f = FrameCodec.parse(raw)!!
|
||||||
|
assertEquals(100 to 200, f.cursor)
|
||||||
|
assertEquals(0, f.payload.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user