mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-16 08:03:02 +00:00
* chore: upgrade the native runtime to the current deno crates and v8 150 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: keep data and blob fetches working under no_network Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: pin the simdutf v8 archive and keep telemetry on http exporters Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: force the http otlp exporter for native telemetry and pin v8 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
846 lines
36 KiB
Rust
846 lines
36 KiB
Rust
//! Opt-in smoke tests for the nativets V8 runtime.
|
|
//!
|
|
//! Exercise the deno_core / deno_ast / swc surface (TypeScript transpile,
|
|
//! fetch, timers, URL, structuredClone, error propagation, concurrent
|
|
//! isolates, large payload roundtrip) that the standard worker-level
|
|
//! nativets tests in `backend/tests/worker.rs` don't reach — those tests
|
|
//! validate value passing through the job queue, but not the JS API
|
|
//! surface a deno_core bump would actually move.
|
|
//!
|
|
//! These tests are `#[ignore]`'d so the regular `cargo test` flow doesn't
|
|
//! pay their cost (each spawns a V8 isolate; some hit the network). Run
|
|
//! when changing the `deno_core` / `deno_ast` / `deno_runtime` / `swc_*`
|
|
//! pins in `backend/Cargo.toml`:
|
|
//!
|
|
//! cargo test -p windmill-runtime-nativets smoke -- --ignored
|
|
//!
|
|
//! Tests prefixed `smoke_net_` hit the public internet (httpbin.org,
|
|
//! example.com) and will fail if the runner has no egress. Skip them
|
|
//! locally with `cargo test -p windmill-runtime-nativets smoke -- --ignored --skip smoke_net_`.
|
|
|
|
use crate::{transpile_ts, NativeAnnotation, PrewarmedIsolate, PrewarmedResult};
|
|
|
|
/// Compile a TS snippet, run it through a fresh isolate with the given
|
|
/// positional args, and return the isolate's result + captured logs.
|
|
async fn run_ts(ts: &str, arg_names: &[&str], args: serde_json::Value) -> PrewarmedResult {
|
|
let js = transpile_ts(ts.to_string()).expect("transpile_ts failed");
|
|
let ann = NativeAnnotation::default();
|
|
let arg_names: Vec<String> = arg_names.iter().map(|s| s.to_string()).collect();
|
|
let mut iso = PrewarmedIsolate::spawn(String::new(), js, ann, arg_names, None);
|
|
iso.wait_ready().await.expect("isolate failed to pre-warm");
|
|
iso.start_execution(args.to_string())
|
|
.wait()
|
|
.await
|
|
.expect("isolate execution panicked")
|
|
}
|
|
|
|
fn unwrap_value(r: &PrewarmedResult) -> serde_json::Value {
|
|
let raw = r.result.as_ref().expect("script returned an error");
|
|
serde_json::from_str(raw.get()).expect("result not valid JSON")
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Local (no network) — these still need V8 / deno_core ops to be wired.
|
|
// -----------------------------------------------------------------------------
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore = "deno_core upgrade smoke; run with --ignored"]
|
|
async fn smoke_basic_value_passing() {
|
|
let ts = r#"
|
|
export async function main(x: number): Promise<number> {
|
|
return x + 1;
|
|
}
|
|
"#;
|
|
let r = run_ts(ts, &["x"], serde_json::json!({"x": 41})).await;
|
|
assert_eq!(unwrap_value(&r), serde_json::json!(42));
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore = "deno_core upgrade smoke; run with --ignored"]
|
|
async fn smoke_missing_optional_arg_uses_default() {
|
|
// A missing optional arg must arrive as `undefined`, not `null`, so the
|
|
// parameter default applies. With `null`, slice(0, null) -> [] -> length 0.
|
|
let ts = r#"
|
|
export async function main(limit = 50): Promise<number> {
|
|
return [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].slice(0, limit).length;
|
|
}
|
|
"#;
|
|
let r = run_ts(ts, &["limit"], serde_json::json!({})).await;
|
|
assert_eq!(unwrap_value(&r), serde_json::json!(10));
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore = "deno_core upgrade smoke; run with --ignored"]
|
|
async fn smoke_explicit_null_arg_is_preserved() {
|
|
// An explicitly-provided JSON null must stay null (distinct from a missing
|
|
// arg), so the default does NOT apply.
|
|
let ts = r#"
|
|
export async function main(x: number | null = 7): Promise<string> {
|
|
return x === null ? "null" : String(x);
|
|
}
|
|
"#;
|
|
let r = run_ts(ts, &["x"], serde_json::json!({ "x": null })).await;
|
|
assert_eq!(unwrap_value(&r), serde_json::json!("null"));
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore = "deno_core upgrade smoke; run with --ignored"]
|
|
async fn smoke_transpile_enum_and_union() {
|
|
// Enums + discriminated union + as-cast exercise the swc_ecma_ast +
|
|
// swc_ecma_parser TS-syntax paths the bare value tests don't.
|
|
let ts = r#"
|
|
enum Direction { Up = "U", Down = "D" }
|
|
type Msg = { kind: "move"; dir: Direction } | { kind: "stop" };
|
|
export async function main(): Promise<string> {
|
|
const msgs: Msg[] = [
|
|
{ kind: "move", dir: Direction.Up },
|
|
{ kind: "stop" },
|
|
{ kind: "move", dir: Direction.Down },
|
|
];
|
|
return msgs.map(m => m.kind === "move" ? m.dir : "_").join(",");
|
|
}
|
|
"#;
|
|
let r = run_ts(ts, &[], serde_json::json!({})).await;
|
|
assert_eq!(unwrap_value(&r), serde_json::json!("U,_,D"));
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore = "deno_core upgrade smoke; run with --ignored"]
|
|
async fn smoke_set_timeout_and_promise_all() {
|
|
// setTimeout lives in deno_web; Promise.all hits the V8 microtask
|
|
// queue. A bump that breaks timer-op registration or microtask drain
|
|
// would surface here (script would hang or return wrong order).
|
|
let ts = r#"
|
|
export async function main(): Promise<number[]> {
|
|
const delays = [40, 10, 20, 30];
|
|
return await Promise.all(delays.map(d =>
|
|
new Promise<number>(resolve => setTimeout(() => resolve(d), d))
|
|
));
|
|
}
|
|
"#;
|
|
let r = run_ts(ts, &[], serde_json::json!({})).await;
|
|
// Promise.all preserves input order regardless of resolution order.
|
|
assert_eq!(unwrap_value(&r), serde_json::json!([40, 10, 20, 30]));
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore = "deno_core upgrade smoke; run with --ignored"]
|
|
async fn smoke_url_and_searchparams() {
|
|
// deno_url surface: URL ctor, URLSearchParams parsing + iteration.
|
|
let ts = r#"
|
|
export async function main(): Promise<{ host: string; pairs: [string, string][] }> {
|
|
const u = new URL("https://example.com:8443/path?b=2&a=1&a=3");
|
|
const pairs: [string, string][] = [];
|
|
for (const [k, v] of u.searchParams) pairs.push([k, v]);
|
|
return { host: u.host, pairs };
|
|
}
|
|
"#;
|
|
let r = run_ts(ts, &[], serde_json::json!({})).await;
|
|
assert_eq!(
|
|
unwrap_value(&r),
|
|
serde_json::json!({
|
|
"host": "example.com:8443",
|
|
"pairs": [["b", "2"], ["a", "1"], ["a", "3"]],
|
|
}),
|
|
);
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore = "deno_core upgrade smoke; run with --ignored"]
|
|
async fn smoke_web_blob_btoa_atob() {
|
|
// deno_web surface: Blob, atob/btoa. (`structuredClone` and the rest of
|
|
// the wired web globals are covered by `smoke_web_globals_are_wired`.)
|
|
let ts = r#"
|
|
export async function main(): Promise<{ b64: string; round_trip: string; size: number }> {
|
|
const blob = new Blob(["hello"], { type: "text/plain" });
|
|
const b64 = btoa("hello");
|
|
const round_trip = atob(b64);
|
|
return { b64, round_trip, size: blob.size };
|
|
}
|
|
"#;
|
|
let r = run_ts(ts, &[], serde_json::json!({})).await;
|
|
assert_eq!(
|
|
unwrap_value(&r),
|
|
serde_json::json!({
|
|
"b64": "aGVsbG8=",
|
|
"round_trip": "hello",
|
|
"size": 5,
|
|
}),
|
|
);
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore = "deno_core upgrade smoke; run with --ignored"]
|
|
async fn smoke_large_payload_roundtrip() {
|
|
// ~512 KB string in and out — exercises arg encoding + result
|
|
// serialization through the deno_core <-> host op boundary at sizes
|
|
// an op-table change could break.
|
|
let big_in: String = "a".repeat(512 * 1024);
|
|
let ts = r#"
|
|
export async function main(s: string): Promise<{ in_len: number; out: string }> {
|
|
if (typeof s !== "string") throw new Error(`expected string, got ${typeof s}`);
|
|
return { in_len: s.length, out: "b".repeat(512 * 1024) };
|
|
}
|
|
"#;
|
|
let r = run_ts(ts, &["s"], serde_json::json!({"s": big_in})).await;
|
|
let v = unwrap_value(&r);
|
|
assert_eq!(v.get("in_len").and_then(|x| x.as_u64()), Some(512 * 1024));
|
|
let out_len = v
|
|
.get("out")
|
|
.and_then(|x| x.as_str())
|
|
.map(|s| s.len())
|
|
.unwrap_or(0);
|
|
assert_eq!(out_len, 512 * 1024);
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore = "deno_core upgrade smoke; run with --ignored"]
|
|
async fn smoke_error_propagation_with_message() {
|
|
// Throwing a typed Error must surface as PrewarmedResult::Err with
|
|
// the original message. A deno_core bump that changes the host-side
|
|
// error wrapping would lose this contract.
|
|
let ts = r#"
|
|
export async function main(): Promise<void> {
|
|
throw new Error("nativets_smoke_marker_xyz");
|
|
}
|
|
"#;
|
|
let r = run_ts(ts, &[], serde_json::json!({})).await;
|
|
let err = r.result.expect_err("expected script to fail");
|
|
assert!(
|
|
err.contains("nativets_smoke_marker_xyz"),
|
|
"thrown error message did not reach result: {err}",
|
|
);
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
|
#[ignore = "deno_core upgrade smoke; run with --ignored"]
|
|
async fn smoke_concurrent_isolates() {
|
|
// Spawn N isolates in parallel from the same tokio runtime. Each
|
|
// PrewarmedIsolate uses spawn_blocking + a fresh V8 isolate.
|
|
// Catches isolate-setup races (V8_ISOLATE_CREATE_LOCK ordering) and
|
|
// any per-isolate state that a deno_core bump could break under
|
|
// concurrency.
|
|
let ts = r#"
|
|
export async function main(i: number): Promise<number> {
|
|
return i * 10;
|
|
}
|
|
"#;
|
|
let js = transpile_ts(ts.to_string()).expect("transpile_ts failed");
|
|
|
|
const N: i64 = 8;
|
|
let mut handles = Vec::with_capacity(N as usize);
|
|
for i in 0..N {
|
|
let js = js.clone();
|
|
let h = tokio::spawn(async move {
|
|
let ann = NativeAnnotation::default();
|
|
let mut iso =
|
|
PrewarmedIsolate::spawn(String::new(), js, ann, vec!["i".to_string()], None);
|
|
iso.wait_ready().await.expect("pre-warm failed");
|
|
let res = iso
|
|
.start_execution(serde_json::json!({"i": i}).to_string())
|
|
.wait()
|
|
.await
|
|
.expect("isolate panicked");
|
|
res.result.expect("script errored")
|
|
});
|
|
handles.push(h);
|
|
}
|
|
|
|
let mut got: Vec<i64> = Vec::with_capacity(N as usize);
|
|
for h in handles {
|
|
let raw = h.await.expect("join failed");
|
|
let v: serde_json::Value = serde_json::from_str(raw.get()).expect("not JSON");
|
|
got.push(v.as_i64().unwrap_or(-1));
|
|
}
|
|
got.sort();
|
|
let expected: Vec<i64> = (0..N).map(|i| i * 10).collect();
|
|
assert_eq!(got, expected);
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore = "deno_core upgrade smoke; run with --ignored"]
|
|
async fn smoke_text_encoder_decoder() {
|
|
// TextEncoder/TextDecoder are wired from deno_web's 08_text_encoding.
|
|
// The "€" (U+20AC) is a 3-byte UTF-8 sequence, so this asserts the
|
|
// multi-byte encode → decode round-trip (not just ASCII) survives the
|
|
// deno_core op boundary.
|
|
let ts = r#"
|
|
export async function main(): Promise<{ bytes: number[]; round_trip: string }> {
|
|
const enc = new TextEncoder();
|
|
const dec = new TextDecoder();
|
|
const bytes = enc.encode("a€b");
|
|
return { bytes: Array.from(bytes), round_trip: dec.decode(bytes) };
|
|
}
|
|
"#;
|
|
let r = run_ts(ts, &[], serde_json::json!({})).await;
|
|
assert_eq!(
|
|
unwrap_value(&r),
|
|
serde_json::json!({
|
|
// "a" = 0x61, "€" = 0xE2 0x82 0xAC, "b" = 0x62
|
|
"bytes": [0x61, 0xE2, 0x82, 0xAC, 0x62],
|
|
"round_trip": "a€b",
|
|
}),
|
|
);
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore = "deno_core upgrade smoke; run with --ignored"]
|
|
async fn smoke_web_globals_are_wired() {
|
|
// Guard against a wrong export name silently leaving a global unwired:
|
|
// assert each web-platform global we assign in runtime.js is actually
|
|
// defined. If a deno_web/deno_url export is renamed on a future bump,
|
|
// the corresponding `globalThis.X = mod.X` becomes `undefined` and this
|
|
// test flips it to false.
|
|
let ts = r#"
|
|
export async function main(): Promise<Record<string, boolean>> {
|
|
const names = [
|
|
"DOMException",
|
|
"TextEncoder", "TextDecoder", "TextEncoderStream", "TextDecoderStream",
|
|
"File",
|
|
"Event", "EventTarget", "CustomEvent",
|
|
"MessageEvent", "CloseEvent", "ErrorEvent", "reportError",
|
|
"ReadableStream", "WritableStream", "TransformStream",
|
|
"ReadableStreamDefaultReader", "ReadableStreamBYOBReader",
|
|
"ReadableStreamDefaultController", "ReadableByteStreamController",
|
|
"ReadableStreamBYOBRequest", "WritableStreamDefaultWriter",
|
|
"WritableStreamDefaultController", "TransformStreamDefaultController",
|
|
"ByteLengthQueuingStrategy", "CountQueuingStrategy",
|
|
"Performance", "PerformanceEntry", "PerformanceMark", "PerformanceMeasure",
|
|
"URLPattern",
|
|
"CompressionStream", "DecompressionStream",
|
|
"MessageChannel", "MessagePort",
|
|
"structuredClone", "performance",
|
|
];
|
|
const out: Record<string, boolean> = {};
|
|
for (const n of names) out[n] = typeof (globalThis as any)[n] !== "undefined";
|
|
return out;
|
|
}
|
|
"#;
|
|
let r = run_ts(ts, &[], serde_json::json!({})).await;
|
|
let v = unwrap_value(&r);
|
|
let obj = v.as_object().expect("expected an object result");
|
|
let unwired: Vec<&String> = obj
|
|
.iter()
|
|
.filter(|(_, defined)| defined.as_bool() != Some(true))
|
|
.map(|(name, _)| name)
|
|
.collect();
|
|
assert!(
|
|
unwired.is_empty(),
|
|
"these globals were not wired: {unwired:?}"
|
|
);
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore = "deno_core upgrade smoke; run with --ignored"]
|
|
async fn smoke_structured_clone_and_performance() {
|
|
// structuredClone is the spec function (from 13_message_port.js): it
|
|
// deep-clones and accepts an options bag. performance.now() must be finite,
|
|
// and performance.timeOrigin must be seeded per isolate (via the Rust-side
|
|
// __wmInitPerIsolate call) so that `timeOrigin + now()` tracks wall-clock
|
|
// time rather than being NaN.
|
|
let ts = r#"
|
|
export async function main(): Promise<{ deep_equal: boolean; source_unchanged: boolean; now_ok: boolean; origin_ok: boolean }> {
|
|
const src = { a: 1, nested: { b: [2, 3] } };
|
|
const copy = structuredClone(src);
|
|
copy.nested.b.push(4);
|
|
const deep_equal = JSON.stringify(copy) === JSON.stringify({ a: 1, nested: { b: [2, 3, 4] } });
|
|
// Mutating the clone must not touch the source (proves a real deep clone).
|
|
const source_unchanged = src.nested.b.length === 2;
|
|
const now_ok = Number.isFinite(performance.now());
|
|
// timeOrigin must be a finite epoch-ms value, and timeOrigin + now() must
|
|
// land within a few seconds of Date.now() (guards the per-isolate seeding).
|
|
const origin = performance.timeOrigin;
|
|
const origin_ok = Number.isFinite(origin) && Math.abs(origin + performance.now() - Date.now()) < 5000;
|
|
return { deep_equal, source_unchanged, now_ok, origin_ok };
|
|
}
|
|
"#;
|
|
let r = run_ts(ts, &[], serde_json::json!({})).await;
|
|
assert_eq!(
|
|
unwrap_value(&r),
|
|
serde_json::json!({ "deep_equal": true, "source_unchanged": true, "now_ok": true, "origin_ok": true }),
|
|
);
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore = "deno_core upgrade smoke; run with --ignored"]
|
|
async fn smoke_web_globals_construct() {
|
|
// `smoke_web_globals_are_wired` only checks the constructors are defined.
|
|
// Several are backed by deno_web ops (compression, message-port, URL
|
|
// pattern parsing) that a future bump could move out from under the
|
|
// still-defined constructor — it would pass the presence check but throw
|
|
// at `new`. Actually construct those here so that regression surfaces.
|
|
let ts = r#"
|
|
export async function main(): Promise<{ pathname: boolean; channel: boolean; gzip_ok: boolean }> {
|
|
const pat = new URLPattern({ pathname: "/books/:id" });
|
|
const pathname = pat.test("https://example.com/books/42");
|
|
|
|
const chan = new MessageChannel();
|
|
const channel = chan.port1 instanceof MessagePort && chan.port2 instanceof MessagePort;
|
|
|
|
// Round-trip "hi" through gzip compression then decompression.
|
|
const compressed = new Blob(["hi"]).stream().pipeThrough(new CompressionStream("gzip"));
|
|
const restored = compressed.pipeThrough(new DecompressionStream("gzip"));
|
|
const bytes = new Uint8Array(await new Response(restored).arrayBuffer());
|
|
const gzip_ok = new TextDecoder().decode(bytes) === "hi";
|
|
|
|
return { pathname, channel, gzip_ok };
|
|
}
|
|
"#;
|
|
let r = run_ts(ts, &[], serde_json::json!({})).await;
|
|
assert_eq!(
|
|
unwrap_value(&r),
|
|
serde_json::json!({ "pathname": true, "channel": true, "gzip_ok": true }),
|
|
);
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore = "deno_core upgrade smoke; run with --ignored"]
|
|
async fn smoke_readable_stream_roundtrip() {
|
|
// ReadableStream + TextEncoderStream/TextDecoderStream: pipe an encode
|
|
// stream through and read chunks back. Exercises the streams surface
|
|
// (06_streams.js) that `res.body instanceof ReadableStream` relies on.
|
|
let ts = r#"
|
|
export async function main(): Promise<{ is_readable: boolean; text: string }> {
|
|
const rs = new ReadableStream<string>({
|
|
start(controller) {
|
|
controller.enqueue("hello ");
|
|
controller.enqueue("stream");
|
|
controller.close();
|
|
},
|
|
});
|
|
const is_readable = rs instanceof ReadableStream;
|
|
const decoded = rs
|
|
.pipeThrough(new TextEncoderStream())
|
|
.pipeThrough(new TextDecoderStream());
|
|
let text = "";
|
|
for await (const chunk of decoded) text += chunk;
|
|
return { is_readable, text };
|
|
}
|
|
"#;
|
|
let r = run_ts(ts, &[], serde_json::json!({})).await;
|
|
assert_eq!(
|
|
unwrap_value(&r),
|
|
serde_json::json!({ "is_readable": true, "text": "hello stream" }),
|
|
);
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore = "deno_core upgrade smoke; run with --ignored"]
|
|
async fn smoke_web_crypto() {
|
|
// deno_crypto surface: the Web Crypto globals (`crypto`, `crypto.subtle`)
|
|
// that bring nativets to parity with the bun runner. Covers all three
|
|
// op families: getRandomValues (sync fill), randomUUID (RNG + formatting),
|
|
// and subtle.digest (async op returning an ArrayBuffer). The SHA-256 of
|
|
// "abc" is a fixed NIST vector, so a broken digest op fails the assert
|
|
// rather than silently returning garbage.
|
|
let ts = r#"
|
|
export async function main(): Promise<{ uuid: string; nonzero: boolean; sha256: string }> {
|
|
const uuid = crypto.randomUUID();
|
|
|
|
const buf = new Uint8Array(16);
|
|
crypto.getRandomValues(buf);
|
|
// A 16-byte CSPRNG fill returning all zeros is astronomically unlikely;
|
|
// a no-op/stub getRandomValues would leave the array zeroed.
|
|
const nonzero = buf.some((b) => b !== 0);
|
|
|
|
// "abc" as raw bytes — this test targets deno_crypto, so it avoids
|
|
// depending on deno_web's text encoding.
|
|
const data = new Uint8Array([0x61, 0x62, 0x63]);
|
|
const digest = await crypto.subtle.digest("SHA-256", data);
|
|
const sha256 = Array.from(new Uint8Array(digest))
|
|
.map((b) => b.toString(16).padStart(2, "0"))
|
|
.join("");
|
|
|
|
// `crypto` is installed as an accessor; polyfill-style assignment must
|
|
// still work rather than throw for want of a setter.
|
|
(globalThis as any).crypto = globalThis.crypto;
|
|
|
|
return { uuid, nonzero, sha256 };
|
|
}
|
|
"#;
|
|
let r = run_ts(ts, &[], serde_json::json!({})).await;
|
|
let v = unwrap_value(&r);
|
|
|
|
// UUIDv4 shape: 8-4-4-4-12 hex, version nibble 4, variant nibble 8/9/a/b.
|
|
let uuid = v.get("uuid").and_then(|x| x.as_str()).unwrap_or("");
|
|
let re =
|
|
regex::Regex::new(r"^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$")
|
|
.unwrap();
|
|
assert!(
|
|
re.is_match(uuid),
|
|
"randomUUID did not match UUIDv4 shape: {uuid:?}"
|
|
);
|
|
|
|
assert_eq!(
|
|
v.get("nonzero"),
|
|
Some(&serde_json::json!(true)),
|
|
"getRandomValues left the buffer all zeros",
|
|
);
|
|
|
|
// Known SHA-256("abc") vector.
|
|
assert_eq!(
|
|
v.get("sha256").and_then(|x| x.as_str()),
|
|
Some("ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"),
|
|
);
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore = "deno_core upgrade smoke; run with --ignored"]
|
|
async fn smoke_web_globals_edge_cases() {
|
|
// Broad functional sweep of every wired global — not just "defined" but
|
|
// "actually works". A constructor can be present yet throw at `new`/on call
|
|
// (an op moved by a deno bump, or a global dependency like DOMException not
|
|
// wired), which a presence check misses. Each check returns a bool; anything
|
|
// that isn't "ok" is reported in the failure message.
|
|
let ts = r#"
|
|
export async function main(): Promise<Record<string, string>> {
|
|
const out: Record<string, string> = {};
|
|
async function check(name: string, fn: () => any) {
|
|
try { out[name] = (await fn()) ? "ok" : "FAIL"; }
|
|
catch (e: any) { out[name] = "ERR: " + (e && e.message ? e.message : String(e)); }
|
|
}
|
|
|
|
// --- DOMException + AbortController/AbortSignal (needs the DOMException global) ---
|
|
await check("DOMException.construct", () => {
|
|
const e = new DOMException("nope", "AbortError");
|
|
return e.name === "AbortError" && e.message === "nope" && e instanceof DOMException;
|
|
});
|
|
await check("AbortController.abort_default_reason", () => {
|
|
const ac = new AbortController();
|
|
let fired = false;
|
|
ac.signal.addEventListener("abort", () => { fired = true; });
|
|
ac.abort(); // constructs a default DOMException("...", "AbortError")
|
|
return fired && ac.signal.aborted === true && ac.signal.reason?.name === "AbortError";
|
|
});
|
|
await check("AbortSignal.timeout", async () => {
|
|
const sig = AbortSignal.timeout(5);
|
|
await new Promise((r) => setTimeout(r, 30));
|
|
return sig.aborted === true && sig.reason?.name === "TimeoutError";
|
|
});
|
|
|
|
// --- TextEncoder / TextDecoder ---
|
|
await check("TextEncoder.encodeInto", () => {
|
|
const buf = new Uint8Array(3);
|
|
const { read, written } = new TextEncoder().encodeInto("abc", buf);
|
|
return read === 3 && written === 3 && buf[0] === 0x61;
|
|
});
|
|
await check("TextDecoder.utf16le", () =>
|
|
new TextDecoder("utf-16le").decode(new Uint8Array([0x41, 0x00])) === "A");
|
|
await check("TextDecoder.fatal_throws", () => {
|
|
try { new TextDecoder("utf-8", { fatal: true }).decode(new Uint8Array([0xff])); return false; }
|
|
catch { return true; }
|
|
});
|
|
|
|
// --- File ---
|
|
await check("File.props_and_read", async () => {
|
|
const f = new File(["hi"], "a.txt", { type: "text/plain", lastModified: 123 });
|
|
return f.name === "a.txt" && f.type === "text/plain" && f.lastModified === 123
|
|
&& f.size === 2 && (await f.text()) === "hi" && f instanceof Blob;
|
|
});
|
|
|
|
// --- Events ---
|
|
await check("EventTarget.dispatch_fires", () => {
|
|
const et = new EventTarget();
|
|
let got = "";
|
|
et.addEventListener("ping", (e: any) => { got = e.detail; });
|
|
et.dispatchEvent(new CustomEvent("ping", { detail: "pong" }));
|
|
return got === "pong";
|
|
});
|
|
// (A throwing EventTarget listener and reportError both surface the error
|
|
// asynchronously as an unhandled exception — matching bun, which exits
|
|
// non-zero — so they fail the script rather than a `check`; the dedicated
|
|
// smoke_eventtarget_throwing_listener_reports_original test covers that the
|
|
// ORIGINAL error is surfaced, not a masking one.)
|
|
await check("MessageEvent.data", () => new MessageEvent("m", { data: 42 }).data === 42);
|
|
await check("CloseEvent.code_reason", () => {
|
|
const e = new CloseEvent("close", { code: 1000, reason: "bye" });
|
|
return e.code === 1000 && e.reason === "bye";
|
|
});
|
|
await check("ErrorEvent.message", () => new ErrorEvent("error", { message: "boom" }).message === "boom");
|
|
|
|
// --- Streams ---
|
|
await check("ReadableStream.tee", async () => {
|
|
const rs = new ReadableStream<number>({ start(c) { c.enqueue(1); c.enqueue(2); c.close(); } });
|
|
const [a, b] = rs.tee();
|
|
const ra: number[] = []; for await (const x of a) ra.push(x);
|
|
const rb: number[] = []; for await (const x of b) rb.push(x);
|
|
return JSON.stringify(ra) === "[1,2]" && JSON.stringify(rb) === "[1,2]";
|
|
});
|
|
await check("ReadableStream.reader_cancel", async () => {
|
|
const rs = new ReadableStream<string>({ start(c) { c.enqueue("x"); } });
|
|
const rd = rs.getReader();
|
|
const { value } = await rd.read();
|
|
await rd.cancel();
|
|
return value === "x";
|
|
});
|
|
await check("WritableStream.write_close", async () => {
|
|
const chunks: string[] = [];
|
|
const ws = new WritableStream<string>({ write(c) { chunks.push(c); } });
|
|
const w = ws.getWriter();
|
|
await w.write("a"); await w.write("b"); await w.close();
|
|
return JSON.stringify(chunks) === '["a","b"]';
|
|
});
|
|
await check("TransformStream.identity", async () => {
|
|
const t = new TransformStream<string, string>();
|
|
const w = t.writable.getWriter(); w.write("hello"); w.close();
|
|
let o = ""; for await (const c of t.readable) o += c;
|
|
return o === "hello";
|
|
});
|
|
await check("Stream reader/controller instanceof globals", async () => {
|
|
// The reader/writer/controller constructors are exposed as globals for
|
|
// instanceof checks (bun parity). Obtain real instances and verify.
|
|
let ctrlOk = false;
|
|
const rs = new ReadableStream({
|
|
start(c: any) { ctrlOk = c instanceof ReadableStreamDefaultController; c.close(); },
|
|
});
|
|
const reader = rs.getReader();
|
|
const readerOk = reader instanceof ReadableStreamDefaultReader;
|
|
const ws = new WritableStream();
|
|
const writerOk = ws.getWriter() instanceof WritableStreamDefaultWriter;
|
|
return ctrlOk && readerOk && writerOk;
|
|
});
|
|
await check("ByteLengthQueuingStrategy", () => {
|
|
const s = new ByteLengthQueuingStrategy({ highWaterMark: 16 });
|
|
return s.highWaterMark === 16 && typeof s.size === "function";
|
|
});
|
|
await check("CountQueuingStrategy.in_stream", async () => {
|
|
const s = new CountQueuingStrategy({ highWaterMark: 1 });
|
|
const rs = new ReadableStream<number>({ start(c) { c.enqueue(7); c.close(); } }, s);
|
|
const rd = rs.getReader();
|
|
return (await rd.read()).value === 7 && s.highWaterMark === 1;
|
|
});
|
|
|
|
// --- URLPattern ---
|
|
await check("URLPattern.exec_groups", () => {
|
|
const p = new URLPattern({ pathname: "/users/:id" });
|
|
const m = p.exec("https://x.com/users/42");
|
|
return p.test("https://x.com/users/42") && m?.pathname.groups.id === "42";
|
|
});
|
|
|
|
// --- Compression (all three formats) ---
|
|
async function roundtrip(fmt: string): Promise<string> {
|
|
const comp = new Blob(["hello compression"]).stream().pipeThrough(new CompressionStream(fmt as any));
|
|
const decomp = comp.pipeThrough(new DecompressionStream(fmt as any));
|
|
return new TextDecoder().decode(new Uint8Array(await new Response(decomp).arrayBuffer()));
|
|
}
|
|
await check("Compression.gzip", async () => (await roundtrip("gzip")) === "hello compression");
|
|
await check("Compression.deflate", async () => (await roundtrip("deflate")) === "hello compression");
|
|
await check("Compression.deflate_raw", async () => (await roundtrip("deflate-raw")) === "hello compression");
|
|
|
|
// --- structuredClone edge cases ---
|
|
await check("structuredClone.Map", () => {
|
|
const c = structuredClone(new Map([["k", 1]]));
|
|
return c instanceof Map && c.get("k") === 1;
|
|
});
|
|
await check("structuredClone.Set", () => {
|
|
const c = structuredClone(new Set([1, 2]));
|
|
return c instanceof Set && c.has(2);
|
|
});
|
|
await check("structuredClone.Date", () => {
|
|
const c = structuredClone(new Date(0));
|
|
return c instanceof Date && c.getTime() === 0;
|
|
});
|
|
await check("structuredClone.TypedArray", () => {
|
|
const c = structuredClone(new Uint8Array([1, 2, 3]));
|
|
return c instanceof Uint8Array && c[1] === 2;
|
|
});
|
|
await check("structuredClone.circular", () => {
|
|
const o: any = {}; o.self = o;
|
|
const c = structuredClone(o);
|
|
return c.self === c;
|
|
});
|
|
await check("structuredClone.rejects_function", () => {
|
|
try { structuredClone(() => {}); return false; } catch { return true; }
|
|
});
|
|
await check("structuredClone.options_bag", () => {
|
|
const c = structuredClone({ a: 1 }, { transfer: [] });
|
|
return c.a === 1;
|
|
});
|
|
|
|
// --- performance ---
|
|
await check("performance.now_monotonic", () => {
|
|
const a = performance.now(); const b = performance.now();
|
|
return Number.isFinite(a) && b >= a;
|
|
});
|
|
await check("performance.mark_measure", () => {
|
|
performance.mark("m1");
|
|
performance.measure("meas", "m1");
|
|
return performance.getEntriesByType("measure").some((e: any) => e.name === "meas");
|
|
});
|
|
await check("performance.constructor_globals", () => {
|
|
// The Performance/PerformanceEntry/Mark/Measure constructors are exposed
|
|
// as globals (bun parity) for instanceof checks against real entries.
|
|
const mark = performance.mark("m2");
|
|
const meas = performance.measure("meas2", "m2");
|
|
return performance instanceof Performance
|
|
&& mark instanceof PerformanceMark && mark instanceof PerformanceEntry
|
|
&& meas instanceof PerformanceMeasure && meas instanceof PerformanceEntry;
|
|
});
|
|
await check("performance.toJSON_origin", () => {
|
|
const j: any = performance.toJSON();
|
|
return Number.isFinite(j.timeOrigin);
|
|
});
|
|
|
|
// --- MessageChannel / MessagePort (async delivery, guarded by timeout) ---
|
|
await check("MessagePort.postMessage", async () => {
|
|
const mc = new MessageChannel();
|
|
const got = await Promise.race([
|
|
new Promise<string>((res) => {
|
|
mc.port2.onmessage = (e: any) => res(JSON.stringify(e.data));
|
|
mc.port1.postMessage({ hello: "world" });
|
|
}),
|
|
new Promise<string>((res) => setTimeout(() => res("TIMEOUT"), 3000)),
|
|
]);
|
|
return got === '{"hello":"world"}';
|
|
});
|
|
|
|
// --- Web Crypto works alongside the other wired globals ---
|
|
await check("crypto.getRandomValues", () => {
|
|
const buf = new Uint8Array(16);
|
|
crypto.getRandomValues(buf);
|
|
return buf.some((b) => b !== 0);
|
|
});
|
|
await check("crypto.subtle.digest", async () => {
|
|
const d = await crypto.subtle.digest("SHA-256", new Uint8Array([0x61, 0x62, 0x63]));
|
|
return new Uint8Array(d)[0] === 0xba; // SHA-256("abc") starts with 0xba
|
|
});
|
|
|
|
return out;
|
|
}
|
|
"#;
|
|
let r = run_ts(ts, &[], serde_json::json!({})).await;
|
|
let v = unwrap_value(&r);
|
|
let obj = v.as_object().expect("expected an object result");
|
|
let failures: Vec<String> = obj
|
|
.iter()
|
|
.filter(|(_, val)| val.as_str() != Some("ok"))
|
|
.map(|(name, val)| format!("{name} => {}", val.as_str().unwrap_or("?")))
|
|
.collect();
|
|
assert!(
|
|
failures.is_empty(),
|
|
"edge-case checks that did not pass ({} of {}):\n{}",
|
|
failures.len(),
|
|
obj.len(),
|
|
failures.join("\n"),
|
|
);
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore = "deno_core upgrade smoke; run with --ignored"]
|
|
async fn smoke_eventtarget_throwing_listener_reports_original() {
|
|
// Invariant: globalThis is a functional EventTarget, so deno_web's
|
|
// reportException has a valid saved global dispatch target. An uncaught
|
|
// EventTarget-listener error is therefore surfaced with its original message
|
|
// as an async unhandled exception (matching bun, which exits non-zero), not
|
|
// replaced by a masking "Illegal invocation"/undefined-reference error.
|
|
let ts = r#"
|
|
export async function main(): Promise<void> {
|
|
const et = new EventTarget();
|
|
et.addEventListener("boom", () => { throw new Error("wm_listener_marker"); });
|
|
et.dispatchEvent(new Event("boom"));
|
|
// Give the async unhandled-exception report a tick to fire.
|
|
await new Promise((r) => setTimeout(r, 20));
|
|
}
|
|
"#;
|
|
let r = run_ts(ts, &[], serde_json::json!({})).await;
|
|
let err = r
|
|
.result
|
|
.expect_err("a throwing listener should surface an error");
|
|
assert!(
|
|
err.contains("wm_listener_marker"),
|
|
"the original listener error was lost: {err}",
|
|
);
|
|
assert!(
|
|
!err.contains("Illegal invocation") && !err.to_lowercase().contains("undefined"),
|
|
"dispatchEvent surfaced a masking error instead of the original: {err}",
|
|
);
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore = "deno_core upgrade smoke; run with --ignored"]
|
|
async fn smoke_report_error_both_call_forms() {
|
|
// reportError must work both unqualified and as a property of globalThis.
|
|
// The property form goes through a receiver check (`this === globalThis_`),
|
|
// which only passes because globalThis is the saved EventTarget reference;
|
|
// otherwise it throws "Illegal invocation". Both forms report the error as an
|
|
// async unhandled exception (matching bun), so the script errors with the
|
|
// original message rather than a masking one.
|
|
for (label, call) in [
|
|
("bare", "reportError(new Error(\"wm_report_marker\"));"),
|
|
(
|
|
"property",
|
|
"globalThis.reportError(new Error(\"wm_report_marker\"));",
|
|
),
|
|
] {
|
|
let ts = format!(
|
|
r#"
|
|
export async function main(): Promise<void> {{
|
|
{call}
|
|
await new Promise((r) => setTimeout(r, 20));
|
|
}}
|
|
"#
|
|
);
|
|
let r = run_ts(&ts, &[], serde_json::json!({})).await;
|
|
let err = r
|
|
.result
|
|
.expect_err(&format!("{label} reportError should surface an error"));
|
|
assert!(
|
|
err.contains("wm_report_marker"),
|
|
"{label} reportError lost the original error: {err}",
|
|
);
|
|
assert!(
|
|
!err.contains("Illegal invocation"),
|
|
"{label} reportError threw Illegal invocation: {err}",
|
|
);
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Network — actually exercise deno_fetch end-to-end. Skip in air-gapped CI
|
|
// with `--skip smoke_net_`.
|
|
// -----------------------------------------------------------------------------
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore = "deno_core upgrade smoke (network); run with --ignored"]
|
|
async fn smoke_net_fetch_example_com() {
|
|
// example.com is one of the most stable hosts on the internet and
|
|
// returns a tiny known-text body, so we can both assert "fetch works"
|
|
// and "the response body parses correctly through deno_fetch".
|
|
let ts = r#"
|
|
export async function main(): Promise<{ status: number; has_marker: boolean }> {
|
|
const r = await fetch("https://example.com/");
|
|
const body = await r.text();
|
|
return { status: r.status, has_marker: body.includes("Example Domain") };
|
|
}
|
|
"#;
|
|
let r = run_ts(ts, &[], serde_json::json!({})).await;
|
|
let v = unwrap_value(&r);
|
|
assert_eq!(v.get("status").and_then(|x| x.as_u64()), Some(200));
|
|
assert_eq!(v.get("has_marker"), Some(&serde_json::json!(true)));
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore = "deno_core upgrade smoke (network); run with --ignored"]
|
|
async fn smoke_net_fetch_json_and_headers() {
|
|
// httpbin.org/anything echoes request metadata back as JSON, so we
|
|
// can verify: deno_fetch sends custom headers, parses JSON response,
|
|
// and propagates query params end-to-end.
|
|
let ts = r#"
|
|
export async function main(): Promise<{ ua: string; arg: string }> {
|
|
const r = await fetch("https://httpbin.org/anything?nativets=ok", {
|
|
headers: { "x-windmill-smoke": "1" },
|
|
});
|
|
if (!r.ok) throw new Error(`status ${r.status}`);
|
|
const j: any = await r.json();
|
|
return {
|
|
ua: j.headers["X-Windmill-Smoke"] ?? "",
|
|
arg: j.args.nativets ?? "",
|
|
};
|
|
}
|
|
"#;
|
|
let r = run_ts(ts, &[], serde_json::json!({})).await;
|
|
let v = unwrap_value(&r);
|
|
assert_eq!(v.get("ua").and_then(|x| x.as_str()), Some("1"));
|
|
assert_eq!(v.get("arg").and_then(|x| x.as_str()), Some("ok"));
|
|
}
|