use lua_rs_runtime::{AnyUserData, Lua, UserData, UserDataMethods};
fn lua_error_message(lua: &Lua, body: &str) -> String {
let wrapper = format!(
"local ok, err = pcall(function() {body} end); return ok, tostring(err)"
);
let (ok, msg): (bool, String) = lua
.load(&wrapper)
.eval()
.expect("pcall wrapper should evaluate cleanly");
assert!(!ok, "expected `{body}` to fail, but it returned ok");
msg
}
#[derive(Default)]
struct World {
entities: Vec<String>,
}
impl UserData for World {
fn add_methods<M: UserDataMethods<Self>>(m: &mut M) {
m.add_method_mut("spawn", |_lua, this, name: String| {
this.entities.push(name);
Ok(())
});
m.add_method("count", |_lua, this, ()| Ok(this.entities.len() as i64));
}
}
struct App {
world: World,
name: String,
}
impl App {
fn world_mut(&mut self) -> &mut World {
&mut self.world
}
}
impl UserData for App {
fn add_methods<M: UserDataMethods<Self>>(m: &mut M) {
m.add_method("name", |_lua, this, ()| Ok(this.name.clone()));
m.add_function("world", |lua, this: AnyUserData| {
this.delegate::<App, World, _>(lua, |app| app.world_mut())
});
}
}
#[test]
fn add_function_receives_userdata_handle() {
let lua = Lua::new();
let mut app = App {
world: World::default(),
name: "demo".into(),
};
let app_name: String = lua
.scope(|s| {
let app_ud = s.create_userdata_ref_mut(&lua, &mut app)?;
lua.globals().set("app", &app_ud)?;
lua.load("return app:name()").eval()
})
.expect("scope body should succeed");
assert_eq!(app_name, "demo");
}
#[test]
fn add_function_mut_reentrant_call_is_rejected() {
struct CallCounter {
n: i64,
}
impl UserData for CallCounter {
fn add_methods<M: UserDataMethods<Self>>(m: &mut M) {
let count = std::cell::RefCell::new(0i64);
m.add_function_mut("recurse", move |lua, ud: AnyUserData| {
*count.borrow_mut() += 1;
if *count.borrow() < 2 {
lua.globals().set("u", &ud)?;
lua.load("u:recurse()").exec()?;
}
Ok(())
});
m.add_method("get", |_lua, this, ()| Ok(this.n));
}
}
let lua = Lua::new();
let mut c = CallCounter { n: 0 };
let msg = lua
.scope(|s| {
let ud = s.create_userdata_ref_mut(&lua, &mut c)?;
lua.globals().set("u", &ud)?;
Ok(lua_error_message(&lua, "u:recurse()"))
})
.expect("pcall wrapper should evaluate cleanly");
assert!(
msg.contains("already") && msg.contains("borrowed"),
"expected FnMut-conflict error, got: {msg}"
);
}
#[test]
fn delegate_subreference_mutates_through_parent() {
let lua = Lua::new();
let mut app = App {
world: World::default(),
name: "demo".into(),
};
lua.scope(|s| {
let app_ud = s.create_userdata_ref_mut(&lua, &mut app)?;
lua.globals().set("app", &app_ud)?;
lua.load(r#"
local w = app:world()
w:spawn("alpha")
w:spawn("beta")
"#).exec()
})
.expect("scope body should succeed");
assert_eq!(app.world.entities, vec!["alpha", "beta"]);
}
#[test]
fn delegate_does_not_block_parent_between_calls() {
let lua = Lua::new();
let mut app = App {
world: World::default(),
name: "demo".into(),
};
let name: String = lua
.scope(|s| {
let app_ud = s.create_userdata_ref_mut(&lua, &mut app)?;
lua.globals().set("app", &app_ud)?;
lua.load(r#"
local w = app:world()
w:spawn("a")
local n = app:name() -- parent call between delegate calls
w:spawn("b")
return n
"#).eval()
})
.expect("scope body should succeed");
assert_eq!(name, "demo");
assert_eq!(app.world.entities, vec!["a", "b"]);
}
#[test]
fn delegate_invalidates_with_parent_at_scope_end() {
let lua = Lua::new();
let mut app = App {
world: World::default(),
name: "demo".into(),
};
lua.scope(|s| {
let app_ud = s.create_userdata_ref_mut(&lua, &mut app)?;
lua.globals().set("app", &app_ud)?;
lua.load(r#"
stashed_world = app:world()
stashed_app = app
stashed_world:spawn("in_scope")
"#).exec()
})
.expect("scope body should succeed");
assert_eq!(app.world.entities, vec!["in_scope"]);
for (label, body) in [
("parent", "stashed_app:name()"),
("delegate", "stashed_world:spawn('after')"),
] {
let msg = lua_error_message(&lua, body);
assert!(
msg.contains("no longer valid") || msg.contains("scope has ended"),
"{label}: expected invalidation error, got: {msg}"
);
}
assert_eq!(app.world.entities, vec!["in_scope"]);
}
#[test]
fn delegate_method_holding_parent_borrow_rejects_reentrant_parent_call() {
struct WorldWithReentry;
impl UserData for WorldWithReentry {
fn add_methods<M: UserDataMethods<Self>>(m: &mut M) {
m.add_method_mut("touch_parent", |lua, _this, ()| {
lua.load("return app:name()").eval::<String>()
});
}
}
struct AppWithReentryWorld {
world: WorldWithReentry,
}
impl UserData for AppWithReentryWorld {
fn add_methods<M: UserDataMethods<Self>>(m: &mut M) {
m.add_method("name", |_lua, _this, ()| Ok("the-app".to_string()));
m.add_function("world", |lua, this: AnyUserData| {
this.delegate::<AppWithReentryWorld, WorldWithReentry, _>(
lua,
|a| &mut a.world,
)
});
}
}
let lua = Lua::new();
let mut app = AppWithReentryWorld {
world: WorldWithReentry,
};
let msg = lua
.scope(|s| {
let app_ud = s.create_userdata_ref_mut(&lua, &mut app)?;
lua.globals().set("app", &app_ud)?;
Ok(lua_error_message(&lua, "app:world():touch_parent()"))
})
.expect("pcall wrapper should evaluate cleanly");
assert!(
msg.contains("already") && msg.contains("borrowed"),
"expected re-entrant borrow error, got: {msg}"
);
}
#[test]
fn delegate_chains_multiple_levels() {
#[derive(Default)]
struct Inner {
bumps: i64,
}
impl UserData for Inner {
fn add_methods<M: UserDataMethods<Self>>(m: &mut M) {
m.add_method_mut("bump", |_lua, this, ()| {
this.bumps += 1;
Ok(this.bumps)
});
}
}
struct Middle {
inner: Inner,
}
impl UserData for Middle {
fn add_methods<M: UserDataMethods<Self>>(m: &mut M) {
m.add_function("inner", |lua, this: AnyUserData| {
this.delegate::<Middle, Inner, _>(lua, |m| &mut m.inner)
});
}
}
struct Outer {
middle: Middle,
}
impl UserData for Outer {
fn add_methods<M: UserDataMethods<Self>>(m: &mut M) {
m.add_function("middle", |lua, this: AnyUserData| {
this.delegate::<Outer, Middle, _>(lua, |o| &mut o.middle)
});
}
}
let lua = Lua::new();
let mut outer = Outer {
middle: Middle {
inner: Inner::default(),
},
};
let result: i64 = lua
.scope(|s| {
let ud = s.create_userdata_ref_mut(&lua, &mut outer)?;
lua.globals().set("o", &ud)?;
lua.load(r#"
local m = o:middle()
local i = m:inner()
i:bump(); i:bump(); i:bump()
return i:bump()
"#).eval()
})
.expect("3-level chain should work");
assert_eq!(result, 4);
assert_eq!(outer.middle.inner.bumps, 4);
}
#[test]
fn delegate_cloned_handles_invalidate_together() {
let lua = Lua::new();
let mut app = App {
world: World::default(),
name: "x".into(),
};
lua.scope(|s| {
let app_ud = s.create_userdata_ref_mut(&lua, &mut app)?;
lua.globals().set("app", &app_ud)?;
lua.load(r#"
stashed_a = app:world()
stashed_b = stashed_a -- Lua reference; same cell
"#).exec()
})
.expect("scope body should succeed");
for name in ["stashed_a", "stashed_b"] {
let msg = lua_error_message(&lua, &format!("return {name}:count()"));
assert!(
msg.contains("no longer valid") || msg.contains("scope has ended"),
"{name}: expected invalidation error, got: {msg}"
);
}
}