gmod-rs/crates/lua/tests/include.rs
Niko Marmeladkov 0b82cd77f3 Lua: continue, include и загрузка геймода
- Препроцессор GMod: continue внутри for/while/repeat -> goto + уникальная
  метка; учтены строки/комментарии, вложенные циклы и функции, завершающие
  break/return (оборачиваются в do ... end). widget_bones.lua грузится:
  энтити 6 из 6.
- include(): поиск рядом с текущим файлом и в lua/ игры, тот же препроцессор,
  лимиты размера/глубины, защита от циклов и выхода за каталог игры.
- LuaRuntime::load_gamemode(): init.lua и cl_init.lua, сбор ошибок без
  падения. Sandbox init.lua выполняется, cl_init.lua упирается в VGUI
  (AccessorFunc); реестры list/scripted_ents после загрузки целы.
- Безопасные заглушки загрузочного слоя Sandbox: CLIENT/SERVER, is*,
  FindMetaTable, cleanup, drive, duplicator, net, cvars, player_manager.
- Тесты: юнит-тесты препроцессора, include на временных файлах и реальных
  lua/includes, загрузка sandbox на ассетах.
2026-10-24 13:29:30 +03:00

196 lines
6.3 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Настоящий `include()`: поиск в `lua/`, препроцессор, циклы и песочница.
use glua::LuaRuntime;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicUsize, Ordering};
static NEXT_DIR: AtomicUsize = AtomicUsize::new(0);
/// Уникальный временный каталог игры для одного теста.
struct TempGame {
root: PathBuf,
}
impl TempGame {
fn new(name: &str) -> Self {
let root = std::env::temp_dir().join(format!(
"glua_include_{name}_{}_{}",
std::process::id(),
NEXT_DIR.fetch_add(1, Ordering::Relaxed)
));
let _ = std::fs::remove_dir_all(&root);
std::fs::create_dir_all(root.join("lua")).expect("каталог игры");
Self { root }
}
fn write(&self, relative: &str, contents: &str) {
let path = self.root.join(relative);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).expect("каталог скрипта");
}
std::fs::write(path, contents).expect("файл скрипта");
}
fn path(&self) -> &Path {
&self.root
}
}
impl Drop for TempGame {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.root);
}
}
#[test]
fn include_preprocesses_and_does_not_repeat_files() {
let game = TempGame::new("basic");
game.write("lua/inc/counter.lua", "counter = ( counter or 0 ) + 1\n");
game.write(
"lua/inc/syntax.lua",
r#"
local out = {}
for i = 1, 3 do
if ( i == 2 ) then continue end
out[ #out + 1 ] = i
end
continued = table.concat( out, "" )
if ( !flag ) then not_flag = true end
"#,
);
game.write(
"lua/inc/cycle_a.lua",
"cycle_a = ( cycle_a or 0 ) + 1\ninclude( \"inc/cycle_b.lua\" )\n",
);
game.write(
"lua/inc/cycle_b.lua",
"cycle_b = ( cycle_b or 0 ) + 1\ninclude( \"inc/cycle_a.lua\" )\n",
);
let rt = LuaRuntime::new().expect("рантайм");
rt.set_file_root(game.path()).expect("корень");
rt.run(
"include_test.lua",
br#"
flag = false
include( "inc/counter.lua" )
include( "inc/counter.lua" )
include( "inc/syntax.lua" )
include( "inc/cycle_a.lua" )
"#,
)
.expect("include выполнился");
assert_eq!(
rt.probe("return tostring(counter)").expect("probe"),
Some("1".into())
);
// Препроцессор тот же: continue превратился в goto, `!` — в not.
assert_eq!(
rt.probe("return continued").expect("probe"),
Some("13".into())
);
assert_eq!(
rt.probe("return tostring(not_flag)").expect("probe"),
Some("true".into())
);
// Цикл a -> b -> a не зациклился и не выполнил a второй раз.
assert_eq!(
rt.probe("return tostring(cycle_a) .. tostring(cycle_b)")
.expect("probe"),
Some("11".into())
);
}
#[test]
fn include_refuses_escape_and_oversized_files() {
let game = TempGame::new("sandbox");
game.write("lua/ok.lua", "ok = true\n");
let secret_name = format!(
"glua_include_secret_{}_{}.lua",
std::process::id(),
NEXT_DIR.fetch_add(1, Ordering::Relaxed)
);
let outside = game.path().parent().expect("родитель").join(&secret_name);
std::fs::write(&outside, "leaked = true\n").expect("файл вне корня");
let rt = LuaRuntime::new().expect("рантайм");
rt.set_file_root(game.path()).expect("корень");
rt.run("ok.lua", b"include( \"ok.lua\" )")
.expect("обычный include");
let script = format!("include( \"../../{secret_name}\" )");
let escape = rt.run("escape.lua", script.as_bytes());
assert!(escape.is_err(), "выход за пределы каталога игры запрещён");
game.write(
"lua/big.lua",
&"-- большой файл\n".repeat((1 << 20) / 16 + 1),
);
let big = rt.run("big.lua", b"include( \"big.lua\" )");
assert!(big.is_err(), "файл больше лимита должен быть отклонён");
let _ = std::fs::remove_file(outside);
}
#[test]
fn load_gamemode_collects_errors_and_continues() {
let game = TempGame::new("gamemode");
game.write(
"gamemodes/test/gamemode/init.lua",
"error( \"init упал\" )\n",
);
game.write(
"gamemodes/test/gamemode/cl_init.lua",
"include( \"shared.lua\" )\ncl_loaded = true\n",
);
game.write(
"gamemodes/test/gamemode/shared.lua",
"shared_loaded = true\n",
);
let rt = LuaRuntime::new().expect("рантайм");
let report = rt.load_gamemode(game.path(), "test");
assert_eq!(report.loaded, vec!["cl_init.lua".to_string()]);
assert_eq!(report.errors.len(), 1, "{:?}", report.errors);
assert!(
report.errors[0].contains("init упал"),
"{:?}",
report.errors
);
assert_eq!(
rt.probe("return tostring(cl_loaded) .. tostring(shared_loaded)")
.expect("probe"),
Some("truetrue".into()),
"cl_init и include рядом с геймодом должны выполниться"
);
// Имя режима не должно становиться путём.
let bad = rt.load_gamemode(game.path(), "../escape");
assert_eq!(bad.loaded.len(), 0);
assert!(!bad.errors.is_empty());
}
#[test]
fn include_limits_depth() {
let game = TempGame::new("depth");
for index in 1..=10 {
game.write(
&format!("lua/depth/f{index}.lua"),
&format!(
"depth = ( depth or 0 ) + 1\ninclude( \"depth/f{}.lua\" )\n",
index + 1
),
);
}
// Последний файл цепочки ничего не включает.
game.write("lua/depth/f11.lua", "depth_end = true\n");
let rt = LuaRuntime::new().expect("рантайм");
rt.set_file_root(game.path()).expect("корень");
let result = rt.run("depth.lua", b"include( \"depth/f1.lua\" )");
let error = result.expect_err("глубина должна быть ограничена");
assert!(
error.to_string().contains("глубина"),
"неожиданная ошибка: {error}"
);
}