use omnilua::{HostHooks, Lua, LuaVersion, Value};
const ALL: [LuaVersion; 5] = [
LuaVersion::V51,
LuaVersion::V52,
LuaVersion::V53,
LuaVersion::V54,
LuaVersion::V55,
];
fn eval_str(version: LuaVersion, code: &str) -> Vec<u8> {
let lua = Lua::new_versioned(version);
match lua.load(code).eval::<Value>() {
Ok(Value::String(s)) => s
.as_bytes()
.unwrap_or_else(|e| panic!("string bytes under {version:?} for `{code}`: {e:?}"))
.to_vec(),
Ok(other) => panic!("`{code}` under {version:?} returned {other:?}, expected a string"),
Err(e) => panic!("eval of `{code}` failed under {version:?}: {e:?}"),
}
}
fn eval_int(version: LuaVersion, code: &str) -> i64 {
let lua = Lua::new_versioned(version);
match lua.load(code).eval::<Value>() {
Ok(Value::Integer(i)) => i,
Ok(Value::Number(n)) if n.fract() == 0.0 => n as i64,
Ok(other) => panic!("`{code}` under {version:?} returned {other:?}, expected an integer"),
Err(e) => panic!("eval of `{code}` failed under {version:?}: {e:?}"),
}
}
fn eval_value(version: LuaVersion, code: &str) -> Value {
let lua = Lua::new_versioned(version);
lua.load(code)
.eval::<Value>()
.unwrap_or_else(|e| panic!("eval of `{code}` failed under {version:?}: {e:?}"))
}
fn assert_true(version: LuaVersion, code: &str) {
match eval_value(version, code) {
Value::Boolean(true) => {}
other => panic!("`{code}` under {version:?} returned {other:?}, expected true"),
}
}
#[test]
fn config_string_is_version_exact() {
assert_eq!(eval_str(LuaVersion::V51, "return package.config"), b"/\n;\n?\n!\n-");
assert_eq!(eval_int(LuaVersion::V51, "return #package.config"), 9);
for v in [LuaVersion::V52, LuaVersion::V53, LuaVersion::V54, LuaVersion::V55] {
assert_eq!(eval_str(v, "return package.config"), b"/\n;\n?\n!\n-\n", "{v:?}");
assert_eq!(eval_int(v, "return #package.config"), 10, "{v:?}");
}
for v in ALL {
assert_eq!(eval_str(v, "return package.config:sub(1,1)"), b"/", "{v:?}");
}
}
#[test]
fn require_preload_returns_module_and_caches() {
for v in ALL {
let probe = "\
package.preload['mymod'] = function() return {answer = 42} end \
local m = require('mymod') \
return m.answer == 42 \
and m == package.loaded['mymod'] \
and type(package.preload['mymod']) == 'function'";
assert_true(v, probe);
}
}
#[test]
fn require_caches_loader_runs_once() {
for v in ALL {
let probe = "\
local n = 0 \
package.preload['m'] = function() n = n + 1; return {id = n} end \
local a = require('m') \
local b = require('m') \
return a == b and a.id == 1 and n == 1";
assert_true(v, probe);
}
}
#[test]
fn require_preseeded_loaded_short_circuits_searchers() {
for v in ALL {
assert_eq!(
eval_str(v, "package.loaded['pre'] = 'CACHED'; return require('pre')"),
b"CACHED",
"{v:?}"
);
}
}
#[test]
fn require_second_value_is_loader_data_only_5_4_plus() {
let probe = "\
package.preload['m'] = function() return 'MOD' end \
local a, b = require('m') \
return b";
for v in [LuaVersion::V51, LuaVersion::V52, LuaVersion::V53] {
assert!(
matches!(eval_value(v, probe), Value::Nil),
"{v:?}: require's 2nd value must be nil pre-5.4, got {:?}",
eval_value(v, probe)
);
}
for v in [LuaVersion::V54, LuaVersion::V55] {
assert_eq!(eval_str(v, probe), b":preload:", "{v:?}");
}
}
#[test]
fn preload_loader_argument_count_is_one_on_5_1_two_after() {
let probe = "\
package.preload['m'] = function(...) return select('#', ...) end \
return (require('m'))";
assert_eq!(eval_int(LuaVersion::V51, probe), 1, "5.1 passes the name only");
for v in [LuaVersion::V52, LuaVersion::V53, LuaVersion::V54, LuaVersion::V55] {
assert_eq!(eval_int(v, probe), 2, "{v:?} passes name + loader data");
assert_eq!(
eval_str(
v,
"package.preload['m'] = function(n) return n end; return (require('m'))"
),
b"m",
"{v:?}"
);
}
assert_eq!(
eval_str(
LuaVersion::V51,
"package.preload['m'] = function(n) return n end; return (require('m'))"
),
b"m"
);
}
#[test]
fn require_missing_module_error_is_full_four_searcher_trace() {
let setup_and_require = "\
package.path = './?.lua'; package.cpath = './?.so' \
local ok, err = pcall(require, 'no.such.mod') \
assert(not ok) \
return err";
let expected: &[u8] = b"module 'no.such.mod' not found:\n\
\tno field package.preload['no.such.mod']\n\
\tno file './no/such/mod.lua'\n\
\tno file './no/such/mod.so'\n\
\tno file './no.so'";
for v in ALL {
assert_eq!(eval_str(v, setup_and_require), expected, "{v:?}");
}
}
#[test]
fn require_missing_dotless_module_trace_has_no_croot_line() {
let probe = "\
package.path = './?.lua'; package.cpath = './?.so' \
local ok, err = pcall(require, 'solo') \
assert(not ok) \
return err";
let expected: &[u8] = b"module 'solo' not found:\n\
\tno field package.preload['solo']\n\
\tno file './solo.lua'\n\
\tno file './solo.so'";
for v in ALL {
assert_eq!(eval_str(v, probe), expected, "{v:?}");
}
}
#[test]
fn require_preload_not_found_line_wording() {
for v in ALL {
let probe = "\
local ok, err = pcall(require, 'absent_xyz') \
assert(not ok) \
return err:match(\"no field package%.preload%['absent_xyz'%]\") ~= nil";
assert_true(v, probe);
let probe2 = "\
local ok, err = pcall(require, 'absent_xyz') \
return err:sub(1, 30) == \"module 'absent_xyz' not found:\"";
assert_true(v, probe2);
}
}
#[test]
fn searchpath_absent_on_5_1_present_after() {
assert_eq!(eval_str(LuaVersion::V51, "return type(package.searchpath)"), b"nil");
for v in [LuaVersion::V52, LuaVersion::V53, LuaVersion::V54, LuaVersion::V55] {
assert_eq!(eval_str(v, "return type(package.searchpath)"), b"function", "{v:?}");
}
}
#[test]
fn searchpath_failure_returns_nil_not_false() {
for v in [LuaVersion::V52, LuaVersion::V53, LuaVersion::V54, LuaVersion::V55] {
assert!(
matches!(eval_value(v, "return (package.searchpath('x', 'noq'))"), Value::Nil),
"{v:?}: searchpath fail value must be nil, got {:?}",
eval_value(v, "return (package.searchpath('x', 'noq'))")
);
}
let probe = "local _, err = package.searchpath('x', 'noq'); return err";
for v in [LuaVersion::V52, LuaVersion::V53] {
assert_eq!(eval_str(v, probe), b"\n\tno file 'noq'", "{v:?}");
}
for v in [LuaVersion::V54, LuaVersion::V55] {
assert_eq!(eval_str(v, probe), b"no file 'noq'", "{v:?}");
}
}
#[test]
fn searchpath_error_message_leading_separator_is_5_2_5_3_only() {
let probe = "\
local _, err = package.searchpath('a.b.c', './?.lua;/x/?.lua') \
return err";
for v in [LuaVersion::V52, LuaVersion::V53] {
assert_eq!(
eval_str(v, probe),
b"\n\tno file './a/b/c.lua'\n\tno file '/x/a/b/c.lua'",
"{v:?}"
);
}
for v in [LuaVersion::V54, LuaVersion::V55] {
assert_eq!(
eval_str(v, probe),
b"no file './a/b/c.lua'\n\tno file '/x/a/b/c.lua'",
"{v:?}"
);
}
}
#[test]
fn searchpath_separator_and_rep_logic() {
for v in [LuaVersion::V52, LuaVersion::V53, LuaVersion::V54, LuaVersion::V55] {
let probe = "\
local _, err = package.searchpath('a.b', './?.lua') \
return err:match(\"no file './a/b%.lua'\") ~= nil";
assert_true(v, probe);
let probe2 = "\
local _, err = package.searchpath('a.b', './?.lua', '') \
return err:match(\"no file './a%.b%.lua'\") ~= nil";
assert_true(v, probe2);
let probe3 = "\
local _, err = package.searchpath('a/b', './?.lua', '/', '_') \
return err:match(\"no file './a_b%.lua'\") ~= nil";
assert_true(v, probe3);
}
}
#[test]
fn searcher_list_name_matrix() {
assert_eq!(eval_str(LuaVersion::V51, "return type(package.loaders)"), b"table");
assert_eq!(eval_str(LuaVersion::V51, "return type(package.searchers)"), b"nil");
assert_eq!(eval_str(LuaVersion::V52, "return type(package.loaders)"), b"table");
assert_eq!(eval_str(LuaVersion::V52, "return type(package.searchers)"), b"table");
assert_true(LuaVersion::V52, "return package.loaders == package.searchers");
for v in [LuaVersion::V53, LuaVersion::V54, LuaVersion::V55] {
assert_eq!(eval_str(v, "return type(package.searchers)"), b"table", "{v:?}");
assert_eq!(eval_str(v, "return type(package.loaders)"), b"nil", "{v:?}");
}
for v in ALL {
let list = if matches!(v, LuaVersion::V51) { "loaders" } else { "searchers" };
let probe = format!("return type(package.{list}[1])");
assert_eq!(eval_str(v, &probe), b"function", "{v:?}");
}
}
#[test]
fn module_and_seeall_roster_matrix() {
for v in [LuaVersion::V51, LuaVersion::V52] {
assert_eq!(eval_str(v, "return type(module)"), b"function", "{v:?}");
assert_eq!(eval_str(v, "return type(package.seeall)"), b"function", "{v:?}");
}
for v in [LuaVersion::V53, LuaVersion::V54, LuaVersion::V55] {
assert_eq!(eval_str(v, "return type(module)"), b"nil", "{v:?}");
assert_eq!(eval_str(v, "return type(package.seeall)"), b"nil", "{v:?}");
}
}
#[test]
fn module_seeall_creates_and_registers_module() {
let probe = "\
module('foo', package.seeall) \
return _NAME .. ',' .. tostring(_M == foo) .. ',' \
.. tostring(package.loaded.foo == foo) .. ',' .. tostring(print ~= nil)";
assert_eq!(eval_str(LuaVersion::V51, probe), b"foo,true,true,true");
}
fn no_env_hook(_name: &[u8]) -> Option<Vec<u8>> {
None
}
fn env_versioned_wins_hook(name: &[u8]) -> Option<Vec<u8>> {
match name {
b"LUA_PATH_5_2" | b"LUA_PATH_5_3" | b"LUA_PATH_5_4" | b"LUA_PATH_5_5" => {
Some(b"/versioned/?.lua".to_vec())
}
b"LUA_PATH" => Some(b"/unversioned/?.lua".to_vec()),
_ => None,
}
}
fn env_5_1_hook(name: &[u8]) -> Option<Vec<u8>> {
match name {
b"LUA_PATH_5_1" => Some(b"/should-be-ignored/?.lua".to_vec()),
b"LUA_PATH" => Some(b"/unversioned/?.lua".to_vec()),
_ => None,
}
}
fn eval_str_with_hooks(version: LuaVersion, hooks: HostHooks, code: &str) -> Vec<u8> {
let lua = Lua::with_hooks_versioned(hooks, version).expect("Lua runtime should initialize");
match lua.load(code).eval::<Value>() {
Ok(Value::String(s)) => s
.as_bytes()
.unwrap_or_else(|e| panic!("string bytes under {version:?} for `{code}`: {e:?}"))
.to_vec(),
Ok(other) => panic!("`{code}` under {version:?} returned {other:?}, expected a string"),
Err(e) => panic!("eval of `{code}` failed under {version:?}: {e:?}"),
}
}
#[test]
#[cfg(not(target_os = "windows"))]
fn default_package_path_is_version_exact() {
assert_eq!(
eval_str_with_hooks(LuaVersion::V51, HostHooks::new().env(no_env_hook), "return package.path"),
b"./?.lua;/usr/local/share/lua/5.1/?.lua;/usr/local/share/lua/5.1/?/init.lua;\
/usr/local/lib/lua/5.1/?.lua;/usr/local/lib/lua/5.1/?/init.lua"
.to_vec()
);
assert_eq!(
eval_str_with_hooks(LuaVersion::V52, HostHooks::new().env(no_env_hook), "return package.path"),
b"/usr/local/share/lua/5.2/?.lua;/usr/local/share/lua/5.2/?/init.lua;\
/usr/local/lib/lua/5.2/?.lua;/usr/local/lib/lua/5.2/?/init.lua;./?.lua"
.to_vec()
);
for (v, expected) in [
(
LuaVersion::V53,
b"/usr/local/share/lua/5.3/?.lua;/usr/local/share/lua/5.3/?/init.lua;\
/usr/local/lib/lua/5.3/?.lua;/usr/local/lib/lua/5.3/?/init.lua;\
./?.lua;./?/init.lua" as &[u8],
),
(
LuaVersion::V54,
b"/usr/local/share/lua/5.4/?.lua;/usr/local/share/lua/5.4/?/init.lua;\
/usr/local/lib/lua/5.4/?.lua;/usr/local/lib/lua/5.4/?/init.lua;\
./?.lua;./?/init.lua",
),
(
LuaVersion::V55,
b"/usr/local/share/lua/5.5/?.lua;/usr/local/share/lua/5.5/?/init.lua;\
/usr/local/lib/lua/5.5/?.lua;/usr/local/lib/lua/5.5/?/init.lua;\
./?.lua;./?/init.lua",
),
] {
assert_eq!(
eval_str_with_hooks(v, HostHooks::new().env(no_env_hook), "return package.path"),
expected,
"{v:?}"
);
}
}
#[test]
#[cfg(not(target_os = "windows"))]
fn default_package_cpath_is_version_exact() {
assert_eq!(
eval_str_with_hooks(LuaVersion::V51, HostHooks::new().env(no_env_hook), "return package.cpath"),
b"./?.so;/usr/local/lib/lua/5.1/?.so;/usr/local/lib/lua/5.1/loadall.so".to_vec()
);
for (v, expected) in [
(
LuaVersion::V52,
b"/usr/local/lib/lua/5.2/?.so;/usr/local/lib/lua/5.2/loadall.so;./?.so" as &[u8],
),
(
LuaVersion::V53,
b"/usr/local/lib/lua/5.3/?.so;/usr/local/lib/lua/5.3/loadall.so;./?.so",
),
(
LuaVersion::V54,
b"/usr/local/lib/lua/5.4/?.so;/usr/local/lib/lua/5.4/loadall.so;./?.so",
),
(
LuaVersion::V55,
b"/usr/local/lib/lua/5.5/?.so;/usr/local/lib/lua/5.5/loadall.so;./?.so",
),
] {
assert_eq!(
eval_str_with_hooks(v, HostHooks::new().env(no_env_hook), "return package.cpath"),
expected,
"{v:?}"
);
}
}
#[test]
fn versioned_env_var_wins_over_unversioned_from_5_2() {
for v in [LuaVersion::V52, LuaVersion::V53, LuaVersion::V54, LuaVersion::V55] {
assert_eq!(
eval_str_with_hooks(
v,
HostHooks::new().env(env_versioned_wins_hook),
"return package.path"
),
b"/versioned/?.lua".to_vec(),
"{v:?}"
);
}
}
#[test]
fn v5_1_has_no_versioned_env_vars() {
assert_eq!(
eval_str_with_hooks(LuaVersion::V51, HostHooks::new().env(env_5_1_hook), "return package.path"),
b"/unversioned/?.lua".to_vec()
);
}
fn versioned_package_env(name: &[u8]) -> Option<Vec<u8>> {
match name {
b"LUA_PATH_5_1" => Some(b"path-5.1".to_vec()),
b"LUA_CPATH_5_1" => Some(b"cpath-5.1".to_vec()),
b"LUA_PATH_5_4" => Some(b"path-5.4".to_vec()),
b"LUA_CPATH_5_4" => Some(b"cpath-5.4".to_vec()),
b"LUA_PATH_5_5" => Some(b"path-5.5".to_vec()),
b"LUA_CPATH_5_5" => Some(b"cpath-5.5".to_vec()),
_ => None,
}
}
fn empty_env(_name: &[u8]) -> Option<Vec<u8>> {
None
}
#[test]
fn v51_ignores_versioned_package_env_vars() {
let hooks = HostHooks::new().env(versioned_package_env);
let lua = Lua::with_hooks_versioned(hooks, LuaVersion::V51).expect("5.1 runtime");
let value = lua
.load("return package.path")
.eval::<Value>()
.expect("package path");
let Value::String(value) = value else {
panic!("package.path was not a string");
};
let path = value.to_str().expect("UTF-8 package path");
assert!(
path.starts_with("./?.lua;/usr/local/share/lua/5.1/"),
"5.1 must ignore LUA_PATH_5_1 and keep its ./?.lua-first defaults, got: {path}"
);
}
#[test]
fn package_paths_use_the_active_version_env_suffix() {
for (version, expected) in [
(LuaVersion::V54, "path-5.4,cpath-5.4"),
(LuaVersion::V55, "path-5.5,cpath-5.5"),
] {
let hooks = HostHooks::new().env(versioned_package_env);
let lua = Lua::with_hooks_versioned(hooks, version).expect("versioned runtime");
let value = lua
.load("return package.path .. ',' .. package.cpath")
.eval::<Value>()
.expect("package paths");
let Value::String(value) = value else {
panic!("package paths under {version:?} were not a string");
};
assert_eq!(
value.to_str().expect("UTF-8 package paths"),
expected,
"{version:?}"
);
}
}
#[test]
fn default_package_paths_use_the_active_version_directory() {
for (version, directory) in [
(LuaVersion::V51, "/lua/5.1/"),
(LuaVersion::V54, "/lua/5.4/"),
(LuaVersion::V55, "/lua/5.5/"),
] {
let hooks = HostHooks::new().env(empty_env);
let lua = Lua::with_hooks_versioned(hooks, version).expect("versioned runtime");
let value = lua
.load("return package.path .. ';' .. package.cpath")
.eval::<Value>()
.expect("package paths");
let Value::String(value) = value else {
panic!("package paths under {version:?} were not a string");
};
let value = value.to_str().expect("UTF-8 package paths");
assert!(value.contains(directory), "{version:?}: {value}");
}
}