use std::time::Duration;
use bao_engine::context::JsContext;
use bao_engine::value::JsValue;
fn eval_string(ctx: &mut JsContext, source: &str) -> String {
match ctx.eval(source, "<wave-b>") {
Ok(JsValue::String(s)) => s,
Ok(JsValue::Number(n)) => format!("{}", n),
Ok(JsValue::Bool(b)) => if b { "true" } else { "false" }.to_string(),
Ok(JsValue::Null) => "null".to_string(),
Ok(JsValue::Undefined) => "undefined".to_string(),
Ok(JsValue::Object(_)) => "[object]".to_string(),
Err(e) => format!("ERROR:{}", e.message),
}
}
fn drive_event_loop(ctx: &mut JsContext, max_iters: usize) {
for _ in 0..max_iters {
let mut cxm = ctx.cx();
bun_runtime::timers::drain_and_check(&mut cxm);
std::thread::sleep(Duration::from_millis(1));
}
}
fn setup_ctx() -> JsContext {
bun_runtime::install_exit_handler();
bun_runtime::bun_api::init_process_start();
let mut ctx = JsContext::for_test().expect("JsContext");
ctx.set_global_setup(bun_runtime::globals::install_all);
ctx
}
#[test]
fn test_punycode_to_ascii_idn() {
let mut ctx = setup_ctx();
assert_eq!(
eval_string(&mut ctx, r#"require('punycode').toASCII('日本.jp')"#),
"xn--wgv71a.jp"
);
assert_eq!(
eval_string(&mut ctx, r#"require('punycode').toASCII('mémé.com')"#),
"xn--mm-bjab.com"
);
assert_eq!(
eval_string(&mut ctx, r#"require('punycode').toASCII('example.com')"#),
"example.com"
);
}
#[test]
fn test_punycode_encode_decode_roundtrip() {
let mut ctx = setup_ctx();
assert_eq!(
eval_string(&mut ctx, r#"require('punycode').encode('日本')"#),
"wgv71a"
);
assert_eq!(
eval_string(&mut ctx, r#"require('punycode').decode('wgv71a')"#),
"日本"
);
assert_eq!(
eval_string(
&mut ctx,
r#"var p=require('punycode'); p.toUnicode(p.toASCII('日本.jp'))"#
),
"日本.jp"
);
assert_eq!(
eval_string(
&mut ctx,
r#"try { require('punycode').decode('!'); 'NO-THROW'; } catch (e) { e.constructor.name; }"#
),
"RangeError"
);
}
#[test]
fn test_buffer_concat_type_error_for_non_views() {
let mut ctx = setup_ctx();
assert_eq!(
eval_string(
&mut ctx,
r#"Buffer.concat([Buffer.from('ab'), new Uint8Array([67,68])]).toString()"#
),
"abCD"
);
assert_eq!(
eval_string(&mut ctx, r#"Buffer.concat([]).length"#),
"0"
);
assert_eq!(
eval_string(&mut ctx, r#"Buffer.concat([Buffer.from('abcdef')], 3).toString()"#),
"abc"
);
for bad in ["'nope'", "42", "{length:2,0:65,1:66}", "null", "undefined"] {
let src = format!(
"try {{ Buffer.concat([{}]); 'NO-THROW'; }} catch (e) {{ e.constructor.name; }}",
bad
);
assert_eq!(
eval_string(&mut ctx, &src),
"TypeError",
"Buffer.concat([{}]) must throw TypeError",
bad
);
}
}
#[test]
fn test_zlib_crc32_unsigned_u32() {
let mut ctx = setup_ctx();
assert_eq!(
eval_string(&mut ctx, r#"require('zlib').crc32(Buffer.from([255,255,255,255]))"#),
"4294967295", );
assert_eq!(
eval_string(&mut ctx, r#"require('zlib').crc32('the quick brown fox')"#),
"2445345482", );
assert_eq!(
eval_string(&mut ctx, r#"require('zlib').crc32('hello world')"#),
"222957957"
);
assert_eq!(
eval_string(
&mut ctx,
r#"var z=require('zlib'); z.crc32(' world', z.crc32('hello'))"#
),
"222957957"
);
}
#[test]
fn test_sqlite_backup_returns_destination() {
let mut ctx = setup_ctx();
let out = eval_string(
&mut ctx,
r#"
globalThis.__r = {};
var { Database } = require('bun:sqlite');
var db = new Database(':memory:');
db.exec('CREATE TABLE t(x); INSERT INTO t VALUES (42);');
var path = require('os').tmpdir() + '/waveb-backup-test.db';
try { require('fs').rmSync(path); } catch (e) {}
var p = db.backup(path);
// NOTE: not `p instanceof Promise` — node_async_hooks replaces the
// global Promise with a JS subclass; the honest observable contract
// is thenable + the real [object Promise] class tag.
__r.thenable = (typeof p.then === 'function') + ':' + Object.prototype.toString.call(p);
p.then(
function(resolved) { __r.ok = (typeof resolved === 'string') + ':' + (resolved === path); },
function(e) { __r.ok = 'REJ:' + (e && e.message); }
);
__r.exists = '' + require('fs').existsSync(path);
// 重复 backup 到同一目标:VACUUM INTO 要求新文件 → rejected promise
// (非同步 throw),reject 理由是含 'already exists' 的 Error(fail-closed)。
var dup;
try {
db.backup(path).then(
function() { __r.dup = 'RESOLVED'; },
function(e) { __r.dup = 'REJ:' + (e instanceof Error) + ':' + (e.message.indexOf('already exists') >= 0); }
);
dup = 'NO-SYNC-THROW';
} catch (e) { dup = 'SYNC-THREW'; }
__r.dupSync = dup;
'queued'
"#,
);
assert_eq!(out, "queued", "backup wiring must eval cleanly");
drive_event_loop(&mut ctx, 10);
assert_eq!(
eval_string(&mut ctx, "globalThis.__r.thenable"),
"true:[object Promise]",
"backup() must return a real Promise (thenable + [object Promise] tag)"
);
assert_eq!(
eval_string(&mut ctx, "globalThis.__r.ok"),
"true:true",
"backup() must resolve with the destination path string"
);
assert_eq!(
eval_string(&mut ctx, "globalThis.__r.exists"),
"true",
"backup must write the snapshot file"
);
assert_eq!(
eval_string(&mut ctx, "globalThis.__r.dupSync"),
"NO-SYNC-THROW",
"duplicate backup must reject, not throw synchronously"
);
assert_eq!(
eval_string(&mut ctx, "globalThis.__r.dup"),
"REJ:true:true",
"duplicate backup must reject with an Error mentioning 'already exists' (fail-closed)"
);
}
#[test]
fn test_ffi_dlopen_symbols_face() {
let mut ctx = setup_ctx();
let out = eval_string(
&mut ctx,
r#"
var { dlopen } = require('bun:ffi');
var lib = dlopen('/usr/lib/x86_64-linux-gnu/libc.so.6',
{ getpid: { args: [], returns: 'i32' } });
(typeof lib.symbols) + '|' + (lib.symbols.getpid === lib.getpid) + '|' +
(lib.symbols.getpid() === lib.getpid() && lib.getpid() > 0);
"#,
);
assert_eq!(out, "object|true|true", "symbols face must expose the same callables");
}
#[test]
fn test_dgram_bind_callback_forms() {
let mut ctx = setup_ctx();
ctx.eval(
r#"
globalThis.__dgram_results = [];
var dgram = require('dgram');
var s1 = dgram.createSocket('udp4');
s1.bind(0, function () {
__dgram_results.push('form2:' + (typeof s1.address().port === 'number'));
s1.close();
});
"#,
"<wave-b>",
)
.expect("setup");
drive_event_loop(&mut ctx, 60);
let got = eval_string(&mut ctx, "JSON.stringify(__dgram_results)");
assert_eq!(got, r#"["form2:true"]"#, "bind(port, cb) callback must fire");
ctx.eval(
r#"
var s2 = dgram.createSocket('udp4');
s2.bind(function () { __dgram_results.push('cbOnly'); s2.close(); });
var s3 = dgram.createSocket('udp4');
s3.bind({ port: 0 }, function () { __dgram_results.push('optsForm:' + (s3.address().port > 0)); s3.close(); });
"#,
"<wave-b>",
)
.expect("setup2");
drive_event_loop(&mut ctx, 60);
let got = eval_string(&mut ctx, "JSON.stringify(__dgram_results)");
assert_eq!(
got,
r#"["form2:true","cbOnly","optsForm:true"]"#,
"all bind forms must fire their callbacks"
);
}
#[test]
fn test_util_textencoder_identity() {
let mut ctx = setup_ctx();
assert_eq!(
eval_string(
&mut ctx,
"var u = require('util'); (u.TextEncoder === globalThis.TextEncoder) + '|' + (u.TextDecoder === globalThis.TextDecoder)"
),
"true|true"
);
assert_eq!(
eval_string(&mut ctx, "new (require('util').TextEncoder)().encode('hi').length"),
"2"
);
}
#[test]
fn test_text_decoder_buffer_source() {
let mut ctx = setup_ctx();
assert_eq!(
eval_string(&mut ctx, "new TextDecoder().decode(new Uint8Array([104,105]).buffer)"),
"hi"
);
assert_eq!(
eval_string(
&mut ctx,
"new TextDecoder().decode(new Uint8Array([65,66,67,68]).subarray(1,3))"
),
"BC"
);
assert_eq!(
eval_string(
&mut ctx,
"new TextDecoder().decode(new DataView(new Uint8Array([120,121]).buffer))"
),
"xy"
);
assert_eq!(
eval_string(&mut ctx, "new TextDecoder().decode(new Uint8Array([0xff,0xfe]))"),
"\u{FFFD}\u{FFFD}"
);
for bad in ["42", "'str'", "null", "{}"] {
let src = format!(
"try {{ new TextDecoder().decode({}); 'NO-THROW'; }} catch (e) {{ e.constructor.name; }}",
bad
);
assert_eq!(
eval_string(&mut ctx, &src),
"TypeError",
"decode({}) must throw TypeError",
bad
);
}
assert_eq!(eval_string(&mut ctx, "JSON.stringify(new TextDecoder().decode())"), "\"\"");
}
#[test]
fn test_ffi_callback_with_args_qsort_e2e() {
let mut ctx = setup_ctx();
let out = eval_string(
&mut ctx,
r#"
var ffi = require('bun:ffi');
var dlopen = ffi.dlopen, callback = ffi.callback, toBuffer = ffi.toBuffer;
var lib = dlopen('/usr/lib/x86_64-linux-gnu/libc.so.6', {
calloc: { args: ['usize', 'usize'], returns: 'ptr' },
qsort: { args: ['ptr', 'usize', 'usize', 'js_function'], returns: 'void' }
});
var region = lib.calloc(8, 4); // 8 zeroed int32 slots (mapped, writable)
var seen = [];
var cb = callback(['ptr', 'ptr'], 'i32', function (a, b) {
var va = toBuffer(a, 4).readInt32LE(0);
var vb = toBuffer(b, 4).readInt32LE(0);
seen.push([typeof a, b - a, va - vb]);
return 0;
});
lib.qsort(region, 2, 4, cb);
JSON.stringify(seen);
"#,
);
assert_eq!(
out, r#"[["number",4,0]]"#,
"C must invoke the JS closure with two real pointer args 4 bytes apart, readable via toBuffer"
);
let out2 = eval_string(
&mut ctx,
r#"
var region2 = lib.calloc(8, 4);
var seen2 = [];
var cb2 = callback(2, 'i32', function (a, b) {
seen2.push(typeof a + ':' + typeof b);
return 0;
});
lib.qsort(region2, 2, 4, cb2);
JSON.stringify([seen2.length, seen2[0]]);
"#,
);
assert_eq!(out2, r#"[1,"number:number"]"#, "argCount callback form must be invoked from C with numeric args");
let out3 = eval_string(
&mut ctx,
r#"
try { lib.qsort(lib.calloc(8, 4), 2, 4, function () { return 0; }); 'NO-THROW'; }
catch (e) { e.constructor.name + ':' + (e.message.indexOf('callback') >= 0); }
"#,
);
assert_eq!(
out3, "Error:true",
"js_function slot must reject plain JS functions (only callback() wrappers)"
);
}