refactor(android-ffi): 抽 pack_frame_header 并加写端字面量对拍单测
步骤 4 的跨端契约闭环:把 30 字节头构造从 nativeNextFrame 内联提取成纯函数, 写端只有一条路径(消除内联/函数两处并存 = R-5 同源漂移)。 新增 host 可跑的 3 条单测(#[cfg(test)],进 --workspace --exclude linux-agent 口径): - header_bytes_match_the_declared_layout:逐字节手抄常数,与 Kotlin 侧 RustBridgeTest.goldenHeader 是同一组——两侧各握一份手抄,改任一偏移另一侧必红 (只锁读端有盲区:Kotlin 字面量若本身抄错 Rust 布局会跟着一起错还全绿) - none_cursor_writes_zero_flag_and_zero_coords:None 分支 has=0 且坐标字节不残留脏值 - negative_coords_keep_bit_pattern:i16 负坐标位模式忠实 android-ffi 在 host(gnu) cargo test 可编译可跑,实测 3 passed。
This commit is contained in:
parent
8eed1e20e1
commit
4379b01d41
@ -34,6 +34,90 @@ use viewer_core::{ConnectConfig, Session};
|
|||||||
/// (v5:25→30,尾部三字段是光标;放末尾保证 Kotlin 旧偏移量全部不移动)
|
/// (v5:25→30,尾部三字段是光标;放末尾保证 Kotlin 旧偏移量全部不移动)
|
||||||
const FRAME_HEADER_LEN: usize = 30;
|
const FRAME_HEADER_LEN: usize = 30;
|
||||||
|
|
||||||
|
/// 把一个帧的元数据打包成定长头字节(**不含载荷**)。
|
||||||
|
///
|
||||||
|
/// 独立成纯函数并被单测钉住的原因:Kotlin 侧 `FrameCodec` 按同样的偏移读这些字节,
|
||||||
|
/// 任一侧挪一位,两端都编译得过、真机也连得上,只是标记画错位置——静默失败。
|
||||||
|
/// 单测用**手写字面量**断言(不是"算出来的期望值"),这样 RustBridgeTest 的
|
||||||
|
/// golden bytes 与这里是同一组常数对拍:改了写入布局,两边至少一边必红。
|
||||||
|
fn pack_frame_header(f: &viewer_core::Frame) -> [u8; FRAME_HEADER_LEN] {
|
||||||
|
let mut head = [0u8; FRAME_HEADER_LEN];
|
||||||
|
head[0] = f.keyframe as u8;
|
||||||
|
head[1..9].copy_from_slice(&f.seq.to_le_bytes());
|
||||||
|
head[9..17].copy_from_slice(&f.ts_ms.to_le_bytes());
|
||||||
|
head[17..21].copy_from_slice(&f.width.to_le_bytes());
|
||||||
|
head[21..25].copy_from_slice(&f.height.to_le_bytes());
|
||||||
|
// v5 光标三字段(偏移 25/26/28):None → has=0 且坐标字节保持 0(无意义)。
|
||||||
|
// 用显式 match 而不是 `f.cursor.is_some() as u8`——两处读同一个 Option,
|
||||||
|
// 任何「Some 但忘写坐标」的编辑错误都会在这里暴露。
|
||||||
|
match f.cursor {
|
||||||
|
Some((x, y)) => {
|
||||||
|
head[25] = 1;
|
||||||
|
head[26..28].copy_from_slice(&x.to_le_bytes());
|
||||||
|
head[28..30].copy_from_slice(&y.to_le_bytes());
|
||||||
|
}
|
||||||
|
None => head[25] = 0,
|
||||||
|
}
|
||||||
|
head
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// 构造最小 Frame(载荷只放一字节,本测试只关心定长头)。
|
||||||
|
fn frame(seq: u64, cursor: Option<(i16, i16)>) -> viewer_core::Frame {
|
||||||
|
viewer_core::Frame {
|
||||||
|
width: 1920,
|
||||||
|
height: 1080,
|
||||||
|
ts_ms: 0x0011_2233_4455_6677,
|
||||||
|
seq,
|
||||||
|
keyframe: true,
|
||||||
|
cursor,
|
||||||
|
payload: vec![0xAB],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **写端**字面量对拍:期望值逐字节手写,与 Kotlin 侧 RustBridgeTest 的
|
||||||
|
/// goldenHeader 是同一组常数。两侧各握一份手抄常数,改任何一侧的偏移,
|
||||||
|
/// 另一侧必红——这才是跨端契约的闭环(golden 测试只锁读端时有盲区:
|
||||||
|
/// 若 Kotlin 的字面量本身抄错了 Rust 布局,读端测试会跟着一起错还全绿)。
|
||||||
|
#[test]
|
||||||
|
fn header_bytes_match_the_declared_layout() {
|
||||||
|
let head = pack_frame_header(&frame(0x0102_0304_0506_0708, Some((960, 540))));
|
||||||
|
assert_eq!(
|
||||||
|
head,
|
||||||
|
[
|
||||||
|
0x01, // [0] keyframe=true
|
||||||
|
0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, // [1..9] seq=0x0102..0708 LE
|
||||||
|
0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x00, // [9..17] ts LE
|
||||||
|
0x80, 0x07, 0x00, 0x00, // [17..21] w=1920=0x0780 LE
|
||||||
|
0x38, 0x04, 0x00, 0x00, // [21..25] h=1080=0x0438 LE
|
||||||
|
0x01, // [25] has_cursor
|
||||||
|
0xC0, 0x03, // [26..28] x=960=0x03C0 LE
|
||||||
|
0x1C, 0x02, // [28..30] y=540=0x021C LE
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// None 分支:has=0 且**坐标字节必须为 0**(vec 初始化保证),Kotlin 侧
|
||||||
|
/// 测试锁「has=0 时无条件 null」,这里锁「写端不残留脏坐标」。
|
||||||
|
#[test]
|
||||||
|
fn none_cursor_writes_zero_flag_and_zero_coords() {
|
||||||
|
let head = pack_frame_header(&frame(1, None));
|
||||||
|
assert_eq!(head[25], 0);
|
||||||
|
assert_eq!(&head[26..30], &[0u8; 4]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 负坐标(协议层不该到达,但写端要忠实 i16 位模式——符号截断是静默错位)。
|
||||||
|
#[test]
|
||||||
|
fn negative_coords_keep_bit_pattern() {
|
||||||
|
let head = pack_frame_header(&frame(1, Some((-1, -32768))));
|
||||||
|
assert_eq!(&head[26..28], &[0xFF, 0xFF]);
|
||||||
|
assert_eq!(&head[28..30], &[0x00, 0x80]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct SessionBox {
|
struct SessionBox {
|
||||||
session: Arc<Session>,
|
session: Arc<Session>,
|
||||||
runtime: tokio::runtime::Runtime,
|
runtime: tokio::runtime::Runtime,
|
||||||
@ -167,24 +251,11 @@ pub extern "system" fn Java_com_verify_remote_RustBridge_nativeNextFrame<'local>
|
|||||||
match rt.block_on(session.next_frame()) {
|
match rt.block_on(session.next_frame()) {
|
||||||
None => std::ptr::null_mut(),
|
None => std::ptr::null_mut(),
|
||||||
Some(f) => {
|
Some(f) => {
|
||||||
let mut buf = vec![0u8; FRAME_HEADER_LEN + f.payload.len()];
|
// 布局单一真源是 pack_frame_header(那里有字面量对拍单测)
|
||||||
buf[0] = f.keyframe as u8;
|
let head = pack_frame_header(&f);
|
||||||
buf[1..9].copy_from_slice(&f.seq.to_le_bytes());
|
let mut buf = Vec::with_capacity(FRAME_HEADER_LEN + f.payload.len());
|
||||||
buf[9..17].copy_from_slice(&f.ts_ms.to_le_bytes());
|
buf.extend_from_slice(head.as_slice());
|
||||||
buf[17..21].copy_from_slice(&f.width.to_le_bytes());
|
buf.extend_from_slice(&f.payload);
|
||||||
buf[21..25].copy_from_slice(&f.height.to_le_bytes());
|
|
||||||
// v5 光标三字段(偏移 25/26/28):None → has=0 且坐标字节保持 0(无意义)。
|
|
||||||
// Option 展开成显式分支而不是 f.cursor.is_some() as u8——两处读同一个
|
|
||||||
// Option,任何「Some 但忘写坐标」的编辑错误都会在这里编译期暴露。
|
|
||||||
match f.cursor {
|
|
||||||
Some((x, y)) => {
|
|
||||||
buf[25] = 1;
|
|
||||||
buf[26..28].copy_from_slice(&x.to_le_bytes());
|
|
||||||
buf[28..30].copy_from_slice(&y.to_le_bytes());
|
|
||||||
}
|
|
||||||
None => buf[25] = 0,
|
|
||||||
}
|
|
||||||
buf[FRAME_HEADER_LEN..].copy_from_slice(&f.payload);
|
|
||||||
match env.byte_array_from_slice(&buf) {
|
match env.byte_array_from_slice(&buf) {
|
||||||
Ok(a) => a.into_raw(),
|
Ok(a) => a.into_raw(),
|
||||||
Err(_) => std::ptr::null_mut(),
|
Err(_) => std::ptr::null_mut(),
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user