481 lines
21 KiB
Rust
481 lines
21 KiB
Rust
//! Rust 测试框架驱动的源码类集成测试:从前端/编译器到 VM 的宿主构造、绑定方法、字段检查及 GC。
|
||
//! execute/try_execute 按现有 globals 声明可用宿主名,加载临时代码,执行后统一归还代码根。
|
||
use compiler::{CompileOptions, compile_source};
|
||
use endor_core::{
|
||
ContextNativeFunction, EndorTypeObject, NativeCallable, NativeContext, NativeResult,
|
||
ObjectPayload, TypeConstructor, VmError,
|
||
};
|
||
use endor_core::{ObjectRef, Value, Vm};
|
||
use frontend::{EndorReader, ParseMode};
|
||
|
||
#[derive(Debug)]
|
||
/// 原生实例载荷,WidgetFactory 分配;字符串是宿主标签,没有需要 trace 的托管对象边。
|
||
struct NativeWidget(String);
|
||
impl ObjectPayload for NativeWidget {
|
||
/// Store::payload 的类型检查通过 Any 下转,测试据此读取原生标签。
|
||
fn as_any(&self) -> &dyn std::any::Any {
|
||
self
|
||
}
|
||
/// 对象种类诊断入口使用稳定标签 widget,与测试载荷检查保持一致。
|
||
fn kind_name(&self) -> &'static str {
|
||
"widget"
|
||
}
|
||
/// Store 属性协议查询此开关;允许在原生实例上存放源码类声明的动态字段。
|
||
fn allows_attributes(&self) -> bool {
|
||
true
|
||
}
|
||
}
|
||
|
||
#[derive(Debug)]
|
||
/// NativeTyped 构造策略使用的宿主回调;源码子类沿继承链复用它,但实际实例附着请求的子类。
|
||
struct WidgetFactory;
|
||
impl NativeCallable for WidgetFactory {
|
||
/// VM 原生调用入口调用;参数依次为请求的 Type、宿主标签、声明字段值,结果由 NativeContext 返回协议接管。
|
||
fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
|
||
// 解构 NativeTyped 调用布局;类型对象与标签必须存在,其余切片作为字段参数。
|
||
let [
|
||
Value::Object(requested),
|
||
Value::Text(native_label),
|
||
values @ ..,
|
||
] = args
|
||
else {
|
||
return Err(VmError::InvalidArgument("widget constructor arguments"));
|
||
};
|
||
// 复制请求类型的完整字段元数据,以便稍后可变访问 Store 并分配实例。
|
||
let fields = cx
|
||
.store()
|
||
.payload::<EndorTypeObject>(*requested, "Type")?
|
||
.fields()
|
||
.to_vec();
|
||
// 字段数量必须匹配,错误时还没有分配新实例。
|
||
if values.len() != fields.len() {
|
||
return Err(VmError::InvalidArgument("widget field count"));
|
||
}
|
||
// 先校验字段注解,避免无效实参进入实例分配和字段写入阶段。
|
||
cx.store().check_class_field_arguments(*requested, values)?;
|
||
// 通过当前原生上下文分配载荷;发生后续错误时由上下文清理临时分配根。
|
||
let instance = cx.allocate(NativeWidget(native_label.clone()))?;
|
||
// 附着实际请求的 Type,而非固定 Widget,保持 Child/Grand 的名义身份。
|
||
cx.attach_type(instance, *requested)?;
|
||
// 按声明字段顺序写值,Value clone 仍由 Store 属性更新建立内部引用边。
|
||
for (field, value) in fields.iter().zip(values) {
|
||
cx.set_attr(instance, &field.name, value.clone())?;
|
||
}
|
||
Ok(NativeResult::Return(Value::Object(instance)))
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
/// 由 Rust 测试框架调用:验证源码两级继承仍由原生工厂分配载荷,字段约束和名义父类型保留,结果根独立保活类型图。
|
||
fn source_subclasses_preserve_native_allocation_fields_and_nominal_identity() {
|
||
// 隔离本场景堆、类型配置与执行状态,最后检查没有遗留对象。
|
||
let mut vm = Vm::new().unwrap();
|
||
// 开启源码注解及字段检查,下面的类型错误才会按约束拒绝。
|
||
vm.set_source_type_checks(true).unwrap();
|
||
// 测试持环境初始根,源码函数和类型声明可在内部引用此环境。
|
||
let globals = vm.new_dict().unwrap();
|
||
// 引导得到 Object/Type 拥有根,注册环境后分别归还测试根。
|
||
let roots = vm.bootstrap_types().unwrap();
|
||
// 注册 NativeContext 回调,后续通过 NativeTyped 类型构造协议调用。
|
||
let factory = vm
|
||
.allocate(ContextNativeFunction::new("widget", WidgetFactory, vec![]))
|
||
.unwrap();
|
||
// 创建宿主 Widget 类型,以 Object 为父类并保存原生构造器引用。
|
||
let widget = vm
|
||
.new_type(
|
||
"Widget",
|
||
roots.object,
|
||
TypeConstructor::NativeTyped(factory),
|
||
)
|
||
.unwrap();
|
||
// 把引导类型/宿主类型放入源码可访问的环境,再归还各自外部根。
|
||
for (name, object) in [
|
||
("Object", roots.object),
|
||
("Type", roots.type_type),
|
||
("Widget", widget),
|
||
] {
|
||
vm.dict_set(globals, Value::Text(name.into()), Value::Object(object))
|
||
.unwrap();
|
||
vm.release(object).unwrap();
|
||
}
|
||
// Widget 已持有工厂内部边,测试不再额外持工厂根。
|
||
vm.release(factory).unwrap();
|
||
// Child 增加数值字段,Grand 再增加文本字段,两层源码继承都沿用 Widget 原生分配策略。
|
||
let source = "class Child: Widget { value: Number }\nclass Grand: Child { label: String }";
|
||
assert_eq!(
|
||
execute(&mut vm, globals, source, ParseMode::Module),
|
||
Value::Nil
|
||
);
|
||
// 检查确实由运行时字段类型约束拒绝,而非返回一个看似成功的实例。
|
||
assert!(matches!(
|
||
try_execute(
|
||
&mut vm,
|
||
globals,
|
||
r#"Grand("native", "bad", "extra")"#,
|
||
ParseMode::Expression
|
||
),
|
||
Err(VmError::TypeMismatch { .. })
|
||
));
|
||
// 实际调用 Grand 得到拥有实例结果,随后独立于 globals 验证其原生载荷及继承字段。
|
||
let value = execute(
|
||
&mut vm,
|
||
globals,
|
||
"Grand(\"native\", 9, \"extra\")",
|
||
ParseMode::Expression,
|
||
);
|
||
// 成功结果必须是托管实例;此句柄持有 execute 返回的拥有根。
|
||
let Value::Object(instance) = value else {
|
||
panic!("native instance")
|
||
};
|
||
assert_eq!(
|
||
vm.payload::<NativeWidget>(instance, "widget").unwrap().0,
|
||
"native"
|
||
);
|
||
assert!(vm.is_instance(instance, widget).unwrap());
|
||
assert_eq!(
|
||
vm.get_attr(instance, "value").unwrap(),
|
||
Value::Number(9_i64.into())
|
||
);
|
||
assert_eq!(
|
||
vm.get_attr(instance, "label").unwrap(),
|
||
Value::Text("extra".into())
|
||
);
|
||
// 先清环境的全局拥有边,打断函数/类型与环境之间可能的引用环。
|
||
vm.dict_clear(globals).unwrap();
|
||
// 归还环境外部根,剩余图是否存活只由其他拥有根决定。
|
||
vm.release(globals).unwrap();
|
||
// 收集已无外部根的类型/函数/环境环;仍由实例或回调拥有的图必须保留。
|
||
vm.collect_cycles();
|
||
assert!(vm.is_instance(instance, widget).unwrap());
|
||
// 归还实例拥有结果根,之后其类型链及内部图也应可清理。
|
||
vm.release(instance).unwrap();
|
||
// 收集已无外部根的类型/函数/环境环;仍由实例或回调拥有的图必须保留。
|
||
vm.collect_cycles();
|
||
assert_eq!(vm.object_count(), 0);
|
||
}
|
||
|
||
/// 成功路径测试复用的包装,try_execute 的 VM 错误直接使该断言场景失败;返回 Value 归调用方释放。
|
||
fn execute(vm: &mut Vm, globals: ObjectRef, source: &str, mode: ParseMode) -> Value {
|
||
try_execute(vm, globals, source, mode).unwrap()
|
||
}
|
||
|
||
/// 类测试共用的编译/执行桥接,解析与加载失败直接使测试失败,VM 执行错误则原样返回给错误用例。
|
||
/// globals 是借用环境,返回对象值有独立拥有根;无论执行成功或失败,本辅助函数都归还加载的代码根。
|
||
fn try_execute(
|
||
vm: &mut Vm,
|
||
globals: ObjectRef,
|
||
source: &str,
|
||
mode: ParseMode,
|
||
) -> Result<Value, VmError> {
|
||
// 从当前环境条目提取文本键作为编译期已知名;这只声明名称,不复制运行时值。
|
||
let names = vm
|
||
.dict_items(globals)
|
||
.unwrap()
|
||
.iter()
|
||
// 非文本键不能作为源码标识符,忽略它们而不向编译器提供伪名称。
|
||
.filter_map(|(name, _)| {
|
||
// 只把文本键转成声明名,其余键留在环境但不参与源码解析。
|
||
if let Value::Text(name) = name {
|
||
Some(name.clone())
|
||
} else {
|
||
None
|
||
}
|
||
})
|
||
.collect();
|
||
// 为临时源码添加测试来源身份,错误诊断据此显示文件名与位置。
|
||
let source = EndorReader::from_text("<classes>", source);
|
||
// 根据当前名字列表编译,前端或语义失败属于测试设置错误而非期望的 VM 错误。
|
||
let template = compile_source(&source, mode, &CompileOptions { globals: names }).unwrap();
|
||
// 显式加载模板取得当前 VM 的拥有代码根,禁止跨堆复用对象句柄。
|
||
let code = template.load(vm).unwrap();
|
||
// 先保存 Result,随后即使执行失败也必须释放当前代码根。
|
||
let result = vm.execute(code, globals);
|
||
// 临时代码的拥有根归还,成功结果若含对象仍有自己的根。
|
||
vm.release(code).unwrap();
|
||
result
|
||
}
|
||
|
||
#[test]
|
||
/// 由 Rust 测试框架调用:验证继承方法绑定真实子类实例、绑定参数数目扣除 self,单独回调根在清空环境后仍能动态派发。
|
||
fn source_inheritance_binding_and_dynamic_dispatch_keep_the_real_receiver_alive() {
|
||
// 隔离本场景堆、类型配置与执行状态,最后检查没有遗留对象。
|
||
let mut vm = Vm::new().unwrap();
|
||
// 测试持环境初始根,源码函数和类型声明可在内部引用此环境。
|
||
let globals = vm.new_dict().unwrap();
|
||
// 引导得到 Object/Type 拥有根,注册环境后分别归还测试根。
|
||
let roots = vm.bootstrap_types().unwrap();
|
||
// 把引导类型/宿主类型放入源码可访问的环境,再归还各自外部根。
|
||
for (name, object) in [("Object", roots.object), ("Type", roots.type_type)] {
|
||
vm.dict_set(globals, Value::Text(name.into()), Value::Object(object))
|
||
.unwrap();
|
||
vm.release(object).unwrap();
|
||
}
|
||
// Base.via 通过 self.read 动态派发;Child 覆盖 read,withEvent 用于检查绑定后的参数数量。
|
||
let source = r#"
|
||
class Base {
|
||
value: Number
|
||
function read(self: Base): Number { return self.value }
|
||
function via(self: Base): Number { return self.read() }
|
||
function withEvent(self: Base, event: Object): Object { return event }
|
||
}
|
||
class Child: Base {
|
||
label: String
|
||
function read(self: Child): Number { return 12 }
|
||
}
|
||
"#;
|
||
assert_eq!(
|
||
execute(&mut vm, globals, source, ParseMode::Module),
|
||
Value::Nil
|
||
);
|
||
// 读取临时 Child 实例的继承方法 via,返回绑定方法拥有根,方法必须保存实际接收者。
|
||
let callback = execute(
|
||
&mut vm,
|
||
globals,
|
||
"Child(9, \"ok\").via",
|
||
ParseMode::Expression,
|
||
);
|
||
// 回调必须是托管绑定方法,拥有根一直保留到环境清理与实际调用之后。
|
||
let Value::Object(callback) = callback else {
|
||
panic!("bound callback")
|
||
};
|
||
assert_eq!(
|
||
vm.object_store()
|
||
.positional_parameter_count(callback)
|
||
.unwrap(),
|
||
Some(0)
|
||
);
|
||
// 另取带 event 参数的绑定方法,检查 self 自动绑定后仍剩一个位置参数。
|
||
let with_event = execute(
|
||
&mut vm,
|
||
globals,
|
||
"Child(9, \"ok\").withEvent",
|
||
ParseMode::Expression,
|
||
);
|
||
// 将第二个回调结果解构为拥有句柄,稍后单独释放。
|
||
let Value::Object(with_event) = with_event else {
|
||
panic!("bound callback")
|
||
};
|
||
assert_eq!(
|
||
vm.object_store()
|
||
.positional_parameter_count(with_event)
|
||
.unwrap(),
|
||
Some(1)
|
||
);
|
||
// 参数计数检查结束,归还第二个绑定方法,避免它掩盖主回调的保活责任。
|
||
vm.release(with_event).unwrap();
|
||
// 先清环境的全局拥有边,打断函数/类型与环境之间可能的引用环。
|
||
vm.dict_clear(globals).unwrap();
|
||
// 归还环境外部根,剩余图是否存活只由其他拥有根决定。
|
||
vm.release(globals).unwrap();
|
||
// 收集已无外部根的类型/函数/环境环;仍由实例或回调拥有的图必须保留。
|
||
vm.collect_cycles();
|
||
// 绑定回调是接收者最后的外部可达入口;Base.via 内的 self.read 应派发到 Child.read。
|
||
// No global name or external instance root remains. The bound callback
|
||
// alone retains its receiver and can still dispatch to Child.read.
|
||
assert_eq!(
|
||
vm.call(callback, &[]).unwrap(),
|
||
Value::Number(12_i64.into())
|
||
);
|
||
// 完成清空环境后的动态派发验证,归还最后回调根。
|
||
vm.release(callback).unwrap();
|
||
// 收集已无外部根的类型/函数/环境环;仍由实例或回调拥有的图必须保留。
|
||
vm.collect_cycles();
|
||
assert_eq!(vm.object_count(), 0);
|
||
}
|
||
|
||
#[test]
|
||
/// 由 Rust 测试框架调用:验证重复字段及缺显式 self 在语义分析阶段被拒绝,不需创建 VM 或执行源码。
|
||
fn duplicate_members_and_missing_receivers_fail_before_execution() {
|
||
// 两个独立语义错误案例:同类重复字段,以及方法没有显式 self 参数。
|
||
for text in [
|
||
"class Bad { value: Number value: String }",
|
||
"class Bad { function read(): Number { return 1 } }",
|
||
] {
|
||
// 为临时源码添加测试来源身份,错误诊断据此显示文件名与位置。
|
||
let source = EndorReader::from_text("<invalid-class>", text);
|
||
assert!(
|
||
compile_source(
|
||
&source,
|
||
ParseMode::Module,
|
||
&CompileOptions {
|
||
globals: vec!["Object".into()]
|
||
}
|
||
)
|
||
.is_err()
|
||
);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
/// 由 Rust 测试框架调用:验证继承字段构造/赋值/删除检查、失败前后版本不变、动态名义注解重绑定与清理。
|
||
fn declared_field_checks_cover_construction_inheritance_writes_and_gc() {
|
||
// 隔离本场景堆、类型配置与执行状态,最后检查没有遗留对象。
|
||
let mut vm = Vm::new().unwrap();
|
||
// 开启源码注解及字段检查,下面的类型错误才会按约束拒绝。
|
||
vm.set_source_type_checks(true).unwrap();
|
||
// 测试持环境初始根,源码函数和类型声明可在内部引用此环境。
|
||
let globals = vm.new_dict().unwrap();
|
||
// 引导得到 Object/Type 拥有根,注册环境后分别归还测试根。
|
||
let roots = vm.bootstrap_types().unwrap();
|
||
vm.dict_set(
|
||
globals,
|
||
Value::Text("Object".into()),
|
||
Value::Object(roots.object),
|
||
)
|
||
.unwrap();
|
||
vm.release(roots.object).unwrap();
|
||
vm.release(roots.type_type).unwrap();
|
||
// 一次模块声明建立继承字段、名义对象字段及未知注解三种约束,真正的构造/写入在后面分别触发。
|
||
execute(
|
||
&mut vm,
|
||
globals,
|
||
r#"
|
||
class Base { count: Number }
|
||
class Child: Base { label: String }
|
||
class Holder { child: Base }
|
||
class Unknown { value: Missing }
|
||
"#,
|
||
ParseMode::Module,
|
||
);
|
||
// 每份交互源码只提交一条语句:先登记 Child 实例,再让 Holder 的字段持有同一实例。
|
||
// Interactive intentionally accepts one complete statement per source unit.
|
||
execute(
|
||
&mut vm,
|
||
globals,
|
||
r#"value = Child(9, "ok")"#,
|
||
ParseMode::Interactive,
|
||
);
|
||
execute(
|
||
&mut vm,
|
||
globals,
|
||
"holder = Holder(value)",
|
||
ParseMode::Interactive,
|
||
);
|
||
// 从 globals 借用已登记实例;测试不额外持该句柄的拥有根。
|
||
let Value::Object(value) = vm
|
||
.dict_get(globals, &Value::Text("value".into()))
|
||
.unwrap()
|
||
.unwrap()
|
||
else {
|
||
panic!("instance")
|
||
};
|
||
// 记录合法实例当前版本,后续失败的字段写入不得改变它。
|
||
let version = vm.object_store().change_version(value).unwrap();
|
||
// 覆盖继承字段、子字段、名义对象字段、未知注解及错误赋值,错误必须在更新前返回。
|
||
for source in [
|
||
r#"Child("bad", "ok")"#,
|
||
"Child(9, 2)",
|
||
"Holder(1)",
|
||
"Unknown(1)",
|
||
r#"value.count = "bad""#,
|
||
] {
|
||
// 错误应是类型不匹配或未知注解,不接受其他无关失败掩盖约束问题。
|
||
assert!(matches!(
|
||
try_execute(&mut vm, globals, source, ParseMode::Interactive),
|
||
Err(VmError::TypeMismatch { .. } | VmError::UnknownAnnotation(_))
|
||
));
|
||
}
|
||
assert_eq!(
|
||
vm.get_attr(value, "count").unwrap(),
|
||
Value::Number(9.into())
|
||
);
|
||
assert_eq!(vm.object_store().change_version(value).unwrap(), version);
|
||
assert!(vm.del_attr(value, "count").is_err());
|
||
vm.set_attr(value, "count", Value::Number(10.into()))
|
||
.unwrap();
|
||
// 用同名新类替换全局 Base,旧 value 的名义类型图仍保留最初的 Base 身份。
|
||
execute(&mut vm, globals, "class Base {}", ParseMode::Interactive);
|
||
// 其他名义类型名称在声明环境中动态解析;Base 重定义后 Holder 的字段约束应按新身份检查。
|
||
// Other nominal names resolve dynamically in the declaring environment.
|
||
assert!(matches!(
|
||
try_execute(&mut vm, globals, "Holder(value)", ParseMode::Expression),
|
||
Err(VmError::TypeMismatch { .. })
|
||
));
|
||
// 继承来的 count 保留其原声明字段元数据,环境同名类重定义不抹掉旧实例的数值约束。
|
||
// An inherited scalar field still belongs to its original declaration.
|
||
vm.set_attr(value, "count", Value::Number(11.into()))
|
||
.unwrap();
|
||
// 先清环境的全局拥有边,打断函数/类型与环境之间可能的引用环。
|
||
vm.dict_clear(globals).unwrap();
|
||
// 归还环境外部根,剩余图是否存活只由其他拥有根决定。
|
||
vm.release(globals).unwrap();
|
||
// 收集已无外部根的类型/函数/环境环;仍由实例或回调拥有的图必须保留。
|
||
vm.collect_cycles();
|
||
assert_eq!(vm.object_count(), 0);
|
||
}
|
||
|
||
#[test]
|
||
/// 由 Rust 测试框架调用:验证声明类自身的递归类型按原身份检查,即使全局同名类重定义;关闭检查允许临时写入 Nil。
|
||
fn checked_fields_keep_declaring_self_type_and_can_be_opted_out() {
|
||
// 隔离本场景堆、类型配置与执行状态,最后检查没有遗留对象。
|
||
let mut vm = Vm::new().unwrap();
|
||
// 开启源码注解及字段检查,下面的类型错误才会按约束拒绝。
|
||
vm.set_source_type_checks(true).unwrap();
|
||
// 测试持环境初始根,源码函数和类型声明可在内部引用此环境。
|
||
let globals = vm.new_dict().unwrap();
|
||
// 引导得到 Object/Type 拥有根,注册环境后分别归还测试根。
|
||
let roots = vm.bootstrap_types().unwrap();
|
||
vm.dict_set(
|
||
globals,
|
||
Value::Text("Object".into()),
|
||
Value::Object(roots.object),
|
||
)
|
||
.unwrap();
|
||
vm.release(roots.object).unwrap();
|
||
vm.release(roots.type_type).unwrap();
|
||
// Link.next 是递归且不可空的字段;Seed 声明展示继承,后续由可信宿主直接构造旧 Link 实例。
|
||
execute(
|
||
&mut vm,
|
||
globals,
|
||
r#"
|
||
class Link { next: Link }
|
||
class Seed: Link {}
|
||
"#,
|
||
ParseMode::Module,
|
||
);
|
||
// A recursive nonnullable field can be initialized by the trusted host in two steps.
|
||
// 从 globals 借用最初 Link 类型,后面同名重定义不应改变此类型的自引用身份。
|
||
let Value::Object(link) = vm
|
||
.dict_get(globals, &Value::Text("Link".into()))
|
||
.unwrap()
|
||
.unwrap()
|
||
else {
|
||
panic!("class")
|
||
};
|
||
// 宿主先分配空对象再附类型,用两步构造填入不可空的递归 next 字段。
|
||
let seed = vm.new_object().unwrap();
|
||
// 附上旧 Link 名义类型,此时先由可信宿主完成递归对象初始化。
|
||
vm.attach_type(seed, link).unwrap();
|
||
vm.set_attr(seed, "next", Value::Object(seed)).unwrap();
|
||
vm.dict_set(globals, Value::Text("old".into()), Value::Object(seed))
|
||
.unwrap();
|
||
// 新 Link 采用不同字段布局,只替换环境绑定;seed 及其 next 的原声明身份应不受影响。
|
||
execute(
|
||
&mut vm,
|
||
globals,
|
||
"class Link { value: Number }",
|
||
ParseMode::Interactive,
|
||
);
|
||
vm.set_attr(seed, "next", Value::Object(seed)).unwrap();
|
||
// 旧 Link 的自引用字段不可空,即使当前全局 Link 已换成另一个类。
|
||
assert!(matches!(
|
||
vm.set_attr(seed, "next", Value::Nil),
|
||
Err(VmError::TypeMismatch { .. })
|
||
));
|
||
// 显式关闭检查,允许下一步向不可空字段写 Nil;这不会改变声明本身。
|
||
vm.set_source_type_checks(false).unwrap();
|
||
vm.set_attr(seed, "next", Value::Nil).unwrap();
|
||
// 恢复检查配置后写回旧 Link 自身实例,验证声明身份和约束没有被关闭操作修改。
|
||
vm.set_source_type_checks(true).unwrap();
|
||
vm.set_attr(seed, "next", Value::Object(seed)).unwrap();
|
||
// 归还种子外部根;环境清理后,自引用环仍需循环收集器处理。
|
||
vm.release(seed).unwrap();
|
||
// 先清环境的全局拥有边,打断函数/类型与环境之间可能的引用环。
|
||
vm.dict_clear(globals).unwrap();
|
||
// 归还环境外部根,剩余图是否存活只由其他拥有根决定。
|
||
vm.release(globals).unwrap();
|
||
// 收集已无外部根的类型/函数/环境环;仍由实例或回调拥有的图必须保留。
|
||
vm.collect_cycles();
|
||
assert_eq!(vm.object_count(), 0);
|
||
}
|