use std::path::Path;
use std::sync::{Arc, Mutex};
use mlua::{Debug, HookTriggers, Lua, LuaOptions, StdLib, VmState};
use pasta_dsl::parser::parse_str;
use pasta_lua::LuaTranspiler;
use pasta_lua::debug::source_map::canonicalize_chunk_name;
use pasta_lua::loader::CacheManager;
const PASTA_SRC: &str = "\
*あいさつ
さくら:「こんにちは!」
";
const DIC_REL: &str = "dic/baseware/system.pasta";
const EXPECTED_MODULE: &str = "pasta.scene.baseware.system";
fn build_all_safe_vm() -> Lua {
unsafe { Lua::unsafe_new_with(StdLib::ALL_SAFE, LuaOptions::default()) }
}
fn package_path_for(cache_lua_root: &Path) -> String {
let root = cache_lua_root.to_string_lossy().replace('\\', "/");
format!("{root}/?.lua;{root}/?/init.lua")
}
#[test]
fn hook_source_matches_loader_cache_key_after_normalization() {
let temp = tempfile::TempDir::new().expect("temp dir");
let base_dir = temp.path().to_path_buf();
let source_path = base_dir.join(DIC_REL);
std::fs::create_dir_all(source_path.parent().unwrap()).expect("mkdir dic");
std::fs::write(&source_path, PASTA_SRC).expect("write .pasta");
let pasta_file = parse_str(PASTA_SRC, &source_path.to_string_lossy()).expect("parse .pasta");
let transpiler = LuaTranspiler::default();
let mut out: Vec<u8> = Vec::new();
transpiler
.transpile(&pasta_file, &mut out)
.expect("transpile ok");
let lua_code = String::from_utf8(out).expect("utf-8 lua");
let cache_manager = CacheManager::new(base_dir.clone(), "profile/pasta/cache/lua");
cache_manager.prepare_cache_dir().expect("prepare cache");
let loader_cache_path = cache_manager.source_to_cache_path(&source_path);
let module_name = cache_manager.source_to_module_name(&source_path);
assert_eq!(
module_name, EXPECTED_MODULE,
"前提: モジュール名は require 解決の元になる"
);
let saved_module = cache_manager
.save_cache(&source_path, &lua_code)
.expect("save cache");
assert_eq!(saved_module, EXPECTED_MODULE);
assert!(
loader_cache_path.exists(),
"キャッシュ .lua が source_to_cache_path の位置に保存されている"
);
let loader_key_raw = loader_cache_path.to_string_lossy().to_string();
let cache_lua_root = base_dir.join("profile/pasta/cache/lua");
let pkg_path = package_path_for(&cache_lua_root);
let lua = build_all_safe_vm();
let captured: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
let sink = Arc::clone(&captured);
lua.load("jit.off()").exec().expect("jit.off");
lua.set_global_hook(HookTriggers::EVERY_LINE, move |_lua, debug: &Debug| {
let src = debug
.source()
.source
.as_ref()
.map(|c| c.as_ref().to_string())
.unwrap_or_default();
if let Ok(mut g) = sink.lock() {
g.push(src);
}
Ok(VmState::Continue)
})
.expect("install line hook");
const STUB: &str = r#"
local stub_mt = {}
stub_mt.__index = function(_, _) return function(...) return setmetatable({}, stub_mt) end end
stub_mt.__call = function(_, ...) return setmetatable({}, stub_mt) end
local function stub() return setmetatable({}, stub_mt) end
local PASTA = {}
function PASTA.create_scene(_) return setmetatable({}, stub_mt) end
function PASTA.create_actor(_) return stub() end
function PASTA.create_word(_) return stub() end
function PASTA.finalize_scene() end
package.preload["pasta"] = function() return PASTA end
package.preload["pasta.global"] = function() return stub() end
"#;
lua.load(STUB)
.set_name("@chunk_name_stub")
.exec()
.expect("stub preamble");
lua.load(format!("package.path = [[{pkg_path}]]"))
.set_name("@chunk_name_setpath")
.exec()
.expect("set package.path");
lua.load(format!("require(\"{EXPECTED_MODULE}\")"))
.set_name("@chunk_name_require")
.exec()
.expect("require transpiled module");
lua.remove_global_hook();
let driver_names = [
"@chunk_name_stub",
"@chunk_name_setpath",
"@chunk_name_require",
];
let chunk_sources: Vec<String> = captured
.lock()
.unwrap()
.iter()
.filter(|s| !driver_names.contains(&s.as_str()) && !s.is_empty())
.cloned()
.collect();
assert!(
!chunk_sources.is_empty(),
"ラインフックがロードしたチャンク上で発火し source を報告するはず(実測の前提)"
);
let hook_source_raw = chunk_sources[0].clone();
for s in &chunk_sources {
assert_eq!(
s, &hook_source_raw,
"ロードした単一チャンクの source は一定({chunk_sources:?})"
);
}
eprintln!("=== Task 1.1 chunk-name empirical findings ===");
eprintln!("HOOK_SOURCE_RAW = {hook_source_raw:?}");
eprintln!("LOADER_KEY_RAW = {loader_key_raw:?}");
eprintln!(
"HOOK_CANON = {:?}",
canonicalize_chunk_name(&hook_source_raw)
);
eprintln!(
"LOADER_CANON = {:?}",
canonicalize_chunk_name(&loader_key_raw)
);
assert!(
hook_source_raw.starts_with('@'),
"フック source は `@` 接頭辞付き(loadfile/require 由来)。got: {hook_source_raw:?}"
);
assert!(
hook_source_raw.ends_with("system.lua"),
"フック source は対象 .lua で終わる。got: {hook_source_raw:?}"
);
let hook_canon = canonicalize_chunk_name(&hook_source_raw);
let loader_canon = canonicalize_chunk_name(&loader_key_raw);
assert_eq!(
hook_canon, loader_canon,
"requirements 4.2/5.1・design 437-440: フック source とローダ由来キーは\n\
正規化(@除去・区切り統一・Windows大小無視)後に一致しなければならない。\n\
一致しない場合は `set_name` 明示命名が必要(task 4.3 へ申し送り)。\n\
HOOK_RAW={hook_source_raw:?}\nLOADER_RAW={loader_key_raw:?}"
);
let hook_no_at = hook_source_raw.strip_prefix('@').unwrap();
assert_ne!(
hook_no_at, loader_key_raw,
"実機では生文字列はバイト一致しない(区切り差など)→ 正規化が必須であることの確認"
);
}