use crate::common::create_empty_context;
use pasta_lua::context::TranspileContext;
use pasta_lua::loader::{LoaderContext, TranspileResult};
use pasta_lua::{PastaLuaRuntime, RuntimeConfig};
use std::path::Path;
use tempfile::TempDir;
#[test]
fn test_exec_file_executes_script_and_returns_value() {
let temp = TempDir::new().unwrap();
let script_path = temp.path().join("answer.lua");
std::fs::write(&script_path, "return 6 * 7").unwrap();
let runtime = PastaLuaRuntime::new(create_empty_context()).unwrap();
let value = runtime.exec_file(&script_path).unwrap();
assert_eq!(value.as_i64(), Some(42));
}
#[test]
fn test_exec_file_missing_file_returns_error() {
let temp = TempDir::new().unwrap();
let missing = temp.path().join("no_such_script.lua");
let runtime = PastaLuaRuntime::new(create_empty_context()).unwrap();
let result = runtime.exec_file(&missing);
assert!(result.is_err(), "missing file should produce an error");
}
#[test]
fn test_exec_file_propagates_script_error() {
let temp = TempDir::new().unwrap();
let script_path = temp.path().join("boom.lua");
std::fs::write(&script_path, r#"error("file boom")"#).unwrap();
let runtime = PastaLuaRuntime::new(create_empty_context()).unwrap();
let err = runtime.exec_file(&script_path).unwrap_err();
assert!(
err.to_string().contains("file boom"),
"error message should contain the Lua error text: {err}"
);
}
#[test]
fn test_exec_named_chunk_name_appears_in_error() {
let runtime = PastaLuaRuntime::new(create_empty_context()).unwrap();
let err = runtime
.exec_named("local x = nil\nreturn x + 1", "cell316_chunk.lua")
.unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("cell316_chunk"),
"chunk name should appear in the error: {msg}"
);
assert!(
msg.contains(":2:"),
"line number of the failing statement should appear: {msg}"
);
}
#[test]
fn test_exec_named_returns_value() {
let runtime = PastaLuaRuntime::new(create_empty_context()).unwrap();
let value = runtime.exec_named("return 1 + 2", "sum_chunk").unwrap();
assert_eq!(value.as_i64(), Some(3));
}
#[test]
fn test_register_module_makes_module_requirable() {
let runtime = PastaLuaRuntime::new(create_empty_context()).unwrap();
let module = runtime.lua().create_table().unwrap();
module.set("answer", 42).unwrap();
runtime.register_module("@cell316_custom", module).unwrap();
let value = runtime
.exec(r#"return require("@cell316_custom").answer"#)
.unwrap();
assert_eq!(value.as_i64(), Some(42));
}
#[test]
fn test_register_module_overwrites_existing_registration() {
let runtime = PastaLuaRuntime::new(create_empty_context()).unwrap();
let first = runtime.lua().create_table().unwrap();
first.set("version", 1).unwrap();
runtime.register_module("@cell316_overwrite", first).unwrap();
let second = runtime.lua().create_table().unwrap();
second.set("version", 2).unwrap();
runtime
.register_module("@cell316_overwrite", second)
.unwrap();
let value = runtime
.exec(r#"return require("@cell316_overwrite").version"#)
.unwrap();
assert_eq!(value.as_i64(), Some(2), "later registration should win");
}
#[test]
fn test_with_config_unknown_library_fails_with_name() {
let config = RuntimeConfig::from_libs(vec!["std_bogus".into()]);
let err = match PastaLuaRuntime::with_config(create_empty_context(), config) {
Ok(_) => panic!("with_config should fail for unknown library"),
Err(e) => e,
};
assert!(
err.to_string().contains("std_bogus"),
"error should name the unknown library: {err}"
);
}
#[test]
fn test_config_is_none_for_adhoc_runtime() {
let runtime = PastaLuaRuntime::new(create_empty_context()).unwrap();
assert!(runtime.config().is_none());
}
#[test]
fn test_lua_accessor_shares_vm_state_with_exec() {
let runtime = PastaLuaRuntime::new(create_empty_context()).unwrap();
runtime.lua().globals().set("CELL316_SHARED", 7).unwrap();
let value = runtime.exec("return CELL316_SHARED").unwrap();
assert_eq!(value.as_i64(), Some(7));
}
#[test]
fn test_load_scene_dic_executes_file() {
let temp = TempDir::new().unwrap();
let dic_path = temp.path().join("scene_dic.lua");
std::fs::write(&dic_path, "SCENE_DIC_SENTINEL = 99").unwrap();
let runtime = PastaLuaRuntime::new(create_empty_context()).unwrap();
runtime.load_scene_dic(&dic_path).unwrap();
let value = runtime.exec("return SCENE_DIC_SENTINEL").unwrap();
assert_eq!(value.as_i64(), Some(99));
}
#[test]
fn test_load_scene_dic_error_carries_chunk_name() {
let temp = TempDir::new().unwrap();
let dic_path = temp.path().join("scene_dic.lua");
std::fs::write(&dic_path, "local x = nil\nreturn x + 1").unwrap();
let runtime = PastaLuaRuntime::new(create_empty_context()).unwrap();
let err = runtime.load_scene_dic(&dic_path).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains(r#"[string "pasta.scene_dic"]:2:"#),
"chunk name + line prefix should appear as [string \"pasta.scene_dic\"]:2: — {msg}"
);
}
#[test]
fn test_load_scene_dic_missing_file_returns_error() {
let runtime = PastaLuaRuntime::new(create_empty_context()).unwrap();
let result = runtime.load_scene_dic(Path::new("definitely/missing/scene_dic.lua"));
assert!(result.is_err());
}
#[test]
fn test_from_loader_loads_entry_lua_when_present() {
let temp = TempDir::new().unwrap();
let entry_dir = temp.path().join("scripts/pasta/shiori");
std::fs::create_dir_all(&entry_dir).unwrap();
std::fs::write(entry_dir.join("entry.lua"), "CELL316_ENTRY_LOADED = true").unwrap();
let loader_context = LoaderContext::new(
temp.path(),
vec!["scripts".to_string()],
toml::Table::new(),
);
let runtime = PastaLuaRuntime::from_loader(
TranspileContext::new(),
loader_context,
RuntimeConfig::new(),
&[],
None,
)
.unwrap();
let value = runtime.exec("return CELL316_ENTRY_LOADED == true").unwrap();
assert!(value.as_boolean().unwrap_or(false));
}
#[test]
fn test_from_loader_continues_when_entry_lua_fails() {
let temp = TempDir::new().unwrap();
let entry_dir = temp.path().join("scripts/pasta/shiori");
std::fs::create_dir_all(&entry_dir).unwrap();
std::fs::write(entry_dir.join("entry.lua"), r#"error("entry broken")"#).unwrap();
let loader_context = LoaderContext::new(
temp.path(),
vec!["scripts".to_string()],
toml::Table::new(),
);
let runtime = PastaLuaRuntime::from_loader(
TranspileContext::new(),
loader_context,
RuntimeConfig::new(),
&[],
None,
);
assert!(
runtime.is_ok(),
"from_loader must continue despite entry.lua failure"
);
}
#[test]
fn test_from_loader_executes_transpiled_modules() {
let transpiled = vec![
TranspileResult {
module_name: "cell316_mod_a".to_string(),
lua_code: "CELL316_TRANSPILED_A = 7".to_string(),
source_path: std::path::PathBuf::from("a.pasta"),
},
TranspileResult {
module_name: "cell316_mod_b".to_string(),
lua_code: "CELL316_TRANSPILED_B = CELL316_TRANSPILED_A + 1".to_string(),
source_path: std::path::PathBuf::from("b.pasta"),
},
];
let loader_context = LoaderContext::new(
"/test/path",
vec!["scripts".to_string()],
toml::Table::new(),
);
let runtime = PastaLuaRuntime::from_loader(
TranspileContext::new(),
loader_context,
RuntimeConfig::new(),
&transpiled,
None,
)
.unwrap();
let value = runtime.exec("return CELL316_TRANSPILED_B").unwrap();
assert_eq!(value.as_i64(), Some(8));
}
#[test]
fn test_from_loader_transpiled_error_propagates_with_module_name() {
let transpiled = vec![TranspileResult {
module_name: "cell316_mod_err".to_string(),
lua_code: "local x = nil\nreturn x + 1".to_string(),
source_path: std::path::PathBuf::from("err.pasta"),
}];
let loader_context = LoaderContext::new(
"/test/path",
vec!["scripts".to_string()],
toml::Table::new(),
);
let err = match PastaLuaRuntime::from_loader(
TranspileContext::new(),
loader_context,
RuntimeConfig::new(),
&transpiled,
None,
) {
Ok(_) => panic!("from_loader should fail when transpiled code errors"),
Err(e) => e,
};
let msg = err.to_string();
assert!(
msg.contains("cell316_mod_err"),
"module name should appear as chunk name: {msg}"
);
}
fn runtime_with_custom_fields(custom_fields: toml::Table) -> PastaLuaRuntime {
let loader_context = LoaderContext::new(
"/test/path",
vec!["scripts".to_string()],
custom_fields,
);
PastaLuaRuntime::from_loader(
TranspileContext::new(),
loader_context,
RuntimeConfig::new(),
&[],
None,
)
.unwrap()
}
#[test]
fn test_pasta_config_toml_value_conversions() {
let custom_fields: toml::Table = toml::from_str(
r#"
int_val = 42
float_val = 1.5
bool_val = true
date_val = 2026-06-11T00:00:00Z
arr = ["a", "b", "c"]
[nested]
inner = "deep"
"#,
)
.unwrap();
let runtime = runtime_with_custom_fields(custom_fields);
let value = runtime
.exec(
r#"
local config = require "@pasta_config"
assert(config.int_val == 42, "int_val")
assert(config.float_val == 1.5, "float_val")
assert(config.bool_val == true, "bool_val")
assert(type(config.date_val) == "string", "date_val type")
assert(config.date_val:find("2026%-06%-11") ~= nil, "date_val content")
assert(#config.arr == 3, "arr length")
assert(config.arr[1] == "a" and config.arr[3] == "c", "arr 1-based order")
assert(config.nested.inner == "deep", "nested table")
return true
"#,
)
.unwrap();
assert!(value.as_boolean().unwrap_or(false));
}
#[test]
fn test_pasta_config_actor_name_injection() {
let custom_fields: toml::Table = toml::from_str(
r#"
[actor."さくら"]
spot = 0
[actor."うにゅう"]
spot = 1
name = "should_be_overwritten"
"#,
)
.unwrap();
let runtime = runtime_with_custom_fields(custom_fields);
let value = runtime
.exec(
r#"
local config = require "@pasta_config"
assert(config.actor["さくら"].name == "さくら", "injected name")
assert(config.actor["うにゅう"].name == "うにゅう", "TOML key overwrites existing name")
return true
"#,
)
.unwrap();
assert!(value.as_boolean().unwrap_or(false));
}
#[test]
fn test_pasta_config_non_table_actor_is_noop() {
let custom_fields: toml::Table = toml::from_str(r#"actor = "not_a_table""#).unwrap();
let runtime = runtime_with_custom_fields(custom_fields);
let value = runtime
.exec(
r#"
local config = require "@pasta_config"
return config.actor == "not_a_table"
"#,
)
.unwrap();
assert!(value.as_boolean().unwrap_or(false));
}
#[test]
fn test_log_deeply_nested_table_does_not_overflow_host_stack() {
let runtime = PastaLuaRuntime::new(create_empty_context()).unwrap();
let value = runtime
.exec(
r#"
local log = require "@pasta_log"
local t = {}
for _ = 1, 200000 do t = { t } end
log.info(t)
return true
"#,
)
.unwrap();
assert!(value.as_boolean().unwrap_or(false));
}
#[tracing_test::traced_test]
#[test]
fn test_log_table_at_depth_limit_is_json_converted() {
let runtime = PastaLuaRuntime::new(create_empty_context()).unwrap();
runtime
.exec(
r#"
local log = require "@pasta_log"
local t = { leaf = "d10_json_marker" }
for _ = 1, 9 do t = { t } end
log.info(t)
"#,
)
.unwrap();
assert!(
logs_contain("d10_json_marker"),
"depth-10 table should be JSON-converted, exposing the leaf value"
);
}
#[tracing_test::traced_test]
#[test]
fn test_log_table_one_past_depth_limit_falls_back_to_tostring() {
let runtime = PastaLuaRuntime::new(create_empty_context()).unwrap();
runtime
.exec(
r#"
local log = require "@pasta_log"
local t = { leaf = "d11_fallback_marker" }
for _ = 1, 10 do t = { t } end
log.info(t)
"#,
)
.unwrap();
assert!(
logs_contain("table: "),
"depth-11 table should fall back to Lua tostring()"
);
assert!(
!logs_contain("d11_fallback_marker"),
"leaf value must not leak through JSON conversion past the depth limit"
);
}
#[test]
fn test_log_cyclic_table_falls_back_without_error() {
let runtime = PastaLuaRuntime::new(create_empty_context()).unwrap();
let value = runtime
.exec(
r#"
local log = require "@pasta_log"
local t = {}
t.self = t
log.warn(t)
return true
"#,
)
.unwrap();
assert!(value.as_boolean().unwrap_or(false));
}