mod common; use common::run; #[test] fn formats_values_and_routes_levels() { let output = run( r#" if (Object.getPrototypeOf(Object.getPrototypeOf(console)) !== Object.prototype || Object.prototype.toString.call(console) !== "[object console]") { throw new Error("Unexpected console namespace"); } try { console.count(Symbol("invalid")); throw new Error("Symbol labels must be rejected"); } catch (error) { if (error.name !== "TypeError") throw error; } if (console.log() !== undefined) throw new Error("Unexpected return value"); console.log("%s %i %f %% %c%o", "hello", "12x", "1.5x", "color:red", { a: 1 }); console.debug("debug"); console.info("info"); console.warn("warning"); console.error("error"); console.assert(true, "invisible"); console.assert(false, "bad %s", "input"); let touched = 0; const object = { a: 1 }; object.self = object; Object.defineProperty(object, "getter", { enumerable: true, get() { touched++; throw new Error("Getter invoked"); }, }); console.log(object); if (touched) throw new Error("Inspection invoked a getter"); console.dir(new Map([["key", new Set([1, 2])]])); console.dirxml("xml"); "#, &[], ); let stdout = String::from_utf8(output.stdout).unwrap(); assert!(stdout.starts_with("hello 12 1.5 % { a: 1 }\ndebug\ninfo\n")); assert!(stdout.contains("[Circular]")); assert!(stdout.contains("[Getter]")); assert!(stdout.contains("Map {")); assert!(stdout.contains("Set { 1, 2 }")); assert!(stdout.ends_with("xml\n")); assert_eq!( output.stderr, b"warning\nerror\nAssertion failed: bad input\n" ); } #[test] fn objects_customize_their_inspection() { let output = run( r#" const { inspect } = await import("avril:std"); const custom = Symbol.for("runtime.avril.inspect"); class Temperature { constructor(degrees) { this.degrees = degrees; } [custom](inspect, options) { return `${inspect(this.degrees, options)}°C`; } } const wrapper = { [custom]() { return { unwrapped: true }; } }; const reentrant = { a: 1, [custom](inspect) { return `<${inspect(this)}>`; } }; let touched = 0; const guarded = { get [custom]() { touched++; return () => "getter"; } }; const failing = { [custom]() { throw new Error("boom"); } }; console.log({ now: new Temperature(21) }, wrapper); console.log(reentrant); console.log(guarded); console.log(failing); if (touched) throw new Error("Inspection invoked a getter"); console.log(inspect("quoted"), inspect(["a"], { depth: 0 })); console.log(JSON.stringify(inspect({ text: "x", count: 1 }, { colors: true }))); console.dir("dir"); "#, &[], ); assert_eq!( String::from_utf8(output.stdout).unwrap(), concat!( "{ now: 21°C } { unwrapped: true }\n", "<{ a: 1, [Symbol(runtime.avril.inspect)]: [Function: [runtime.avril.inspect]] }>\n", "{ [Symbol(runtime.avril.inspect)]: [Getter] }\n", "[Uninspectable]\n", "\"quoted\" [ \"a\" ]\n", r#""{ text: \u001b[32m\"x\"\u001b[39m, count: \u001b[33m1\u001b[39m }""#, "\n\"dir\"\n", ) ); } #[test] fn counters_groups_timers_tables_and_traces() { let output = run( r#" console.count("requests"); console.count("requests"); console.countReset("requests"); console.count("requests"); console.group("group"); console.log("inside"); console.groupCollapsed("nested"); console.log("deep"); console.groupEnd(); console.groupEnd(); console.time("elapsed"); console.timeLog("elapsed", "tick"); console.timeEnd("elapsed"); console.timeEnd("elapsed"); console.table([{ a: 1 }, { a: 2 }]); console.clear(); console.trace("here"); "#, &[], ); let stdout = String::from_utf8(output.stdout).unwrap(); assert!(stdout.starts_with( "requests: 1\nrequests: 2\nrequests: 1\ngroup\n inside\n nested\n deep\n" )); assert_eq!( stdout .lines() .filter(|line| line.starts_with("elapsed: ")) .count(), 2 ); assert!(stdout.contains(" ms tick\n")); assert!(stdout.contains("(index)")); assert!(!stdout.contains('\u{1b}')); let stderr = String::from_utf8(output.stderr).unwrap(); assert!(stderr.starts_with("Timer 'elapsed' does not exist\nTrace: here\n")); assert!(stderr.contains("main.js:")); assert!(!stderr.contains("ext:console")); } #[test] fn console_uses_primordials_and_bypasses_public_stream_locks() { let output = run( r#" import { stdout } from "avril:std"; const writer = stdout.getWriter(); await writer.write(new Uint8Array([98, 101, 102, 111, 114, 101, 10])); console.log("locked"); await writer.close(); const mapPrototype = Map.prototype; const setPrototype = Set.prototype; const poison = () => { throw new Error("Poisoned intrinsic"); }; globalThis.String = poison; globalThis.Map = poison; globalThis.Set = poison; Object.keys = poison; Array.prototype.map = poison; Array.prototype.join = poison; Array.prototype.push = poison; Array.prototype[Symbol.iterator] = poison; mapPrototype.get = poison; mapPrototype.set = poison; setPrototype.has = poison; setPrototype.add = poison; RegExp.prototype.exec = poison; Number.prototype.toFixed = poison; console.log("%s", "closed"); console.log({ a: 1 }); console.assert(false, { a: 1 }); console.count(); console.table([{ a: 1 }]); console.time(); console.timeEnd(); "#, &[], ); let stdout = String::from_utf8(output.stdout).unwrap(); assert!(stdout.starts_with("before\nlocked\nclosed\n{ a: 1 }\ndefault: 1\n")); assert!(stdout.contains("(index)")); assert!(stdout.contains("default: ")); assert_eq!(output.stderr, b"Assertion failed { a: 1 }\n"); } #[test] fn inspects_exotic_values_within_depth_bounds() { let output = run( r#" const revocable = Proxy.revocable(function revoked() {}, {}); revocable.revoke(); console.log(revocable.proxy); console.log(new Proxy(function trapped() {}, { getOwnPropertyDescriptor() { throw new Error("Trap invoked"); }, })); console.log(new Proxy(function named() {}, {})); console.log(new Date(NaN), new Date(0)); let deep = {}; for (let index = 0; index < 12; index++) deep = { a: deep }; console.dir(deep, { depth: null }); console.dir(deep, { depth: Infinity }); console.dir(deep, { depth: NaN }); console.dir(deep, null); "#, &[], ); let nested = |depth: usize| format!("{}[Object]{}", "{ a: ".repeat(depth), " }".repeat(depth)); assert_eq!( String::from_utf8(output.stdout).unwrap(), format!( "[Uninspectable]\n[Uninspectable]\n[Function: named]\nInvalid Date 1970-01-01T00:00:00.000Z\n{}\n{}\n{}\n{}\n", nested(11), nested(11), nested(4), nested(4), ) ); } #[test] fn names_objects_and_shows_promise_state() { let output = run( r#" class Point { x = 1 } class Lazy { get [Symbol.toStringTag]() { throw new Error("getter ran"); } } const failed = Promise.reject(new RangeError("no")); failed.catch(() => {}); console.log(new Point(), Object.create(null), { plain: true }, new Lazy()); console.log(new Promise(() => {}), Promise.resolve(new Point())); console.log((async function* () {})(), (function* () {})()); console.dir({ a: { b: { c: { point: new Point(), map: new Map() } } } }); console.log(failed); // Inherited accessors on Object.prototype never run during inspection. Object.defineProperty(Object.prototype, "get", { get() { throw new Error("polluted"); }, configurable: true }); console.log(new Point(), { plain: true }); delete Object.prototype.get; "#, &[], ); let stdout = String::from_utf8(output.stdout).unwrap(); let mut lines = stdout.lines(); assert_eq!( lines.next(), Some("Point { x: 1 } {} { plain: true } Lazy {}") ); assert_eq!( lines.next(), Some("Promise { } Promise { Point { x: 1 } }") ); assert_eq!(lines.next(), Some("[AsyncGenerator] {} [Generator] {}")); assert_eq!( lines.next(), Some("{ a: { b: { c: { point: [Point], map: [Map] } } } }") ); assert_eq!(lines.next(), Some("Promise {")); assert_eq!(lines.next(), Some(" RangeError: no")); assert!(lines.next().unwrap().starts_with(" at ")); assert!(stdout.contains("\n}\nPoint { x: 1 } { plain: true }\n")); } #[test] fn tables_keep_index_apart_and_list_collections() { let output = run( r#" console.table([{ "(index)": "own", a: 1 }, 2]); console.table(new Map([["key", { a: 1 }]])); console.table(new Set(["value"])); const revocable = Proxy.revocable({}, {}); revocable.revoke(); console.table(revocable.proxy); "#, &[], ); let stdout = String::from_utf8(output.stdout).unwrap(); let rows: Vec> = stdout .lines() .filter(|line| line.starts_with('|')) .map(|line| line.trim_matches('|').split('|').map(str::trim).collect()) .collect(); assert_eq!( rows, vec![ vec!["(index)", "(index)", "a", "Values"], vec!["0", "\"own\"", "1", ""], vec!["1", "", "", "2"], vec!["(iteration index)", "Key", "Values"], vec!["0", "\"key\"", "{ a: 1 }"], vec!["(iteration index)", "Values"], vec!["0", "\"value\""], ] ); assert!(stdout.ends_with("[Uninspectable]\n")); } #[test] fn groups_indent_every_line() { let output = run( r#" console.group("group"); console.log("first\nsecond"); console.groupEnd(); console.log("third\nfourth"); "#, &[], ); assert_eq!( String::from_utf8(output.stdout).unwrap(), "group\n first\n second\nthird\nfourth\n" ); } #[test] fn grouped_tables_and_fallback_formatting_share_output_state() { let output = run( r#" console.group('records'); console.table([{ a: 1 }]); console.table('left\nright'); const revoked = Proxy.revocable({}, {}); revoked.revoke(); console.table(revoked.proxy); console.groupEnd(); console.log('%o', { a: 2 }); "#, &[], ); let stdout = String::from_utf8(output.stdout).unwrap(); assert!(stdout.starts_with("records\n")); assert!(stdout.ends_with(" left\n right\n [Uninspectable]\n{ a: 2 }\n")); let table = stdout.split(" left\n").next().unwrap(); assert!(table.contains("| (index) | a |")); for line in table.lines().skip(1) { assert!(line.starts_with(" ")); } assert!(output.stderr.is_empty()); } #[test] fn logs_zod_shaped_errors_with_current_message() { let output = run( r#" class Shaped extends Error { constructor() { super(""); this.name = "Shaped"; const issues = [{ expected: "string", code: "invalid_type", path: [], message: "Invalid input" }]; const text = JSON.stringify(issues, null, 2); Object.defineProperty(this, "message", { get() { return text; }, configurable: true, enumerable: true }); function absent() {} Error.captureStackTrace(this, absent); } } console.log(new Shaped()); "#, &[], ); let stdout = String::from_utf8(output.stdout).unwrap(); assert!(stdout.starts_with("Shaped: [\n"), "{stdout}"); assert!(stdout.contains("\"expected\": \"string\""), "{stdout}"); assert!(!stdout.contains(" at "), "{stdout}"); assert!(output.stderr.is_empty()); } #[test] fn inspection_and_formatting_keep_user_conversion_boundaries() { let output = run( r#" import { inspect } from "avril:std"; const reads = []; const options = function () {}; Object.defineProperties(options, { depth: { get() { reads.push("depth"); return { valueOf() { throw new Error("depth coerced"); } }; } }, colors: { get() { reads.push("colors"); return ""; } }, }); if (inspect({ value: 1 }, options) !== "{ value: 1 }" || reads.join() !== "depth,colors") { throw new Error("inspect option reads"); } console.dir({ value: 1 }, options); if (reads.join() !== "depth,colors") throw new Error("dir invoked getters"); let converted = 0; console.count({ [Symbol.toPrimitive](hint) { if (hint !== "string") throw new Error("label hint"); converted++; return "label"; } }); if (converted !== 1) throw new Error("label read twice"); console.assert({ valueOf() { throw new Error("condition coerced"); } }, "hidden"); let iterated = 0, closed = 0; const columns = { [Symbol.iterator]() { if (this !== columns) throw new Error("iterator receiver"); iterated++; return { next() { return { done: false, value: Symbol("invalid") }; }, return() { closed++; return {}; }, }; }, }; let rejected = false; try { console.table([{ value: 1 }], columns); } catch (error) { if (!(error instanceof TypeError)) throw error; rejected = true; } if (!rejected || iterated !== 1 || closed !== 1) { throw new Error("table iterator cleanup"); } const previous = Error.prepareStackTrace; let formatted = 0; try { Error.prepareStackTrace = () => ({ toString() { formatted++; return "custom\ntrace"; }, }); console.trace("ignored by formatter"); } finally { Error.prepareStackTrace = previous; } if (formatted !== 1 || inspect.length !== 1 || console.dir.length !== 0 || console.count.length !== 0 || console.trace.length !== 0) { throw new Error("trace conversion or method arity"); } "#, &[], ); assert_eq!(output.stdout, b"{ value: 1 }\nlabel: 1\n"); assert_eq!(output.stderr, b"custom\ntrace\n"); }