char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
1738b3c
main
1mod common; 2 3use common:: run; 4 5# [ test ] 6fn formats_values_and_routes_levels () { 7let output =run ( 8r#" 9if (Object.getPrototypeOf(Object.getPrototypeOf(console)) !== Object.prototype 10|| Object.prototype.toString.call(console) !== "[object console]") { 11throw new Error("Unexpected console namespace"); 12} 13try { 14console.count(Symbol("invalid")); 15throw new Error("Symbol labels must be rejected"); 16} catch (error) { 17if (error.name !== "TypeError") throw error; 18} 19if (console.log() !== undefined) throw new Error("Unexpected return value"); 20console.log("%s %i %f %% %c%o", "hello", "12x", "1.5x", "color:red", { a: 1 }); 21console.debug("debug"); 22console.info("info"); 23console.warn("warning"); 24console.error("error"); 25console.assert(true, "invisible"); 26console.assert(false, "bad %s", "input"); 27let touched = 0; 28const object = { a: 1 }; 29object.self = object; 30Object.defineProperty(object, "getter", { 31enumerable: true, 32get() { touched++; throw new Error("Getter invoked"); }, 33}); 34console.log(object); 35if (touched) throw new Error("Inspection invoked a getter"); 36console.dir(new Map([["key", new Set([1, 2])]])); 37console.dirxml("xml"); 38"# , 39& [], 40); 41let stdout =String :: from_utf8 ( output. stdout ). unwrap (); 42assert! ( stdout. starts_with ( "hello 12 1.5 % { a: 1 }\ndebug\ninfo\n" )); 43assert! ( stdout. contains ( "[Circular]" )); 44assert! ( stdout. contains ( "[Getter]" )); 45assert! ( stdout. contains ( "Map {" )); 46assert! ( stdout. contains ( "Set { 1, 2 }" )); 47assert! ( stdout. ends_with ( "xml\n" )); 48assert_eq! ( 49 output. stderr , 50b"warning\nerror\nAssertion failed: bad input\n" 51); 52} 53 54# [ test ] 55fn objects_customize_their_inspection () { 56let output =run ( 57r#" 58const { inspect } = await import("avril:std"); 59const custom = Symbol.for("runtime.avril.inspect"); 60class Temperature { 61constructor(degrees) { this.degrees = degrees; } 62[custom](inspect, options) { 63return `${inspect(this.degrees, options)}°C`; 64} 65} 66const wrapper = { [custom]() { return { unwrapped: true }; } }; 67const reentrant = { a: 1, [custom](inspect) { return `<${inspect(this)}>`; } }; 68let touched = 0; 69const guarded = { get [custom]() { touched++; return () => "getter"; } }; 70const failing = { [custom]() { throw new Error("boom"); } }; 71console.log({ now: new Temperature(21) }, wrapper); 72console.log(reentrant); 73console.log(guarded); 74console.log(failing); 75if (touched) throw new Error("Inspection invoked a getter"); 76console.log(inspect("quoted"), inspect(["a"], { depth: 0 })); 77console.log(JSON.stringify(inspect({ text: "x", count: 1 }, { colors: true }))); 78console.dir("dir"); 79"# , 80& [], 81); 82assert_eq! ( 83String :: from_utf8 ( output. stdout ). unwrap (), 84concat! ( 85"{ now: 21°C } { unwrapped: true }\n" , 86"<{ a: 1, [Symbol(runtime.avril.inspect)]: [Function: [runtime.avril.inspect]] }>\n" , 87"{ [Symbol(runtime.avril.inspect)]: [Getter] }\n" , 88"[Uninspectable]\n" , 89"\"quoted\" [ \"a\" ]\n" , 90r#""{ text: \u001b[32m\"x\"\u001b[39m, count: \u001b[33m1\u001b[39m }""# , 91"\n\"dir\"\n" , 92) 93); 94} 95 96# [ test ] 97fn counters_groups_timers_tables_and_traces () { 98let output =run ( 99r#" 100console.count("requests"); 101console.count("requests"); 102console.countReset("requests"); 103console.count("requests"); 104console.group("group"); 105console.log("inside"); 106console.groupCollapsed("nested"); 107console.log("deep"); 108console.groupEnd(); 109console.groupEnd(); 110console.time("elapsed"); 111console.timeLog("elapsed", "tick"); 112console.timeEnd("elapsed"); 113console.timeEnd("elapsed"); 114console.table([{ a: 1 }, { a: 2 }]); 115console.clear(); 116console.trace("here"); 117"# , 118& [], 119); 120let stdout =String :: from_utf8 ( output. stdout ). unwrap (); 121assert! ( stdout. starts_with ( 122"requests: 1\nrequests: 2\nrequests: 1\ngroup\n inside\n nested\n deep\n" 123)); 124assert_eq! ( 125 stdout 126. lines () 127. filter ( |line| line. starts_with ( "elapsed: " )) 128. count (), 1292 130); 131assert! ( stdout. contains ( " ms tick\n" )); 132assert! ( stdout. contains ( "(index)" )); 133assert! ( !stdout. contains ( '\u{1b}' )); 134let stderr =String :: from_utf8 ( output. stderr ). unwrap (); 135assert! ( stderr. starts_with ( "Timer 'elapsed' does not exist\nTrace: here\n" )); 136assert! ( stderr. contains ( "main.js:" )); 137assert! ( !stderr. contains ( "ext:console" )); 138} 139 140# [ test ] 141fn console_uses_primordials_and_bypasses_public_stream_locks () { 142let output =run ( 143r#" 144import { stdout } from "avril:std"; 145const writer = stdout.getWriter(); 146await writer.write(new Uint8Array([98, 101, 102, 111, 114, 101, 10])); 147console.log("locked"); 148await writer.close(); 149const mapPrototype = Map.prototype; 150const setPrototype = Set.prototype; 151const poison = () => { throw new Error("Poisoned intrinsic"); }; 152globalThis.String = poison; 153globalThis.Map = poison; 154globalThis.Set = poison; 155Object.keys = poison; 156Array.prototype.map = poison; 157Array.prototype.join = poison; 158Array.prototype.push = poison; 159Array.prototype[Symbol.iterator] = poison; 160mapPrototype.get = poison; 161mapPrototype.set = poison; 162setPrototype.has = poison; 163setPrototype.add = poison; 164RegExp.prototype.exec = poison; 165Number.prototype.toFixed = poison; 166console.log("%s", "closed"); 167console.log({ a: 1 }); 168console.assert(false, { a: 1 }); 169console.count(); 170console.table([{ a: 1 }]); 171console.time(); 172console.timeEnd(); 173"# , 174& [], 175); 176let stdout =String :: from_utf8 ( output. stdout ). unwrap (); 177assert! ( stdout. starts_with ( "before\nlocked\nclosed\n{ a: 1 }\ndefault: 1\n" )); 178assert! ( stdout. contains ( "(index)" )); 179assert! ( stdout. contains ( "default: " )); 180assert_eq! ( output. stderr , b"Assertion failed { a: 1 }\n" ); 181} 182 183# [ test ] 184fn inspects_exotic_values_within_depth_bounds () { 185let output =run ( 186r#" 187const revocable = Proxy.revocable(function revoked() {}, {}); 188revocable.revoke(); 189console.log(revocable.proxy); 190console.log(new Proxy(function trapped() {}, { 191getOwnPropertyDescriptor() { throw new Error("Trap invoked"); }, 192})); 193console.log(new Proxy(function named() {}, {})); 194console.log(new Date(NaN), new Date(0)); 195let deep = {}; 196for (let index = 0; index < 12; index++) deep = { a: deep }; 197console.dir(deep, { depth: null }); 198console.dir(deep, { depth: Infinity }); 199console.dir(deep, { depth: NaN }); 200console.dir(deep, null); 201"# , 202& [], 203); 204let nested = |depth : usize |format! ( "{}[Object]{}" , "{ a: " . repeat ( depth), " }" . repeat ( depth)); 205assert_eq! ( 206String :: from_utf8 ( output. stdout ). unwrap (), 207format! ( 208"[Uninspectable]\n[Uninspectable]\n[Function: named]\nInvalid Date 1970-01-01T00:00:00.000Z\n{}\n{}\n{}\n{}\n" , 209nested ( 11 ), 210nested ( 11 ), 211nested ( 4 ), 212nested ( 4 ), 213) 214); 215} 216 217# [ test ] 218fn names_objects_and_shows_promise_state () { 219let output =run ( 220r#" 221class Point { x = 1 } 222class Lazy { get [Symbol.toStringTag]() { throw new Error("getter ran"); } } 223const failed = Promise.reject(new RangeError("no")); failed.catch(() => {}); 224console.log(new Point(), Object.create(null), { plain: true }, new Lazy()); 225console.log(new Promise(() => {}), Promise.resolve(new Point())); 226console.log((async function* () {})(), (function* () {})()); 227console.dir({ a: { b: { c: { point: new Point(), map: new Map() } } } }); 228console.log(failed); 229// Inherited accessors on Object.prototype never run during inspection. 230Object.defineProperty(Object.prototype, "get", { get() { throw new Error("polluted"); }, configurable: true }); 231console.log(new Point(), { plain: true }); 232delete Object.prototype.get; 233"# , 234& [], 235); 236let stdout =String :: from_utf8 ( output. stdout ). unwrap (); 237let mut lines = stdout. lines (); 238assert_eq! ( 239 lines. next (), 240Some ( "Point { x: 1 } {} { plain: true } Lazy {}" ) 241); 242assert_eq! ( 243 lines. next (), 244Some ( "Promise { <pending> } Promise { Point { x: 1 } }" ) 245); 246assert_eq! ( lines. next (), Some ( "[AsyncGenerator] {} [Generator] {}" )); 247assert_eq! ( 248 lines. next (), 249Some ( "{ a: { b: { c: { point: [Point], map: [Map] } } } }" ) 250); 251assert_eq! ( lines. next (), Some ( "Promise {" )); 252assert_eq! ( lines. next (), Some ( " <rejected> RangeError: no" )); 253assert! ( lines. next (). unwrap (). starts_with ( " at " )); 254assert! ( stdout. contains ( "\n}\nPoint { x: 1 } { plain: true }\n" )); 255} 256 257# [ test ] 258fn tables_keep_index_apart_and_list_collections () { 259let output =run ( 260r#" 261console.table([{ "(index)": "own", a: 1 }, 2]); 262console.table(new Map([["key", { a: 1 }]])); 263console.table(new Set(["value"])); 264const revocable = Proxy.revocable({}, {}); 265revocable.revoke(); 266console.table(revocable.proxy); 267"# , 268& [], 269); 270let stdout =String :: from_utf8 ( output. stdout ). unwrap (); 271let rows: Vec < Vec < _ >> = stdout 272. lines () 273. filter ( |line| line. starts_with ( '|' )) 274. map ( |line| line. trim_matches ( '|' ). split ( '|' ). map ( str:: trim). collect ()) 275. collect (); 276assert_eq! ( 277 rows, 278vec! [ 279vec! [ "(index)" , "(index)" , "a" , "Values" ], 280vec! [ "0" , "\"own\"" , "1" , "" ], 281vec! [ "1" , "" , "" , "2" ], 282vec! [ "(iteration index)" , "Key" , "Values" ], 283vec! [ "0" , "\"key\"" , "{ a: 1 }" ], 284vec! [ "(iteration index)" , "Values" ], 285vec! [ "0" , "\"value\"" ], 286] 287); 288assert! ( stdout. ends_with ( "[Uninspectable]\n" )); 289} 290 291# [ test ] 292fn groups_indent_every_line () { 293let output =run ( 294r#" 295console.group("group"); 296console.log("first\nsecond"); 297console.groupEnd(); 298console.log("third\nfourth"); 299"# , 300& [], 301); 302assert_eq! ( 303String :: from_utf8 ( output. stdout ). unwrap (), 304"group\n first\n second\nthird\nfourth\n" 305); 306} 307 308# [ test ] 309fn grouped_tables_and_fallback_formatting_share_output_state () { 310let output =run ( 311r#" 312console.group('records'); 313console.table([{ a: 1 }]); 314console.table('left\nright'); 315const revoked = Proxy.revocable({}, {}); 316revoked.revoke(); 317console.table(revoked.proxy); 318console.groupEnd(); 319console.log('%o', { a: 2 }); 320"# , 321& [], 322); 323let stdout =String :: from_utf8 ( output. stdout ). unwrap (); 324assert! ( stdout. starts_with ( "records\n" )); 325assert! ( stdout. ends_with ( " left\n right\n [Uninspectable]\n{ a: 2 }\n" )); 326let table = stdout. split ( " left\n" ). next (). unwrap (); 327assert! ( table. contains ( "| (index) | a |" )); 328for linein table. lines (). skip ( 1 ) { 329assert! ( line. starts_with ( " " )); 330} 331assert! ( output. stderr . is_empty ()); 332} 333 334# [ test ] 335fn logs_zod_shaped_errors_with_current_message () { 336let output =run ( 337r#" 338class Shaped extends Error { 339constructor() { 340super(""); 341this.name = "Shaped"; 342const issues = [{ expected: "string", code: "invalid_type", path: [], message: "Invalid input" }]; 343const text = JSON.stringify(issues, null, 2); 344Object.defineProperty(this, "message", { get() { return text; }, configurable: true, enumerable: true }); 345function absent() {} 346Error.captureStackTrace(this, absent); 347} 348} 349console.log(new Shaped()); 350"# , 351& [], 352); 353let stdout =String :: from_utf8 ( output. stdout ). unwrap (); 354assert! ( stdout. starts_with ( "Shaped: [\n" ), "{stdout}" ); 355assert! ( stdout. contains ( "\"expected\": \"string\"" ), "{stdout}" ); 356assert! ( !stdout. contains ( " at " ), "{stdout}" ); 357assert! ( output. stderr . is_empty ()); 358} 359 360# [ test ] 361fn inspection_and_formatting_keep_user_conversion_boundaries () { 362let output =run ( 363r#" 364import { inspect } from "avril:std"; 365const reads = []; 366const options = function () {}; 367Object.defineProperties(options, { 368depth: { get() { 369reads.push("depth"); 370return { valueOf() { throw new Error("depth coerced"); } }; 371} }, 372colors: { get() { reads.push("colors"); return ""; } }, 373}); 374if (inspect({ value: 1 }, options) !== "{ value: 1 }" 375|| reads.join() !== "depth,colors") { 376throw new Error("inspect option reads"); 377} 378console.dir({ value: 1 }, options); 379if (reads.join() !== "depth,colors") throw new Error("dir invoked getters"); 380let converted = 0; 381console.count({ [Symbol.toPrimitive](hint) { 382if (hint !== "string") throw new Error("label hint"); 383converted++; 384return "label"; 385} }); 386if (converted !== 1) throw new Error("label read twice"); 387console.assert({ valueOf() { throw new Error("condition coerced"); } }, "hidden"); 388 389let iterated = 0, closed = 0; 390const columns = { 391[Symbol.iterator]() { 392if (this !== columns) throw new Error("iterator receiver"); 393iterated++; 394return { 395next() { return { done: false, value: Symbol("invalid") }; }, 396return() { closed++; return {}; }, 397}; 398}, 399}; 400let rejected = false; 401try { console.table([{ value: 1 }], columns); } 402catch (error) { if (!(error instanceof TypeError)) throw error; rejected = true; } 403if (!rejected || iterated !== 1 || closed !== 1) { 404throw new Error("table iterator cleanup"); 405} 406 407const previous = Error.prepareStackTrace; 408let formatted = 0; 409try { 410Error.prepareStackTrace = () => ({ 411toString() { formatted++; return "custom\ntrace"; }, 412}); 413console.trace("ignored by formatter"); 414} finally { Error.prepareStackTrace = previous; } 415if (formatted !== 1 || inspect.length !== 1 || console.dir.length !== 0 416|| console.count.length !== 0 || console.trace.length !== 0) { 417throw new Error("trace conversion or method arity"); 418} 419"# , 420& [], 421); 422assert_eq! ( output. stdout , b"{ value: 1 }\nlabel: 1\n" ); 423assert_eq! ( output. stderr , b"custom\ntrace\n" ); 424}