effectcraft/crates/script/src/tests_include.rs

95 lines
4.2 KiB
Rust

//! #759: ExtendScript's `#include` / `//@include` / `#includepath` directives.
use std::path::{Path, PathBuf};
use effectcraft_engine::Session;
use serde_json::json;
use crate::{Outcome, run_code};
fn session() -> Session {
let mut s = Session::default();
crate::install(&mut s);
s
}
fn tmp(tag: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("ec-include-{tag}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
dir
}
fn write(path: &Path, text: &str) {
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(path, text).unwrap();
}
/// Run the script file `main` (its code read from disk, as File ▸ Scripts ▸ Run Script File does).
fn run_file(s: &mut Session, main: &Path) -> Outcome {
let code = std::fs::read_to_string(main).unwrap();
run_code(s, &code, &main.to_string_lossy())
}
#[test]
fn includes_are_inlined_relative_to_the_script() {
let dir = tmp("ok");
write(&dir.join("lib/math.jsx"), "#include \"util.jsxinc\"\nfunction twice(x) { return 2 * x; }");
write(&dir.join("lib/util.jsxinc"), "// includes its includer back: inlined once only\n#include \"math.jsx\"\nvar unit = 'px';");
write(&dir.join("vendor/extra.jsx"), "var extra = 40;");
write(
&dir.join("main.jsx"),
"#target aftereffects\n#targetengine \"main\"\n#strict on\n//@include 'lib/math.jsx'\n#includepath \"vendor;lib\"\n#include \"extra.jsx\";\n#include \"util.jsxinc\"\n[twice(1) + extra, unit, $.fileName.split('/').pop()]",
);
let mut s = session();
let o = run_file(&mut s, &dir.join("main.jsx"));
assert!(o.error.is_none(), "{:?}", o.error);
assert_eq!(o.result, json!([42, "px", "main.jsx"]));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn errors_point_at_the_file_and_line_they_came_from() {
let dir = tmp("err");
write(&dir.join("lib.jsx"), "var a = 1;\nvar b = 2;\nnotDefined();");
write(&dir.join("main.jsx"), "// main\n#include \"lib.jsx\"\nvar c = 3;");
let mut s = session();
let e = run_file(&mut s, &dir.join("main.jsx")).error.expect("an error");
assert!(e.file.ends_with("lib.jsx") && e.line == Some(3), "{e:?}");
// After the include, lines in the script itself keep their numbers.
write(&dir.join("lib.jsx"), "var a = 1;\nvar b = 2;");
write(&dir.join("main.jsx"), "// main\n#include \"lib.jsx\"\nvar c = 3;\nalsoNotDefined();");
let e = run_file(&mut s, &dir.join("main.jsx")).error.expect("an error");
assert!(e.file.ends_with("main.jsx") && e.line == Some(4), "{e:?}");
// A missing file is a clear error on the directive's line.
write(&dir.join("main.jsx"), "var x = 1;\n#include \"missing.jsx\"");
let e = run_file(&mut s, &dir.join("main.jsx")).error.expect("an error");
assert!(e.message.contains("missing.jsx") && e.line == Some(2), "{e:?}");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn hostile_includes_are_bounded_and_gated() {
let dir = tmp("hostile");
// A chain deeper than the limit is an error, not a stack overflow.
for i in 0..100 {
write(&dir.join(format!("chain/{i}.jsx")), &format!("#include \"{}.jsx\"", i + 1));
}
write(&dir.join("chain/100.jsx"), "1");
let mut s = session();
let e = run_file(&mut s, &dir.join("chain/0.jsx")).error.expect("an error");
assert!(e.message.contains("nested too deeply"), "{e:?}");
// Files outside the script's (and the project's) folder need the file preference.
write(&dir.join("outside.jsx"), "var secret = 7;");
write(&dir.join("app/main.jsx"), "#include \"../outside.jsx\"\nsecret");
let e = run_file(&mut s, &dir.join("app/main.jsx")).error.expect("blocked");
assert!(e.message.contains("Allow Scripts to Write Files"), "{e:?}");
// Code run without a file can't include relative to anything but the project's folder.
let e = run_code(&mut s, "#include \"outside.jsx\"", "console").error.expect("no folder");
assert!(e.message.contains("outside.jsx"), "{e:?}");
s.prefs.scripting.allow_scripts_write_files = true;
let o = run_file(&mut s, &dir.join("app/main.jsx"));
assert_eq!(o.result, json!(7), "{:?}", o.error);
let _ = std::fs::remove_dir_all(&dir);
}