char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somport console extension to typescript1738b3c

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