299 lines
14 KiB
Rust
299 lines
14 KiB
Rust
//! Rust 测试框架通过 endor_host 的真实 VM/发布帧接口验收 illustrated-manuscript Endor 示例。
|
|
//! 第一项检查指针驱动/火焰生命周期与基本像素,第二项检查 owner-scoped 动画任务、不同视口和绘制几何。
|
|
//! write_png 仅在配置 TESSERA_MANUSCRIPT_SNAPSHOTS 时写图片;测试源码字符串与像素期望都是验收数据。
|
|
use super::*;
|
|
use crate::document::RenderCommand;
|
|
|
|
#[test]
|
|
/// 测试框架调用,证明动画由真实源状态和指针事件驱动,取消输入后源粒子会退场而非原生演示循环继续跑。
|
|
fn illustrated_manuscript_is_driven_by_real_endor_source_and_pointer_events() {
|
|
// 直接加载仓库随应用交付的示例,避免测试另写一份近似逻辑却无法覆盖实际程序。
|
|
let source = include_str!("../../../examples/illustrated-manuscript.endor");
|
|
// 固定会话代际只用于事件身份,几何/Mount ID 从真正发布的文档取出。
|
|
let (mut session, mut document) = ReplSession::load(source, "app.main", 1, 735).unwrap();
|
|
/// 本测试的同步求值助手,执行后只在有新文档时替换观察值,返回 Console 的字符串结果供断言。
|
|
fn evaluate(session: &mut ReplSession, document: &mut RenderDocument, source: &str) -> String {
|
|
// execute 复用持久 globals,不根据浏览文档伪造对象状态。
|
|
let result = session.execute(source, "app.main").unwrap();
|
|
if let Some(next) = result.document {
|
|
// 无变化时保留先前完整帧,不能用默认空文档替换它而误判渲染退场。
|
|
*document = next;
|
|
}
|
|
result.value
|
|
}
|
|
// 关闭自动计时器,后面显式 dragon.step 的次数才是唯一推进因素,消除定时采样干扰。
|
|
evaluate(&mut session, &mut document, "view.timer.cancel()");
|
|
document.validate().unwrap();
|
|
assert!(
|
|
document
|
|
.frame
|
|
.commands
|
|
.iter()
|
|
.any(|c| matches!(c, RenderCommand::Image { .. }))
|
|
);
|
|
assert!(document.frame.mounts.iter().any(|m| m.pointer_events));
|
|
// 读取模型的真实头段 x 作为基线,不靠图片相似度猜动画是否发生。
|
|
let before = evaluate(&mut session, &mut document, "dragon.segments.at(0).x");
|
|
// 目标必须具有 pointer_events,发布几何提供局部区域,不硬编码内部出现编号。
|
|
let target = document
|
|
.frame
|
|
.mounts
|
|
.iter()
|
|
.find(|m| m.pointer_events)
|
|
.unwrap();
|
|
// 用发布版本与实际目标构造合法原始指针,坐标放在画面内部,避免命中边缘的额外裁剪条件。
|
|
let mut input = crate::events::Input {
|
|
window: 1,
|
|
session: 735,
|
|
layout_version: document.frame.layout_version,
|
|
mount: target.id,
|
|
kind: "pointermove".into(),
|
|
point: Some([
|
|
target.x + target.width * 0.65,
|
|
target.y + target.height * 0.55,
|
|
]),
|
|
press_point: None,
|
|
delta: None,
|
|
tab_drop: None,
|
|
screen_position: None,
|
|
};
|
|
document = session
|
|
.dispatch(&input)
|
|
.unwrap()
|
|
.document
|
|
.unwrap_or(document);
|
|
for _ in 0..8 {
|
|
// 只推进源函数,证明位置变化来自已收到的指针目标与程序状态,不是宿主隐藏动画线程。
|
|
document = session
|
|
.execute("dragon.step()", "app.main")
|
|
.unwrap()
|
|
.document
|
|
.unwrap_or(document);
|
|
}
|
|
// 比较同一模型字段的前后值,检查显式推进阶段有实际位置变化,而非只生成更多绘图命令。
|
|
let after = evaluate(&mut session, &mut document, "dragon.segments.at(0).x");
|
|
assert_ne!(before, after);
|
|
// 事件每次使用最新实际几何版本,测试关注行为而非故意触发过期输入拒绝。
|
|
input.layout_version = document.frame.layout_version;
|
|
input.kind = "pointerdown".into();
|
|
document = session
|
|
.dispatch(&input)
|
|
.unwrap()
|
|
.document
|
|
.unwrap_or(document);
|
|
for _ in 0..3 {
|
|
// pointerdown 激活喷火状态,少量显式帧之后应有源粒子实例进入集合。
|
|
document = session
|
|
.execute("dragon.step()", "app.main")
|
|
.unwrap()
|
|
.document
|
|
.unwrap_or(document);
|
|
}
|
|
assert_ne!(
|
|
evaluate(&mut session, &mut document, "dragon.flames.count"),
|
|
"0"
|
|
);
|
|
// 在源模型侧检查粒子尺寸预算,避免仅像素图看起来小却实际对象无限长大。
|
|
let max_size = evaluate(&mut session, &mut document, "dragon.flames.at(0).size < 20");
|
|
assert_eq!(max_size, "true");
|
|
input.layout_version = document.frame.layout_version;
|
|
input.kind = "pointercancel".into();
|
|
// 取消必须沿同一输入分派路径清 pressed,不直接从测试修改 dragon.pressed 代替处理器。
|
|
session.dispatch(&input).unwrap();
|
|
assert_eq!(evaluate(&mut session, &mut document, "dragon.pressed"), "0");
|
|
for _ in 0..16 {
|
|
// 取消后继续更新已有粒子寿命,最终集合归零,验证没有仍被按住状态持续补发新火焰。
|
|
document = session
|
|
.execute("dragon.step()", "app.main")
|
|
.unwrap()
|
|
.document
|
|
.unwrap_or(document);
|
|
}
|
|
assert_eq!(
|
|
evaluate(&mut session, &mut document, "dragon.flames.count"),
|
|
"0"
|
|
);
|
|
// 真正发布列表交软件渲染器,不从源模型的“将要绘制”对象构造另一份测试专用画面。
|
|
let list = document.display_list(1.).unwrap();
|
|
// 预分配固定尺寸 RGB 像素,下面验证页面奶油背景确实进入最终合成结果。
|
|
let mut pixels = vec![0; 1024 * 800];
|
|
crate::software_renderer::SoftwareRenderer::render(&list, &mut pixels, 1024, 800).unwrap();
|
|
assert!(pixels.contains(&0xf4eee0));
|
|
}
|
|
|
|
/// 第二项集成测试共用的持久执行助手;保留最后完整文档,返回源值的 Console 表示,不重建 VM。
|
|
fn execute(session: &mut ReplSession, document: &mut RenderDocument, source: &str) -> String {
|
|
// 执行源代码及其同步刷新,Option 文档为空只代表没有新发布,不代表旧画面失效。
|
|
let result = session.execute(source, "app.main").unwrap();
|
|
if let Some(next) = result.document {
|
|
// 有新完整帧才覆盖测试观察值,后续输入版本与快照都从此值读取。
|
|
*document = next;
|
|
}
|
|
result.value
|
|
}
|
|
/// 第二项测试各阶段调用,先验证实际像素;仅明确配置快照目录时把同一像素编码为 PNG,便于人工检查。
|
|
fn write_png(document: &RenderDocument, w: u32, h: u32, scale: f32, name: &str) {
|
|
// scale 是物理像素倍率,显示列表和输出尺寸必须配套,不改变源程序使用的逻辑 viewport。
|
|
let list = document.display_list(scale).unwrap();
|
|
// 软件渲染覆盖真实像素数组,断言并非只检查命令里有一条背景矩形。
|
|
let mut pixels = vec![0; w as usize * h as usize];
|
|
crate::software_renderer::SoftwareRenderer::render(&list, &mut pixels, w, h).unwrap();
|
|
assert!(pixels.iter().filter(|&&p| p == 0xf4eee0).count() > 1000);
|
|
if let Ok(dir) = std::env::var("TESSERA_MANUSCRIPT_SNAPSHOTS") {
|
|
// 未设置目录时不产生文件;有目录才创建编码器,快照输出是显式选择的测试附加产物。
|
|
std::fs::create_dir_all(&dir).unwrap();
|
|
// name 区分宽窄视口及静止/穿行/喷火阶段,图片来自当前实际发布帧而非 ImageGen。
|
|
let file =
|
|
std::fs::File::create(std::path::Path::new(&dir).join(format!("endor-{name}.png")))
|
|
.unwrap();
|
|
// 像素是 0xRRGGBB,编码为每通道 8 位 RGB,不能把 u32 内存字节直接当 RGBA 保存。
|
|
let mut encoder = png::Encoder::new(file, w, h);
|
|
encoder.set_color(png::ColorType::Rgb);
|
|
encoder.set_depth(png::BitDepth::Eight);
|
|
// 显式提取高到低三个颜色分量,避免平台端序影响导出的颜色。
|
|
let rgb: Vec<_> = pixels
|
|
.iter()
|
|
.flat_map(|p| [(p >> 16) as u8, (p >> 8) as u8, *p as u8])
|
|
.collect();
|
|
encoder
|
|
.write_header()
|
|
.unwrap()
|
|
.write_image_data(&rgb)
|
|
.unwrap();
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
/// 测试框架调用,验证真实 owner 定时任务、宽窄/高倍视口、喷火上限和 Page 关闭后的动画取消。
|
|
fn manuscript_script_snapshots_and_owned_animation_lifecycle() {
|
|
// 只追加测试推进函数,原示例本体不改;每次 step 前刷新 lastMove 以稳定触发运动场景。
|
|
let source = format!(
|
|
"{}\nfunction advanceTest():Unit {{dragon.lastMove=math.now() dragon.step()}}",
|
|
include_str!("../../../examples/illustrated-manuscript.endor")
|
|
);
|
|
for (width, height, scale) in [(1000., 900., 1.), (420., 760., 2.)] {
|
|
// 两种窗口使用独立 VM 与真实逻辑 viewport,窄视图同时覆盖高倍字体/图片采样。
|
|
let (mut session, mut document) = ReplSession::load_with_viewport(
|
|
&source,
|
|
"app.main",
|
|
1,
|
|
736,
|
|
Some(crate::layout::Size { width, height }),
|
|
)
|
|
.unwrap();
|
|
// 真实 owner-scoped Task 调用源 tick,先比较时间字段确认计时器而非宿主演示循环在推进。
|
|
let before = execute(&mut session, &mut document, "dragon.time");
|
|
// 等待超过示例定时截止时间,再在所属 VM poll,后台线程不会直接替程序改 time。
|
|
std::thread::sleep(std::time::Duration::from_millis(85));
|
|
if let Some(next) = session.poll_tasks().unwrap() {
|
|
// 只接收调度后真正完整发布文档,不从任务内部未收敛草稿截屏。
|
|
document = next;
|
|
}
|
|
// time 变化证明 owner/key 当前任务获得了一次有效交付。
|
|
let after = execute(&mut session, &mut document, "dragon.time");
|
|
assert_ne!(before, after);
|
|
execute(&mut session, &mut document, "view.timer.cancel()");
|
|
// 自动动画确认后取消,后续快照阶段由固定次数 step 控制;像素尺寸按逻辑尺寸乘 scale。
|
|
let (w, h) = ((width * scale) as u32, (height * scale) as u32);
|
|
write_png(
|
|
&document,
|
|
w,
|
|
h,
|
|
scale,
|
|
if width > 500. { "rest" } else { "narrow-rest" },
|
|
);
|
|
// 找当前真实指针出现并克隆几何,下面分派会改 document,不能保留对旧文档的借用。
|
|
let mount = document
|
|
.frame
|
|
.mounts
|
|
.iter()
|
|
.find(|m| m.pointer_events)
|
|
.unwrap()
|
|
.clone();
|
|
// 用视口比例选锚点,宽窄布局都位于同类内容区域,不依赖固定像素偏移。
|
|
let point = [mount.x + mount.width * 0.58, mount.y + mount.height * 0.50];
|
|
// 会话/版本/目标均从本轮 VM 和已发布帧取得,确保命中的是此视口而不是另一测试残留。
|
|
let mut input = crate::events::Input {
|
|
window: 1,
|
|
session: 736,
|
|
layout_version: document.frame.layout_version,
|
|
mount: mount.id,
|
|
kind: "pointermove".into(),
|
|
point: Some(point),
|
|
press_point: None,
|
|
delta: None,
|
|
tab_drop: None,
|
|
screen_position: None,
|
|
};
|
|
document = session
|
|
.dispatch(&input)
|
|
.unwrap()
|
|
.document
|
|
.unwrap_or(document);
|
|
for _ in 0..12 {
|
|
// 固定推进源运动,生成穿行状态,快照仍走实际 scene/paint/软件渲染链。
|
|
execute(&mut session, &mut document, "advanceTest()");
|
|
}
|
|
write_png(
|
|
&document,
|
|
w,
|
|
h,
|
|
scale,
|
|
if width > 500. { "crossing" } else { "narrow" },
|
|
);
|
|
input.layout_version = document.frame.layout_version;
|
|
input.kind = "pointerdown".into();
|
|
document = session
|
|
.dispatch(&input)
|
|
.unwrap()
|
|
.document
|
|
.unwrap_or(document);
|
|
// 按住并推进足够帧数达到粒子稳态上限,覆盖比静止更重的真实 paint 工作量。
|
|
// 计时仅作诊断输出,测试没有依赖机器速度的性能通过阈值。
|
|
let start = std::time::Instant::now();
|
|
for _ in 0..16 {
|
|
// 每次仍执行源 advanceTest,不能用原生测试循环直接生成粒子绕过语言能力。
|
|
execute(&mut session, &mut document, "advanceTest()");
|
|
}
|
|
eprintln!(
|
|
"Endor manuscript width={width}: 16 steady-fire VM refreshes {:?}",
|
|
start.elapsed()
|
|
);
|
|
assert_eq!(
|
|
execute(&mut session, &mut document, "dragon.flames.count <= 12"),
|
|
"true"
|
|
);
|
|
// 从源 artwork 与粒子速度对比最终图片角度,验证精灵轴校正符合源模型方向。
|
|
execute(
|
|
&mut session,
|
|
&mut document,
|
|
"function verifyFire(): Bool { art=dragon.artwork(0,0,1) c=art.at(art.count-1) p=dragon.flames.at(dragon.flames.count-1) return math.abs(c.angle+p.art.axis-math.atan2(p.vy,p.vx))<0.0001 }",
|
|
);
|
|
assert_eq!(execute(&mut session, &mut document, "verifyFire()"), "true");
|
|
// 单独验证最终绘图宽高,不只看粒子模型 size 字段,防止图片命令放大绕过尺寸预算。
|
|
execute(
|
|
&mut session,
|
|
&mut document,
|
|
"function verifyFireSize(): Bool { art=dragon.artwork(0,0,1) c=art.at(art.count-1) return math.max(c.width,c.height)<=20 }",
|
|
);
|
|
assert_eq!(
|
|
execute(&mut session, &mut document, "verifyFireSize()"),
|
|
"true"
|
|
);
|
|
write_png(
|
|
&document,
|
|
w,
|
|
h,
|
|
scale,
|
|
if width > 500. { "fire" } else { "narrow-fire" },
|
|
);
|
|
// Page 关闭须立即取消其 Mount 所有的任务;后续即使保留 dragon 对象也不能继续定时更新。
|
|
execute(&mut session, &mut document, "page.close()");
|
|
// 关闭时真实 time 是对照值,等待超过旧定时点再 poll 应完全不变。
|
|
let closed = execute(&mut session, &mut document, "dragon.time");
|
|
std::thread::sleep(std::time::Duration::from_millis(85));
|
|
session.poll_tasks().unwrap();
|
|
assert_eq!(execute(&mut session, &mut document, "dragon.time"), closed);
|
|
}
|
|
}
|