use std::fs;
use std::path::PathBuf;
use bao_engine::context::JsContext;
use bao_engine::module_loader::ModuleLoader;
use bao_engine::value::JsValue;
fn eval_string(ctx: &mut JsContext, source: &str) -> String {
match ctx.eval(source, "<test>") {
Ok(JsValue::String(s)) => s,
Ok(JsValue::Number(n)) => format_number(n),
Ok(JsValue::Bool(b)) => (if b { "true" } else { "false" }).to_string(),
Ok(_) => String::new(),
Err(e) => format!("<error: {}>", e.message),
}
}
fn eval_number(ctx: &mut JsContext, source: &str) -> f64 {
match ctx.eval(source, "<test>") {
Ok(JsValue::Number(n)) => n,
_ => f64::NAN,
}
}
fn eval_bool(ctx: &mut JsContext, source: &str) -> bool {
match ctx.eval(source, "<test>") {
Ok(JsValue::Bool(b)) => b,
Ok(JsValue::Number(0.0)) => false,
Ok(JsValue::Number(_)) => true,
_ => false,
}
}
fn format_number(n: f64) -> String {
if n == (n as i64) as f64 && n.abs() < 2e15 {
format!("{}", n as i64)
} else {
format!("{}", n)
}
}
struct TempDir(PathBuf);
impl TempDir {
fn new(prefix: &str) -> Self {
let dir = std::env::temp_dir().join(format!("bao_test_{}", prefix));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).expect("create temp dir");
TempDir(dir)
}
fn create_file(&self, rel: &str, content: &str) -> PathBuf {
let p = self.0.join(rel);
if let Some(parent) = p.parent() {
fs::create_dir_all(parent).unwrap_or(());
}
fs::write(&p, content).expect("write test file");
p
}
#[allow(dead_code)]
fn absolute_path(&self, rel: &str) -> PathBuf {
self.0.join(rel)
}
}
impl Drop for TempDir {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.0);
}
}
unsafe fn install_test_globals(
cx: &mut mozjs::context::JSContext,
global: mozjs::rust::Handle<*mut mozjs::jsapi::JSObject>,
) {
use bun_sm::define_host_fn;
bao_engine::host_fn::install_console(cx, global);
define_host_fn!(cx, global, c"identityInt", 1, |_cx, args| {
args.return_int(args.get_int(0));
true
});
define_host_fn!(cx, global, c"identityF64", 1, |_cx, args| {
args.return_f64(args.get_f64(0));
true
});
define_host_fn!(cx, global, c"identityBool", 1, |_cx, args| {
args.return_bool(args.get_bool(0));
true
});
define_host_fn!(cx, global, c"identityString", 1, |_cx, args| {
let s = args.get_string(0);
args.return_string(&s);
true
});
define_host_fn!(cx, global, c"identityOptionalString", 1, |_cx, args| {
let s = args.get_optional_string(0);
match s {
Some(v) => args.return_string(&v),
None => args.return_undefined(),
}
true
});
define_host_fn!(cx, global, c"returnArgc", 4, |_cx, args| {
args.return_int(args.argc() as i32);
true
});
define_host_fn!(cx, global, c"checkHas", 2, |_cx, args| {
args.return_bool(args.has(0));
true
});
define_host_fn!(cx, global, c"sumAll", 5, |_cx, args| {
let mut sum = 0i32;
for i in 0..args.argc() {
sum += args.get_int(i);
}
args.return_int(sum);
true
});
define_host_fn!(cx, global, c"stringToInt", 1, |_cx, args| {
args.return_int(args.get_int(0));
true
});
define_host_fn!(cx, global, c"numberToBool", 1, |_cx, args| {
args.return_bool(args.get_bool(0));
true
});
define_host_fn!(cx, global, c"identityValue", 1, |_cx, args| {
let v = args.get_value(0);
args.return_value(v);
true
});
define_host_fn!(cx, global, c"throwHostError", 0, |_cx, args| {
args.throw("test error from host function")
});
define_host_fn!(cx, global, c"returnUndefined", 1, |_cx, args| {
let _ = args.get_int(0);
args.return_undefined();
true
});
define_host_fn!(cx, global, c"doubleInt", 1, |_cx, args| {
args.return_int(args.get_int(0) * 2);
true
});
}
#[test]
fn test_module_loader_host_fn_all() {
let _temp = TempDir::new("modload");
let mut ctx = JsContext::for_test().expect("Failed to create JsContext");
ctx.set_global_setup(install_test_globals);
{
let mut cx = ctx.cx();
let result = ModuleLoader::eval_module(
&mut cx,
"export const x = 42;",
"test_basic.mjs",
None,
None,
);
assert!(
result.is_ok(),
"ESM 'export const' should evaluate: {:?}",
result
);
}
{
let mut cx = ctx.cx();
let result = ModuleLoader::eval_module(
&mut cx,
r#"export const msg = "hello module";"#,
"test_str.mjs",
None,
None,
);
assert!(
result.is_ok(),
"ESM string export should evaluate: {:?}",
result
);
}
{
let mut cx = ctx.cx();
let result = ModuleLoader::eval_module(
&mut cx,
"export function add(a, b) { return a + b; }",
"test_fn.mjs",
None,
None,
);
assert!(
result.is_ok(),
"ESM function export should evaluate: {:?}",
result
);
}
{
let mut cx = ctx.cx();
let result = ModuleLoader::eval_module(
&mut cx,
"export default 42;",
"test_default.mjs",
None,
None,
);
assert!(
result.is_ok(),
"ESM default export should evaluate: {:?}",
result
);
}
{
let mut cx = ctx.cx();
let result = ModuleLoader::eval_module(
&mut cx,
"const a = 1; const b = 2; export { a, b };",
"test_named.mjs",
None,
None,
);
assert!(
result.is_ok(),
"ESM named export should evaluate: {:?}",
result
);
}
{
let mut cx = ctx.cx();
let result = ModuleLoader::eval_module(
&mut cx,
"export { default } from 'data:text/javascript,export default 42';",
"test_reexport.mjs",
None,
None,
);
let _ = result;
}
{
let mut cx = ctx.cx();
let result = ModuleLoader::eval_module(
&mut cx,
"export const = broken syntax",
"test_bad.mjs",
None,
None,
);
assert!(result.is_err(), "Bad ESM syntax should error");
}
{
let mut cx = ctx.cx();
let result = ModuleLoader::eval_module(&mut cx, "", "test_empty.mjs", None, None);
assert!(result.is_ok(), "Empty module should evaluate: {:?}", result);
}
{
let mut cx = ctx.cx();
let result = ModuleLoader::eval_module(
&mut cx,
"export const meta = import.meta;",
"test_meta.mjs",
None,
None,
);
assert!(
result.is_ok(),
"Module with import.meta should evaluate: {:?}",
result
);
}
{
let mut cx = ctx.cx();
let result = ModuleLoader::eval_module(
&mut cx,
"export const obj = { a: 1, b: 2 }; export function total() { return obj.a + obj.b; }",
"test_stdlib.mjs",
None,
None,
);
assert!(
result.is_ok(),
"ESM with object/function should evaluate: {:?}",
result
);
}
{
let _dir = TempDir::new("resolve_js");
_dir.create_file("dep.js", "export const val = 10;");
let main_path = _dir.create_file(
"main.mjs",
"import { val } from './dep.js';\nexport { val };",
);
let content = fs::read_to_string(&main_path).unwrap();
let mut cx = ctx.cx();
let result =
ModuleLoader::eval_module(&mut cx, &content, main_path.to_str().unwrap(), None, None);
assert!(
result.is_ok(),
"ESM import .js should resolve: {:?}",
result
);
}
{
let _dir = TempDir::new("resolve_mjs");
_dir.create_file("dep.mjs", "export const val = 20;");
let main_path = _dir.create_file(
"main.mjs",
"import { val } from './dep.mjs';\nexport { val };",
);
let content = fs::read_to_string(&main_path).unwrap();
let mut cx = ctx.cx();
let result =
ModuleLoader::eval_module(&mut cx, &content, main_path.to_str().unwrap(), None, None);
assert!(
result.is_ok(),
"ESM import .mjs should resolve: {:?}",
result
);
}
{
let _dir = TempDir::new("resolve_extless");
_dir.create_file("dep.js", "export const val = 30;");
let main_path =
_dir.create_file("main.mjs", "import { val } from './dep';\nexport { val };");
let content = fs::read_to_string(&main_path).unwrap();
let mut cx = ctx.cx();
let result =
ModuleLoader::eval_module(&mut cx, &content, main_path.to_str().unwrap(), None, None);
assert!(
result.is_ok(),
"ESM extensionless import → .js should resolve: {:?}",
result
);
}
{
let _dir = TempDir::new("resolve_ts");
_dir.create_file("dep.ts", "export const val = 40;");
let main_path =
_dir.create_file("main.mjs", "import { val } from './dep';\nexport { val };");
let content = fs::read_to_string(&main_path).unwrap();
let mut cx = ctx.cx();
let result =
ModuleLoader::eval_module(&mut cx, &content, main_path.to_str().unwrap(), None, None);
assert!(
result.is_ok(),
"ESM extensionless import → .ts should resolve: {:?}",
result
);
}
{
let _dir = TempDir::new("resolve_index");
_dir.create_file("lib/index.js", "export const val = 50;");
let main_path =
_dir.create_file("main.mjs", "import { val } from './lib';\nexport { val };");
let content = fs::read_to_string(&main_path).unwrap();
let mut cx = ctx.cx();
let result =
ModuleLoader::eval_module(&mut cx, &content, main_path.to_str().unwrap(), None, None);
assert!(
result.is_ok(),
"ESM index.js resolution should work: {:?}",
result
);
}
{
let _dir = TempDir::new("resolve_nm");
_dir.create_file("node_modules/helper/index.js", "export const val = 60;");
let main_path =
_dir.create_file("main.mjs", "import { val } from 'helper';\nexport { val };");
let content = fs::read_to_string(&main_path).unwrap();
let mut cx = ctx.cx();
let result =
ModuleLoader::eval_module(&mut cx, &content, main_path.to_str().unwrap(), None, None);
assert!(
result.is_ok(),
"ESM node_modules resolution should work: {:?}",
result
);
}
{
let _dir = TempDir::new("cache_test");
_dir.create_file("dep.mjs", "export const val = 100;");
let main_path = _dir.create_file(
"main.mjs",
"import { val } from './dep.mjs';\nexport { val };",
);
let content = fs::read_to_string(&main_path).unwrap();
let path_str = main_path.to_str().unwrap().to_owned();
let result1 = {
let mut cx = ctx.cx();
ModuleLoader::eval_module(&mut cx, &content, &path_str, None, None)
};
assert!(
result1.is_ok(),
"First module eval should succeed: {:?}",
result1
);
let result2 = {
let mut cx = ctx.cx();
ModuleLoader::eval_module(&mut cx, &content, &path_str, None, None)
};
assert!(
result2.is_ok(),
"Second module eval (cached) should succeed: {:?}",
result2
);
}
{
let _dir = TempDir::new("cache_shared");
_dir.create_file("shared.mjs", "export const v = 200;");
let a_path = _dir.create_file(
"a.mjs",
"import { v } from './shared.mjs';\nexport { v as a_val };",
);
let b_path = _dir.create_file(
"b.mjs",
"import { v } from './shared.mjs';\nexport { v as b_val };",
);
let content_a = fs::read_to_string(&a_path).unwrap();
let content_b = fs::read_to_string(&b_path).unwrap();
let result_a = {
let mut cx = ctx.cx();
ModuleLoader::eval_module(&mut cx, &content_a, a_path.to_str().unwrap(), None, None)
};
assert!(
result_a.is_ok(),
"Module A should load shared dep: {:?}",
result_a
);
let result_b = {
let mut cx = ctx.cx();
ModuleLoader::eval_module(&mut cx, &content_b, b_path.to_str().unwrap(), None, None)
};
assert!(
result_b.is_ok(),
"Module B should load cached shared dep: {:?}",
result_b
);
}
{
let _dir = TempDir::new("resolve_dot");
_dir.create_file("dep.js", "export const val = 70;");
let main_path =
_dir.create_file("main.mjs", "import { val } from './dep';\nexport { val };");
let content = fs::read_to_string(&main_path).unwrap();
let mut cx = ctx.cx();
let result =
ModuleLoader::eval_module(&mut cx, &content, main_path.to_str().unwrap(), None, None);
assert!(result.is_ok(), "ESM ./ import should resolve: {:?}", result);
}
{
let _dir = TempDir::new("resolve_parent");
_dir.create_file("sub/dep.mjs", "export const val = 80;");
_dir.create_file(
"sub/main.mjs",
"import { val } from './dep.mjs';\nexport { val };",
);
let main_path = _dir.absolute_path("sub/main.mjs");
let content = fs::read_to_string(&main_path).unwrap();
let mut cx = ctx.cx();
let result =
ModuleLoader::eval_module(&mut cx, &content, main_path.to_str().unwrap(), None, None);
assert!(
result.is_ok(),
"ESM sub-directory import should resolve: {:?}",
result
);
}
{
let _dir = TempDir::new("resolve_nm_nested");
_dir.create_file("node_modules/pkg/src/lib.js", "export const val = 300;");
_dir.create_file("node_modules/pkg/package.json", r#"{"main": "src/lib.js"}"#);
let main_path = _dir.create_file("main.mjs", "import { val } from 'pkg';\nexport { val };");
let content = fs::read_to_string(&main_path).unwrap();
let mut cx = ctx.cx();
let result =
ModuleLoader::eval_module(&mut cx, &content, main_path.to_str().unwrap(), None, None);
assert!(
result.is_ok(),
"ESM package.json main resolution should work: {:?}",
result
);
}
{
let _dir = TempDir::new("resolve_fail");
let main_path = _dir.create_file(
"main.mjs",
"import { val } from 'nonexistent-pkg';\nexport { val };",
);
let content = fs::read_to_string(&main_path).unwrap();
let mut cx = ctx.cx();
let result =
ModuleLoader::eval_module(&mut cx, &content, main_path.to_str().unwrap(), None, None);
assert!(result.is_err(), "Unresolvable bare import should error");
}
assert!(
(eval_number(&mut ctx, "identityInt(42)") - 42.0).abs() < f64::EPSILON,
"identityInt(42) = {}",
eval_number(&mut ctx, "identityInt(42)")
);
assert_eq!(eval_number(&mut ctx, "identityInt(0)"), 0.0);
assert_eq!(eval_number(&mut ctx, "identityInt(-1)"), -1.0);
assert_eq!(
eval_number(&mut ctx, "identityInt(2147483647)"),
2147483647.0
);
assert_eq!(
eval_number(&mut ctx, "identityInt(-2147483648)"),
-2147483648.0
);
assert!((eval_number(&mut ctx, "identityF64(3.14)") - 3.14).abs() < 1e-10);
assert_eq!(eval_number(&mut ctx, "identityF64(0.0)"), 0.0);
assert_eq!(eval_number(&mut ctx, "identityF64(-2.5)"), -2.5);
assert!(eval_number(&mut ctx, "identityF64(1e10)") > 1e9);
assert!(eval_bool(&mut ctx, "identityBool(true)"));
assert!(!eval_bool(&mut ctx, "identityBool(false)"));
assert!(!eval_bool(&mut ctx, "identityBool(1)"));
assert_eq!(eval_string(&mut ctx, r#"identityString("hello")"#), "hello");
assert_eq!(eval_string(&mut ctx, r#"identityString("")"#), "");
assert_eq!(eval_string(&mut ctx, r#"identityString("a b c")"#), "a b c");
assert_eq!(
eval_string(&mut ctx, r#"identityOptionalString("present")"#),
"present"
);
{
match ctx.eval(r#"identityOptionalString()"#, "<test>") {
Ok(JsValue::Undefined) => {} Ok(JsValue::String(s)) => {
assert!(s.is_empty() || s == "undefined");
}
other => {
let _ = other;
}
}
}
assert_eq!(eval_number(&mut ctx, "identityValue(42)"), 42.0);
assert_eq!(eval_number(&mut ctx, "identityValue(3.14)"), 3.14);
assert!(eval_bool(&mut ctx, "identityValue(true)"));
assert!(!eval_bool(&mut ctx, "identityValue(false)"));
assert_eq!(eval_string(&mut ctx, r#"identityValue("hello")"#), "hello");
assert_eq!(eval_number(&mut ctx, r#"stringToInt("not a number")"#), 0.0);
assert!(!eval_bool(&mut ctx, "numberToBool(42)"));
assert!(!eval_bool(&mut ctx, "numberToBool(null)"));
assert_eq!(eval_number(&mut ctx, "stringToInt(undefined)"), 0.0);
assert_eq!(eval_number(&mut ctx, "stringToInt()"), 0.0);
assert!(!eval_bool(&mut ctx, "numberToBool()"));
assert_eq!(eval_number(&mut ctx, "stringToInt({a:1})"), 0.0);
assert!(!eval_bool(&mut ctx, "numberToBool([1,2,3])"));
assert_eq!(eval_number(&mut ctx, "returnArgc()"), 0.0);
assert_eq!(eval_number(&mut ctx, "returnArgc(1)"), 1.0);
assert_eq!(eval_number(&mut ctx, "returnArgc(1, 2)"), 2.0);
assert_eq!(eval_number(&mut ctx, "returnArgc(1, 2, 3)"), 3.0);
assert_eq!(eval_number(&mut ctx, "returnArgc(1, 2, 3, 4)"), 4.0);
assert!(eval_bool(&mut ctx, "checkHas(42)"));
assert!(!eval_bool(&mut ctx, "checkHas()"));
{
match ctx.eval("checkHas()", "<test>") {
Ok(JsValue::Bool(b)) => assert!(!b),
_ => {} }
}
assert_eq!(eval_number(&mut ctx, "sumAll(1, 2, 3, 4, 5)"), 15.0);
assert_eq!(eval_number(&mut ctx, "sumAll()"), 0.0);
assert_eq!(eval_number(&mut ctx, "sumAll(10)"), 10.0);
assert!(
eval_bool(&mut ctx, "typeof identityInt === 'function'"),
"identityInt should be function"
);
assert!(eval_bool(&mut ctx, "typeof identityF64 === 'function'"));
assert!(eval_bool(&mut ctx, "typeof identityBool === 'function'"));
assert!(eval_bool(&mut ctx, "typeof identityString === 'function'"));
assert!(eval_bool(&mut ctx, "typeof returnArgc === 'function'"));
assert!(eval_bool(&mut ctx, "typeof sumAll === 'function'"));
assert!(eval_bool(&mut ctx, "typeof throwHostError === 'function'"));
assert!(eval_bool(&mut ctx, "typeof doubleInt === 'function'"));
assert!(eval_bool(&mut ctx, "typeof returnUndefined === 'function'"));
{
let result = ctx.eval("throwHostError()", "<test>");
assert!(
result.is_err(),
"throwHostError should return Err, got: {:?}",
result
);
if let Err(e) = result {
assert!(
e.message.contains("test error"),
"Error message should contain 'test error', got: '{}'",
e.message
);
}
}
assert_eq!(eval_number(&mut ctx, "doubleInt(21)"), 42.0);
match ctx.eval("returnUndefined(42)", "<test>") {
Ok(JsValue::Undefined) => {} other => {
let _ = other;
}
}
assert_eq!(eval_number(&mut ctx, "identityInt(doubleInt(10))"), 20.0);
assert_eq!(
eval_string(&mut ctx, r#""result: " + identityString("ok")"#),
"result: ok"
);
assert_eq!(
eval_number(
&mut ctx,
"identityInt(10) + identityInt(20) + identityInt(30)"
),
60.0
);
assert_eq!(eval_number(&mut ctx, "identityInt('5')"), 0.0); assert_eq!(eval_number(&mut ctx, "identityInt(true)"), 0.0);
assert_eq!(eval_number(&mut ctx, "sumAll(1, 2, 3)"), 6.0);
assert_eq!(eval_number(&mut ctx, "sumAll(3, 2, 1)"), 6.0);
bao_engine::context::JsContext::shutdown_thread_sm();
}