mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Merge main
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
// Removing the last element with a statement-level pop() or `length = n`,
|
||||
// for every element kind, on dense arrays, arrays with holes and present
|
||||
// undefined values, sparse arrays and arrays shared between bindings.
|
||||
// Removed references are released; the remaining elements, the length and
|
||||
// later appends match Node.
|
||||
|
||||
class Item {
|
||||
readonly id: number;
|
||||
constructor(id: number) {
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
|
||||
function show(label: string, values: unknown[]): void {
|
||||
const parts: string[] = [];
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
if (!(i in values)) parts.push("<hole>");
|
||||
else {
|
||||
const v = values[i];
|
||||
parts.push(v instanceof Item ? `I${v.id}` : (JSON.stringify(v) ?? "undefined"));
|
||||
}
|
||||
}
|
||||
console.log(label, values.length, parts.join(" "));
|
||||
}
|
||||
|
||||
const numbers = [1.5, 2.5, 3.5, 4.5];
|
||||
numbers.pop();
|
||||
numbers.length = numbers.length - 1;
|
||||
numbers.push(9);
|
||||
show("numbers", numbers);
|
||||
|
||||
const flags = [true, false, true];
|
||||
flags.pop();
|
||||
flags.length = 1;
|
||||
flags.push(false);
|
||||
show("flags", flags);
|
||||
|
||||
const words = ["a", "b", "c", "d"];
|
||||
const alias = words;
|
||||
words.pop();
|
||||
alias.length = alias.length - 1;
|
||||
words.push("z");
|
||||
show("words", words);
|
||||
show("alias", alias);
|
||||
|
||||
const items = [new Item(1), new Item(2), new Item(3)];
|
||||
const kept = items[2]!;
|
||||
items.pop();
|
||||
items.length = 1;
|
||||
items.push(new Item(4));
|
||||
show("items", items);
|
||||
console.log("kept", kept.id);
|
||||
|
||||
// Holes and present undefined at the end.
|
||||
const holes: (number | undefined)[] = [1, 2];
|
||||
holes[4] = 5;
|
||||
holes.length = 4;
|
||||
show("holes trimmed", holes);
|
||||
holes.length = 3;
|
||||
show("holes trimmed again", holes);
|
||||
const undef: (string | undefined)[] = ["x", undefined, undefined];
|
||||
undef.pop();
|
||||
undef.length = undef.length - 1;
|
||||
show("undefined", undef);
|
||||
|
||||
// A sparse array: a far index then shrinking past it and back by one.
|
||||
const sparse: number[] = [1, 2, 3];
|
||||
sparse[2000000] = 4;
|
||||
sparse.length = 2000000;
|
||||
show("sparse cut", sparse.slice(0, 4));
|
||||
console.log("sparse length", sparse.length, 1999999 in sparse);
|
||||
sparse.length = sparse.length - 1;
|
||||
console.log("sparse length", sparse.length);
|
||||
|
||||
// A stack used the way a checker pushes and pops contextual entries.
|
||||
const stack: Item[] = [];
|
||||
const top: number[] = [];
|
||||
let checksum = 0;
|
||||
for (let i = 0; i < 20000; i++) {
|
||||
stack.push(new Item(i));
|
||||
top.push(i);
|
||||
if (i % 3 === 2) {
|
||||
stack.pop();
|
||||
top.length = top.length - 1;
|
||||
}
|
||||
if (stack.length > 50) {
|
||||
stack.length = stack.length - 1;
|
||||
top.pop();
|
||||
}
|
||||
checksum = (checksum + stack[stack.length - 1]!.id + top[top.length - 1]!) % 1000003;
|
||||
}
|
||||
console.log("stack", stack.length, top.length, checksum);
|
||||
|
||||
// Lengths that are not one less: unchanged, longer, invalid.
|
||||
const other = [1, 2, 3];
|
||||
other.length = 3;
|
||||
other.length = 5;
|
||||
show("longer", other);
|
||||
other.length = 1;
|
||||
show("shorter by many", other);
|
||||
try {
|
||||
other.length = -1;
|
||||
} catch (e) {
|
||||
console.log("invalid", (e as Error).name, other.length);
|
||||
}
|
||||
try {
|
||||
other.length = 0.5;
|
||||
} catch (e) {
|
||||
console.log("fraction", (e as Error).name, other.length);
|
||||
}
|
||||
const empty: number[] = [];
|
||||
empty.pop();
|
||||
empty.length = 0;
|
||||
show("empty", empty);
|
||||
|
||||
// pop() results: the last element, undefined for an empty array, a hole or a
|
||||
// present undefined, and values taken from sparse storage.
|
||||
const nums = [0.5, -0, 7];
|
||||
console.log("pop number", nums.pop(), Object.is(nums.pop(), -0), nums.pop(), nums.pop(), nums.length);
|
||||
const bools = [false, true];
|
||||
console.log("pop bool", bools.pop(), bools.pop(), bools.pop(), bools.length);
|
||||
const strs = ["p", "q"];
|
||||
const strAlias = strs;
|
||||
console.log("pop string", strs.pop(), strAlias.pop(), strs.pop(), strAlias.length);
|
||||
const objs = [new Item(7), new Item(8)];
|
||||
const popped = objs.pop();
|
||||
console.log("pop item", popped?.id, objs.pop()?.id, objs.pop(), objs.length);
|
||||
const gaps: number[] = [1];
|
||||
gaps[3] = 4;
|
||||
console.log("pop gaps", gaps.pop(), gaps.pop(), gaps.pop(), gaps.pop(), gaps.pop(), gaps.length);
|
||||
const maybe: (number | undefined)[] = [1, undefined];
|
||||
console.log("pop present undefined", maybe.pop(), maybe.length, maybe.pop(), maybe.length);
|
||||
const far: string[] = ["a"];
|
||||
far[100000] = "b";
|
||||
console.log("pop sparse", far.pop(), far.length, far.pop(), far.length);
|
||||
let popTotal = 0;
|
||||
const work: number[] = [];
|
||||
for (let i = 0; i < 10000; i++) {
|
||||
work.push(i, i + 1);
|
||||
popTotal += work.pop()!;
|
||||
if (i % 5 === 0) popTotal += work.pop() ?? -1;
|
||||
}
|
||||
console.log("pop loop", work.length, popTotal);
|
||||
@@ -0,0 +1,91 @@
|
||||
// The module's mock.timers outside a test: a debounce and a retry loop
|
||||
// driven by tick and runAll, the order due timers run in, intervals that
|
||||
// clear themselves, setTime, Node's errors, and real timers made before
|
||||
// enable() that keep running through the loop.
|
||||
import { mock } from "node:test";
|
||||
|
||||
function debounce(fn: (value: string) => void, ms: number): (value: string) => void {
|
||||
let pending: ReturnType<typeof setTimeout> | null = null;
|
||||
return (value: string) => {
|
||||
if (pending !== null) clearTimeout(pending);
|
||||
pending = setTimeout(() => {
|
||||
pending = null;
|
||||
fn(value);
|
||||
}, ms);
|
||||
};
|
||||
}
|
||||
|
||||
const real = setTimeout(() => console.log("real timer fired"), 1);
|
||||
|
||||
mock.timers.enable({ apis: ["setTimeout", "setInterval"] });
|
||||
clearTimeout(real); // the mocked clearTimeout leaves the real timer armed
|
||||
|
||||
const save = debounce((value) => console.log(`saved ${value}`), 100);
|
||||
save("a");
|
||||
mock.timers.tick(50);
|
||||
save("ab");
|
||||
mock.timers.tick(99);
|
||||
console.log("nothing saved yet");
|
||||
save("abc");
|
||||
mock.timers.tick(100);
|
||||
|
||||
let attempts = 0;
|
||||
function attempt(delay: number): void {
|
||||
attempts++;
|
||||
console.log(`attempt ${attempts} after ${delay}ms`);
|
||||
if (attempts < 4) setTimeout(() => attempt(delay * 2), delay * 2);
|
||||
}
|
||||
setTimeout(() => attempt(10), 10);
|
||||
mock.timers.runAll();
|
||||
console.log(`runAll stopped after ${attempts} attempts`);
|
||||
mock.timers.runAll();
|
||||
mock.timers.runAll();
|
||||
mock.timers.runAll();
|
||||
console.log(`attempts ${attempts}`);
|
||||
|
||||
const order: string[] = [];
|
||||
setTimeout(() => order.push("omitted"));
|
||||
setTimeout(() => order.push("zero"), 0);
|
||||
setTimeout(() => order.push("negative"), -1);
|
||||
setTimeout((tag: string) => order.push(tag), 0, "zero with argument");
|
||||
mock.timers.tick(0);
|
||||
console.log(order.join(", "));
|
||||
|
||||
let beats = 0;
|
||||
const heartbeat = setInterval(() => {
|
||||
beats++;
|
||||
if (beats === 5) clearInterval(heartbeat);
|
||||
}, 30);
|
||||
mock.timers.tick(1000);
|
||||
console.log(`beats ${beats}`);
|
||||
|
||||
const handle = setTimeout(() => console.log("refreshed timer fired once"), 20);
|
||||
console.log(handle.hasRef(), handle.unref().hasRef(), handle.ref().hasRef());
|
||||
handle.refresh();
|
||||
mock.timers.tick(20);
|
||||
mock.timers.tick(20);
|
||||
|
||||
mock.timers.setTime(1_000_000);
|
||||
setTimeout(() => console.log("fires after setTime"), 1);
|
||||
mock.timers.tick(1);
|
||||
|
||||
for (const run of [
|
||||
() => mock.timers.enable({ apis: ["setTimeout"] }),
|
||||
() => mock.timers.tick(-2),
|
||||
() => mock.timers.setTime(-0.5),
|
||||
]) {
|
||||
try {
|
||||
run();
|
||||
} catch (error) {
|
||||
const e = error as { name: string; code: string; message: string };
|
||||
console.log(`${e.name} ${e.code}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
mock.timers.reset();
|
||||
try {
|
||||
mock.timers.runAll();
|
||||
} catch (error) {
|
||||
console.log((error as Error).message);
|
||||
}
|
||||
setTimeout(() => console.log("real again after reset"), 30);
|
||||
@@ -0,0 +1,83 @@
|
||||
// The module's mock.timers for Date, setImmediate and AbortSignal.timeout:
|
||||
// a cache whose entries expire by Date.now(), a batch that flushes on
|
||||
// setImmediate, a request deadline from AbortSignal.timeout, and
|
||||
// enable() without options, which mocks every API.
|
||||
import { mock } from "node:test";
|
||||
|
||||
class TtlCache {
|
||||
private entries = new Map<string, { value: string; expires: number }>();
|
||||
private ttl: number;
|
||||
constructor(ttl: number) {
|
||||
this.ttl = ttl;
|
||||
}
|
||||
set(key: string, value: string): void {
|
||||
this.entries.set(key, { value, expires: Date.now() + this.ttl });
|
||||
}
|
||||
get(key: string): string | undefined {
|
||||
const entry = this.entries.get(key);
|
||||
if (entry === undefined) return undefined;
|
||||
if (Date.now() >= entry.expires) {
|
||||
this.entries.delete(key);
|
||||
return undefined;
|
||||
}
|
||||
return entry.value;
|
||||
}
|
||||
}
|
||||
|
||||
mock.timers.enable({ apis: ["Date"], now: new Date("2024-05-01T12:00:00Z") });
|
||||
console.log(new Date().toISOString());
|
||||
const cache = new TtlCache(60_000);
|
||||
cache.set("user", "ada");
|
||||
mock.timers.tick(59_999);
|
||||
console.log(cache.get("user"));
|
||||
mock.timers.tick(1);
|
||||
console.log(cache.get("user"));
|
||||
mock.timers.setTime(0);
|
||||
console.log(Date.now(), new Date().toISOString());
|
||||
mock.timers.reset();
|
||||
console.log(Date.now() > 1_700_000_000_000);
|
||||
|
||||
const batches: string[][] = [];
|
||||
let pending: string[] = [];
|
||||
function queue(item: string): void {
|
||||
if (pending.length === 0) {
|
||||
setImmediate(() => {
|
||||
batches.push(pending);
|
||||
pending = [];
|
||||
});
|
||||
}
|
||||
pending.push(item);
|
||||
}
|
||||
|
||||
mock.timers.enable({ apis: ["setImmediate"] });
|
||||
queue("a");
|
||||
queue("b");
|
||||
mock.timers.tick(0);
|
||||
console.log(JSON.stringify(batches));
|
||||
queue("c");
|
||||
queue("d");
|
||||
console.log(JSON.stringify(pending));
|
||||
mock.timers.tick(0);
|
||||
console.log(JSON.stringify(batches));
|
||||
const skipped = setImmediate(() => console.log("never"));
|
||||
clearImmediate(skipped);
|
||||
console.log(skipped.hasRef());
|
||||
mock.timers.runAll();
|
||||
mock.timers.reset();
|
||||
|
||||
mock.timers.enable();
|
||||
const deadline = AbortSignal.timeout(5_000);
|
||||
deadline.addEventListener("abort", () => {
|
||||
const reason = deadline.reason as { name: string; message: string };
|
||||
console.log(`aborted at ${Date.now()}: ${reason.name}: ${reason.message}`);
|
||||
});
|
||||
mock.timers.tick(4_999);
|
||||
console.log(deadline.aborted);
|
||||
mock.timers.tick(1);
|
||||
console.log(deadline.aborted);
|
||||
mock.timers.reset();
|
||||
|
||||
const real = AbortSignal.timeout(1);
|
||||
real.addEventListener("abort", () => {
|
||||
console.log(`real deadline: ${(real.reason as { name: string }).name}`);
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
// JSON.stringify over typed records whose members are checked-dynamic
|
||||
// values: the undeclared properties a checked record keeps from its source,
|
||||
// and the entries of an `unknown` index signature. Each member follows
|
||||
// SerializeJSONProperty: toJSON runs with the member's key, getters are
|
||||
// read, a Buffer becomes its { type, data } form, undefined, function and
|
||||
// symbol results drop, and a circular structure throws V8's TypeError.
|
||||
type Point = { a: number };
|
||||
type Bag = { a: number; [key: string]: unknown };
|
||||
|
||||
const report = (label: string, run: () => string | undefined): void => {
|
||||
try {
|
||||
console.log(label, run());
|
||||
} catch (error) {
|
||||
const e = error as Error;
|
||||
console.log(label, e.name);
|
||||
console.log(e.message);
|
||||
}
|
||||
};
|
||||
|
||||
const source: any = JSON.parse('{"a":1,"b":"two"}');
|
||||
source.stamp = { toJSON: (key: string) => `stamp:${key}` };
|
||||
source.gone = { toJSON: () => undefined };
|
||||
source.bytes = Buffer.from("hi");
|
||||
Object.defineProperty(source, "computed", { get: () => 42, enumerable: true });
|
||||
source.list = [1, { toJSON: (key: string) => `item:${key}` }];
|
||||
const point = source as Point;
|
||||
report("source", () => JSON.stringify(point));
|
||||
report("source pretty", () => JSON.stringify(point, null, 2));
|
||||
report("source nested", () => JSON.stringify({ point, n: 2 }));
|
||||
report("source array", () => JSON.stringify([point, point]));
|
||||
|
||||
const bag: Bag = { a: 1 };
|
||||
bag.stamp = { toJSON: (key: string) => `stamp:${key}` };
|
||||
bag.gone = { toJSON: () => undefined };
|
||||
bag.data = JSON.parse('{"x":[1,2,{"y":null}]}');
|
||||
bag.nothing = undefined;
|
||||
bag.order = JSON.parse('{"b":1,"2":"two","a":2,"0":"zero"}');
|
||||
const layered: any = JSON.parse('{"level":{"items":[1,{"x":null}]}}');
|
||||
layered.level.items.push({ toJSON: (key: string) => `deep:${key}` });
|
||||
bag.deep = layered;
|
||||
const hidden: any = JSON.parse('{"shown":1}');
|
||||
Object.defineProperty(hidden, "hidden", { value: 2, enumerable: false });
|
||||
bag.hidden = hidden;
|
||||
let nested: any = JSON.parse('{"end":true}');
|
||||
for (let i = 0; i < 40; i++) nested = { i, nested };
|
||||
bag.nested = nested;
|
||||
report("bag", () => JSON.stringify(bag));
|
||||
report("bag in array", () => JSON.stringify([bag]));
|
||||
|
||||
const self: any = JSON.parse('{"k":1}');
|
||||
self.self = self;
|
||||
const cyclic: any = JSON.parse('{"a":1,"x":{"k":2}}');
|
||||
cyclic.x.self = cyclic.x;
|
||||
report("source circular", () => JSON.stringify(cyclic as Point));
|
||||
const deep: any = JSON.parse('{"a":1,"x":{"y":{"z":{"w":{"v":{}}}}}}');
|
||||
deep.x.y.z.w.v.back = deep.x;
|
||||
report("source deep circular", () => JSON.stringify(deep as Point));
|
||||
const circular: Bag = { a: 1 };
|
||||
circular.loop = self;
|
||||
report("bag circular", () => JSON.stringify(circular));
|
||||
report("bag circular nested", () => JSON.stringify({ outer: circular }));
|
||||
|
||||
const shared: any = JSON.parse('{"v":1}');
|
||||
const twice: Bag = { a: 1, first: shared, second: shared };
|
||||
report("bag shared", () => JSON.stringify(twice));
|
||||
|
||||
const big: any = JSON.parse('{"a":1}');
|
||||
big.n = 1n;
|
||||
report("source bigint", () => JSON.stringify(big as Point));
|
||||
const throwing: Bag = { a: 1 };
|
||||
throwing.bad = {
|
||||
toJSON: () => {
|
||||
throw new RangeError("from toJSON");
|
||||
},
|
||||
};
|
||||
report("bag throwing", () => JSON.stringify(throwing));
|
||||
@@ -0,0 +1,57 @@
|
||||
// Date values converted into `unknown` slots stay Dates: typeof answers
|
||||
// "object", instanceof Date holds, util.inspect prints the ISO form or
|
||||
// "Invalid Date", JSON.stringify calls toJSON (null for an invalid date),
|
||||
// a method read through an untyped reference is a function that takes a
|
||||
// Date receiver, and the value converts back to a typed Date with the same
|
||||
// time value.
|
||||
import { inspect } from "node:util";
|
||||
|
||||
const epoch = new Date(0);
|
||||
const bare: unknown = epoch;
|
||||
const record: unknown = { when: new Date(86_400_000), n: 1 };
|
||||
const list: unknown = [new Date(Date.UTC(2024, 1, 29, 12, 30)), new Date(Number.NaN)];
|
||||
const bag: Record<string, unknown> = { at: epoch };
|
||||
|
||||
console.log(typeof bare, bare instanceof Date, inspect(bare), JSON.stringify(bare));
|
||||
console.log(record, JSON.stringify(record), Object.keys(record as object));
|
||||
console.log(list, JSON.stringify(list));
|
||||
console.log(bag, JSON.stringify(bag));
|
||||
|
||||
const when = (record as { when: unknown }).when;
|
||||
console.log(typeof when, when instanceof Date);
|
||||
if (when instanceof Date) console.log(when.getTime(), when.toISOString(), when.getUTCDate());
|
||||
const back = bare as Date;
|
||||
console.log(back.getTime(), back.toISOString());
|
||||
const dynamic = record as any;
|
||||
console.log(dynamic.when.getTime(), dynamic.when.getUTCFullYear(), String(dynamic.when instanceof Date));
|
||||
const read = dynamic.when.getUTCHours;
|
||||
console.log(typeof read, read.name, read.call(dynamic.when), dynamic.when.missing);
|
||||
try {
|
||||
read.call({});
|
||||
} catch (error) {
|
||||
console.log((error as Error).name, (error as Error).message);
|
||||
}
|
||||
|
||||
type Event = { at: Date; tags: string[] };
|
||||
const events: Event[] = [
|
||||
{ at: new Date(0), tags: ["a"] },
|
||||
{ at: new Date(7), tags: [] },
|
||||
];
|
||||
const eventsUnknown: unknown = events;
|
||||
console.log(eventsUnknown, JSON.stringify(eventsUnknown));
|
||||
console.log((eventsUnknown as Event[])[1]!.at.toISOString());
|
||||
class Holder {
|
||||
value: unknown = null;
|
||||
}
|
||||
const holder = new Holder();
|
||||
holder.value = new Date(1000);
|
||||
console.log(holder, JSON.stringify(holder.value));
|
||||
const invalid: any = { d: new Date(Number.NaN) } as unknown;
|
||||
try {
|
||||
console.log(invalid.d.toISOString());
|
||||
} catch (error) {
|
||||
console.log((error as Error).name, (error as Error).message);
|
||||
}
|
||||
console.log(invalid.d.getTime(), JSON.stringify(invalid));
|
||||
const stamped: Readonly<Date> = new Date(0);
|
||||
console.log(stamped.getTime(), stamped.toISOString());
|
||||
@@ -0,0 +1,28 @@
|
||||
// events.defaultMaxListeners through the module's ES module namespace and
|
||||
// named imports reads the namespace's copy, which keeps the value from
|
||||
// program start until module.syncBuiltinESMExports() refreshes it. The
|
||||
// default import and the EventEmitter class read the current default.
|
||||
import EventEmitterDefault, { defaultMaxListeners, EventEmitter, setMaxListeners } from "node:events";
|
||||
import * as events from "node:events";
|
||||
import { syncBuiltinESMExports } from "node:module";
|
||||
|
||||
const show = (label: string): void => {
|
||||
console.log(
|
||||
label,
|
||||
events.defaultMaxListeners,
|
||||
defaultMaxListeners,
|
||||
EventEmitterDefault.defaultMaxListeners,
|
||||
EventEmitter.defaultMaxListeners,
|
||||
);
|
||||
};
|
||||
show("start");
|
||||
setMaxListeners(20);
|
||||
show("after setMaxListeners");
|
||||
EventEmitter.defaultMaxListeners = 7;
|
||||
show("after assignment");
|
||||
syncBuiltinESMExports();
|
||||
show("after sync");
|
||||
events.setMaxListeners(3);
|
||||
show("after namespace setMaxListeners");
|
||||
const emitter = new EventEmitter();
|
||||
console.log(emitter.getMaxListeners(), events.getMaxListeners(emitter));
|
||||
@@ -0,0 +1,10 @@
|
||||
// CommonJS bindings of node:events read defaultMaxListeners from the module
|
||||
// object, which is the EventEmitter class: every read sees the current
|
||||
// default.
|
||||
const events = require("node:events");
|
||||
const { EventEmitter, setMaxListeners } = require("node:events");
|
||||
console.log(events.defaultMaxListeners, EventEmitter.defaultMaxListeners);
|
||||
setMaxListeners(15);
|
||||
console.log(events.defaultMaxListeners, EventEmitter.defaultMaxListeners, require("events").defaultMaxListeners);
|
||||
events.defaultMaxListeners = 4;
|
||||
console.log(events.defaultMaxListeners, EventEmitter.defaultMaxListeners, new EventEmitter().getMaxListeners());
|
||||
@@ -0,0 +1,48 @@
|
||||
// Errors held as `any` convert to strings through Error.prototype.toString
|
||||
// in every dynamic conversion: template literals, `+` with a string on
|
||||
// either side, String(), Array join, loose equality, Number() and property
|
||||
// keys. The name is read through the prototype ("Error", "TypeError", a
|
||||
// subclass's own name), an empty name or message leaves the other alone,
|
||||
// and a subclass without its own name inherits "Error".
|
||||
|
||||
class NamedError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "NamedError";
|
||||
}
|
||||
}
|
||||
|
||||
class PlainSubclass extends Error {}
|
||||
|
||||
function show(label: string, error: any): void {
|
||||
console.log(label, "|", `${error}`, "|", "" + error, "|", error + "!", "|", String(error), "|", [error, 1].join(" "));
|
||||
}
|
||||
|
||||
show("error", new Error("plain"));
|
||||
show("type", new TypeError("bad thing"));
|
||||
show("range", new RangeError("too far"));
|
||||
show("syntax", new SyntaxError("unexpected"));
|
||||
show("named", new NamedError("mine"));
|
||||
show("subclass", new PlainSubclass("inherited name"));
|
||||
show("empty message", new TypeError(""));
|
||||
show("no message", new Error());
|
||||
|
||||
const renamed: any = new Error("message only");
|
||||
renamed.name = "";
|
||||
show("empty name", renamed);
|
||||
|
||||
const changed: any = new RangeError("before");
|
||||
changed.message = "after";
|
||||
show("changed message", changed);
|
||||
|
||||
const numeric: any = new Error("42");
|
||||
numeric.name = "";
|
||||
const unnumbered: any = new TypeError("x");
|
||||
console.log("Number", Number(numeric), Number(unnumbered), +numeric);
|
||||
|
||||
const type: any = new TypeError("bad thing");
|
||||
console.log("loose equality", type == "TypeError: bad thing", type == "bad thing");
|
||||
|
||||
const keyed: any = {};
|
||||
keyed[type] = 1;
|
||||
console.log("key", Object.keys(keyed).join(","));
|
||||
@@ -0,0 +1,9 @@
|
||||
// typeof on functions and classes exported by Node builtin modules, read
|
||||
// through require bindings: Node answers "function" for each.
|
||||
const zlib = require("node:zlib");
|
||||
const path = require("node:path");
|
||||
const fs = require("node:fs");
|
||||
console.log(typeof zlib.createGzip, typeof zlib.createBrotliDecompress, typeof zlib.gunzipSync);
|
||||
console.log(typeof zlib.Inflate, typeof zlib.Gzip, typeof zlib.DeflateRaw);
|
||||
console.log(typeof path.join, typeof path.posix.join, typeof fs.readFileSync);
|
||||
console.log(typeof path.sep);
|
||||
@@ -0,0 +1,59 @@
|
||||
// markAsUncloneable: structuredClone and postMessage throw DataCloneError
|
||||
// for a marked object, error or class instance, also nested in an object;
|
||||
// the mark has no effect on primitives, arrays and typed arrays.
|
||||
import { markAsUncloneable, MessageChannel, isInternalThread } from "node:worker_threads";
|
||||
|
||||
console.log("isInternalThread", isInternalThread);
|
||||
const { port1, port2 } = new MessageChannel();
|
||||
port2.on("message", (m) => console.log("received", JSON.stringify(m)));
|
||||
|
||||
class Point {
|
||||
constructor(x) {
|
||||
this.x = x;
|
||||
}
|
||||
}
|
||||
const cases = {
|
||||
object: { a: 1 },
|
||||
error: new Error("e"),
|
||||
instance: new Point(1),
|
||||
array: [1, 2],
|
||||
bytes: new Uint8Array(2),
|
||||
};
|
||||
for (const [name, value] of Object.entries(cases)) {
|
||||
markAsUncloneable(value);
|
||||
let cloned;
|
||||
let posted;
|
||||
try {
|
||||
structuredClone(value);
|
||||
cloned = "cloned";
|
||||
} catch (e) {
|
||||
cloned = `${e.name} ${e.message} ${e.code}`;
|
||||
}
|
||||
try {
|
||||
port1.postMessage(value);
|
||||
posted = "posted";
|
||||
} catch (e) {
|
||||
posted = `${e.name} ${e.message}`;
|
||||
}
|
||||
console.log(name, "|", cloned, "|", posted);
|
||||
}
|
||||
markAsUncloneable(5);
|
||||
markAsUncloneable("text");
|
||||
markAsUncloneable(undefined);
|
||||
console.log("primitives", structuredClone(5), structuredClone("text"));
|
||||
const marked = { inner: true };
|
||||
markAsUncloneable(marked);
|
||||
markAsUncloneable(marked);
|
||||
try {
|
||||
structuredClone({ nested: marked });
|
||||
} catch (e) {
|
||||
console.log("nested", e.name, e.message);
|
||||
}
|
||||
try {
|
||||
port1.postMessage({ nested: marked });
|
||||
} catch (e) {
|
||||
console.log("nested post", e.name, e.message);
|
||||
}
|
||||
const copy = { ...marked };
|
||||
console.log("spread copy", JSON.stringify(structuredClone(copy)));
|
||||
setTimeout(() => port1.close(), 50);
|
||||
@@ -0,0 +1,25 @@
|
||||
// worker[Symbol.asyncDispose]() terminates the worker and resolves with
|
||||
// undefined after its 'exit' event; `await using` disposes a Worker at the
|
||||
// end of its block.
|
||||
import { Worker, isMainThread, parentPort } from "node:worker_threads";
|
||||
|
||||
if (isMainThread) {
|
||||
const worker = new Worker(new URL(import.meta.url));
|
||||
const dispose = worker[Symbol.asyncDispose];
|
||||
console.log(typeof dispose, dispose.name, dispose.length);
|
||||
worker.on("exit", (code) => console.log("exit", code));
|
||||
await new Promise((resolve) => worker.once("message", resolve));
|
||||
const result = await worker[Symbol.asyncDispose]();
|
||||
console.log("disposed", result);
|
||||
console.log("again", await worker[Symbol.asyncDispose]());
|
||||
{
|
||||
await using scoped = new Worker(new URL(import.meta.url));
|
||||
scoped.on("exit", (code) => console.log("scoped exit", code));
|
||||
await new Promise((resolve) => scoped.once("message", resolve));
|
||||
console.log("leaving block");
|
||||
}
|
||||
console.log("after block");
|
||||
} else {
|
||||
parentPort.postMessage("ready");
|
||||
setInterval(() => {}, 1000);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Classes whose base is an ordinary function constructor, with a declared
|
||||
// constructor calling super and with the implicit derived constructor.
|
||||
function Shape(name, sides) {
|
||||
this.name = name;
|
||||
this.sides = sides;
|
||||
}
|
||||
Shape.prototype.describe = function () {
|
||||
return `${this.name} with ${this.sides} sides`;
|
||||
};
|
||||
|
||||
class Polygon extends Shape {
|
||||
constructor(name, sides, length) {
|
||||
super(name, sides);
|
||||
this.length = length;
|
||||
}
|
||||
perimeter() {
|
||||
return this.sides * this.length;
|
||||
}
|
||||
}
|
||||
|
||||
class Unnamed extends Shape {}
|
||||
|
||||
const square = new Polygon("square", 4, 3);
|
||||
console.log(square.describe(), square.perimeter());
|
||||
console.log(square instanceof Polygon);
|
||||
const shape = new Unnamed("triangle", 3);
|
||||
console.log(shape.describe(), Object.keys(shape).join(","));
|
||||
@@ -0,0 +1,102 @@
|
||||
import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads";
|
||||
import { publish } from "@scriptc/threads";
|
||||
import type { ProgramLifetime } from "@scriptc/types";
|
||||
|
||||
// Workers whose memory goes back when they exit, and workers whose memory
|
||||
// stays because other threads hold what they allocated. Builder workers
|
||||
// create ProgramLifetime instances (which scriptc never frees), ordinary
|
||||
// objects, strings and large typed arrays, post a checksum and exit. A
|
||||
// publisher worker posts published graphs (a class graph and a plain
|
||||
// object) and exits. The main thread reads those graphs again after each
|
||||
// wave of builders, which in scriptc on Linux reuse the memory of the
|
||||
// builders before them.
|
||||
|
||||
class Node implements ProgramLifetime {
|
||||
readonly kind: number;
|
||||
readonly label: string;
|
||||
readonly children: Node[] = [];
|
||||
parent: Node | undefined = undefined;
|
||||
constructor(kind: number, label: string) {
|
||||
this.kind = kind;
|
||||
this.label = label;
|
||||
}
|
||||
}
|
||||
|
||||
type Summary = { readonly name: string; readonly values: readonly number[]; readonly nested: { readonly total: number } };
|
||||
|
||||
type Job = { readonly role: string; readonly seed: number };
|
||||
|
||||
function build(seed: number, count: number): Node {
|
||||
const root = new Node(seed, "root" + seed);
|
||||
let parent = root;
|
||||
for (let i = 0; i < count; i++) {
|
||||
const node = new Node((i * 7 + seed) % 31, "n" + (i % 101));
|
||||
node.parent = parent;
|
||||
parent.children.push(node);
|
||||
if (i % 64 === 63) parent = node;
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
function checksum(root: Node): number {
|
||||
let sum = 0;
|
||||
const stack: Node[] = [root];
|
||||
while (stack.length > 0) {
|
||||
const node = stack.pop()!;
|
||||
sum = (sum * 31 + node.kind + node.label.length + node.children.length) % 1_000_003;
|
||||
for (const child of node.children) stack.push(child);
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
function startWorker(job: Job, onMessage: (value: unknown) => void): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const worker = new Worker(new URL(import.meta.url), { workerData: job });
|
||||
worker.on("message", onMessage);
|
||||
worker.once("messageerror", reject);
|
||||
worker.once("error", reject);
|
||||
worker.once("exit", () => resolve());
|
||||
});
|
||||
}
|
||||
|
||||
if (isMainThread) {
|
||||
let tree: Node | undefined = undefined;
|
||||
let summary: Summary | undefined = undefined;
|
||||
// The publisher posts the tree, then the summary.
|
||||
await startWorker({ role: "publisher", seed: 3 }, (value) => {
|
||||
if (tree === undefined) tree = value as Node;
|
||||
else summary = value as Summary;
|
||||
});
|
||||
const describe = (): string =>
|
||||
`tree ${checksum(tree!)} summary ${summary!.name} ${summary!.values.join(",")} ${summary!.nested.total}`;
|
||||
console.log("published", describe());
|
||||
for (let wave = 0; wave < 3; wave++) {
|
||||
const sums: number[] = [];
|
||||
await Promise.all(
|
||||
[1, 2, 3, 4].map((k) => startWorker({ role: "builder", seed: wave * 10 + k }, (value) => sums.push(value as number))),
|
||||
);
|
||||
sums.sort((a, b) => a - b);
|
||||
console.log("wave", wave, sums.join(" "), describe());
|
||||
}
|
||||
} else {
|
||||
const job = workerData as Job;
|
||||
if (job.role === "publisher") {
|
||||
parentPort!.postMessage(publish(build(job.seed, 20_000)));
|
||||
const values: number[] = [];
|
||||
for (let i = 0; i < 50; i++) values.push(i * job.seed);
|
||||
parentPort!.postMessage(publish({ name: "summary" + job.seed, values, nested: { total: values.length } }));
|
||||
} else {
|
||||
const root = build(job.seed, 150_000);
|
||||
const samples: Float64Array[] = [];
|
||||
for (let i = 0; i < 40; i++) {
|
||||
const sample = new Float64Array(8192);
|
||||
for (let j = 0; j < sample.length; j += 97) sample[j] = i + j + job.seed;
|
||||
samples.push(sample);
|
||||
}
|
||||
let extra = 0;
|
||||
for (const sample of samples) extra = (extra + sample[97]! + sample[8148]!) % 1_000_003;
|
||||
const words: string[] = [];
|
||||
for (let i = 0; i < 20_000; i++) words.push(`word-${i % 977}-${job.seed}`);
|
||||
parentPort!.postMessage((checksum(root) + extra + words.join("").length) % 1_000_003);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// mock.timers lowers enable(), enable({ apis, now }), tick, runAll, reset,
|
||||
// setTime and [Symbol.dispose](), on the module's `mock` and on t.mock, and
|
||||
// timers/promises scheduler.wait(delay) lowers. The rest is refused, each
|
||||
// with a named diagnostic: an apis list or options object that is not a
|
||||
// literal, and the MockTimers object used as a value.
|
||||
import { mock, test } from "node:test";
|
||||
import { scheduler } from "node:timers/promises";
|
||||
|
||||
mock.timers.enable({ apis: ["setTimeout", "setInterval"], now: 10 }); // lowers
|
||||
mock.timers.tick(5); // lowers
|
||||
mock.timers.setTime(20); // lowers
|
||||
mock.timers.runAll(); // lowers
|
||||
mock.timers.reset(); // lowers
|
||||
|
||||
mock.timers.enable(); // lowers
|
||||
mock.timers.enable({ apis: ["Date"] }); // lowers
|
||||
mock.timers.enable({ apis: ["setTimeout", "setImmediate"] }); // lowers
|
||||
|
||||
const apis: ("setTimeout" | "setInterval")[] = ["setTimeout"];
|
||||
mock.timers.enable({ apis }); // apis list that is not a literal
|
||||
|
||||
const options = { apis: ["setTimeout"] as const };
|
||||
mock.timers.enable(options); // options object that is not a literal
|
||||
|
||||
const timers = mock.timers; // the object as a value
|
||||
timers.tick(1);
|
||||
|
||||
test("t.mock.timers", (t) => {
|
||||
t.mock.timers.enable({ apis: ["setInterval"] }); // lowers
|
||||
t.mock.timers.tick(); // lowers
|
||||
t.mock.timers.enable({ apis: ["setTimeout"], now: new Date(0) }); // lowers
|
||||
t.mock.timers[Symbol.dispose](); // lowers
|
||||
});
|
||||
|
||||
void scheduler.wait(1); // lowers
|
||||
@@ -0,0 +1,19 @@
|
||||
// t.assert.snapshot and t.assert.fileSnapshot lower with no options and
|
||||
// with an options object literal whose serializers is an array literal.
|
||||
// The rest is refused, each with a named diagnostic: options that are not
|
||||
// an object literal, a serializers list that is not an array literal, and
|
||||
// a spread serializer.
|
||||
import { test } from "node:test";
|
||||
|
||||
test("snapshots", (t) => {
|
||||
t.assert.snapshot({ a: 1 }); // lowers
|
||||
t.assert.snapshot(1, { serializers: [(v: number) => `${v}`] }); // lowers
|
||||
t.assert.fileSnapshot("text", "out.txt"); // lowers
|
||||
|
||||
const options = { serializers: [(v: unknown) => String(v)] };
|
||||
t.assert.snapshot(1, options); // options that are not an object literal
|
||||
|
||||
const serializers = [(v: unknown) => String(v)];
|
||||
t.assert.snapshot(1, { serializers }); // a serializers list that is not an array literal
|
||||
t.assert.fileSnapshot(1, "out.txt", { serializers: [...serializers] }); // a spread serializer
|
||||
});
|
||||
@@ -7,9 +7,9 @@
|
||||
// differential corpus); what stays rejected:
|
||||
// - symbol-keyed index signatures (no lowering for symbol keys);
|
||||
// - index-signature value types outside the overflow-map slice (Dates and
|
||||
// other lib API objects, even when the value itself has a representation);
|
||||
// - utility types over LIB interfaces stay a type world, not data shapes.
|
||||
// (Dot access/writes to index-signature keys compile now — the overflow
|
||||
// other lib API objects, even when the value itself has a representation).
|
||||
// (Readonly<Date> bindings compile, holding the Date as a checked value.
|
||||
// Dot access/writes to index-signature keys compile now — the overflow
|
||||
// path in dot spelling — and self-referential mapped types intern as
|
||||
// named recursive shapes; differential corpus covers both.)
|
||||
const bySymbol: { [s: symbol]: string } = {};
|
||||
|
||||
+197
@@ -0,0 +1,197 @@
|
||||
// mock.timers for Date, setImmediate, AbortSignal.timeout and
|
||||
// timers/promises scheduler.wait, enable() without apis, which mocks every
|
||||
// API, and [Symbol.dispose](): Date.now() and new Date() read the mock
|
||||
// clock that tick and setTime move, a mocked setImmediate runs before
|
||||
// timers due at the same tick, a mocked AbortSignal.timeout aborts with
|
||||
// the AbortError reason, mocking Date twice throws, and once a reset gives
|
||||
// scheduler.wait back, Node's restored scheduler.wait throws
|
||||
// ERR_INVALID_THIS, so its suite runs first.
|
||||
import { describe, it, mock } from "node:test";
|
||||
import * as assert from "node:assert";
|
||||
import * as timersNamespace from "node:timers";
|
||||
import { setImmediate as namedSetImmediate } from "node:timers";
|
||||
import { scheduler } from "node:timers/promises";
|
||||
import * as timersPromises from "node:timers/promises";
|
||||
|
||||
type Fail = { name: string; code?: string; message: string };
|
||||
const failure = (fn: () => void): string => {
|
||||
try {
|
||||
fn();
|
||||
return "no throw";
|
||||
} catch (error) {
|
||||
const e = error as Fail;
|
||||
return `${e.name} ${e.code ?? "-"} ${e.message}`;
|
||||
}
|
||||
};
|
||||
|
||||
describe("scheduler.wait", () => {
|
||||
it("settles from the mock queue, a microtask after tick", async (t) => {
|
||||
// scheduler.wait is not in the typed apis list; enable() mocks it.
|
||||
t.mock.timers.enable();
|
||||
const log: string[] = [];
|
||||
const named = scheduler.wait(50).then(() => {
|
||||
log.push("named");
|
||||
});
|
||||
const namespace = timersPromises.scheduler.wait(50).then(() => {
|
||||
log.push("namespace");
|
||||
});
|
||||
const omitted = scheduler.wait().then(() => {
|
||||
log.push("omitted");
|
||||
});
|
||||
const unref = scheduler.wait(50, { ref: false }).then(() => {
|
||||
log.push("unref");
|
||||
});
|
||||
t.mock.timers.tick(50);
|
||||
log.push("after tick");
|
||||
queueMicrotask(() => log.push("microtask"));
|
||||
await Promise.all([named, namespace, omitted, unref]);
|
||||
assert.deepStrictEqual(log, [
|
||||
"after tick",
|
||||
"microtask",
|
||||
"named",
|
||||
"namespace",
|
||||
"omitted",
|
||||
"unref",
|
||||
]);
|
||||
});
|
||||
|
||||
it("throws once a reset gave it back", () => {
|
||||
const got = failure(() => {
|
||||
void scheduler.wait(1);
|
||||
});
|
||||
assert.strictEqual(
|
||||
got,
|
||||
'TypeError ERR_INVALID_THIS Value of "this" must be of type Scheduler',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Date", () => {
|
||||
it("reads the mock clock", (t) => {
|
||||
t.mock.timers.enable({ apis: ["Date"], now: 1000 });
|
||||
assert.strictEqual(Date.now(), 1000);
|
||||
assert.strictEqual(new Date().getTime(), 1000);
|
||||
assert.strictEqual(new Date(5).getTime(), 5);
|
||||
assert.strictEqual(new Date() instanceof Date, true);
|
||||
t.mock.timers.tick(5);
|
||||
assert.strictEqual(Date.now(), 1005);
|
||||
t.mock.timers.setTime(50);
|
||||
assert.strictEqual(new Date().toISOString(), "1970-01-01T00:00:00.050Z");
|
||||
});
|
||||
|
||||
it("is real again after reset", (t) => {
|
||||
t.mock.timers.enable({ apis: ["Date"] });
|
||||
assert.strictEqual(Date.now(), 0);
|
||||
t.mock.timers.reset();
|
||||
assert.strictEqual(Date.now() > 1e12, true);
|
||||
});
|
||||
|
||||
it("shares the clock with mocked timers", (t) => {
|
||||
t.mock.timers.enable({ apis: ["Date", "setTimeout"], now: 100 });
|
||||
const seen: number[] = [];
|
||||
setTimeout(() => seen.push(Date.now()), 25);
|
||||
t.mock.timers.tick(30);
|
||||
seen.push(Date.now());
|
||||
assert.deepStrictEqual(seen, [130, 130]);
|
||||
});
|
||||
|
||||
it("throws when another tracker mocks Date", (t) => {
|
||||
mock.timers.enable({ apis: ["Date"], now: 7 });
|
||||
const got = failure(() => t.mock.timers.enable({ apis: ["Date"] }));
|
||||
assert.strictEqual(got, "Error ERR_INVALID_STATE Invalid state: Date is already being mocked!");
|
||||
assert.strictEqual(failure(() => t.mock.timers.tick(1)).includes("enable MockTimers first"), true);
|
||||
assert.strictEqual(Date.now(), 7);
|
||||
mock.timers.reset();
|
||||
});
|
||||
});
|
||||
|
||||
describe("setImmediate", () => {
|
||||
it("runs before timers due at the same tick", (t) => {
|
||||
t.mock.timers.enable({ apis: ["setImmediate", "setTimeout"] });
|
||||
const log: string[] = [];
|
||||
setTimeout(() => log.push("timeout 0"), 0);
|
||||
setImmediate(() => log.push("immediate"));
|
||||
setImmediate((tag: string) => log.push(`immediate ${tag}`), "x");
|
||||
const cleared = setImmediate(() => log.push("cleared"));
|
||||
clearImmediate(cleared);
|
||||
assert.strictEqual(cleared.hasRef(), true);
|
||||
assert.strictEqual(cleared.unref().hasRef(), true);
|
||||
assert.strictEqual(cleared.ref().hasRef(), true);
|
||||
t.mock.timers.tick(0);
|
||||
log.push("tick 0");
|
||||
assert.deepStrictEqual(log, ["immediate", "immediate x", "timeout 0", "tick 0"]);
|
||||
});
|
||||
|
||||
it("an immediate queued by a due timeout runs that timeout again", (t) => {
|
||||
// Node removes a fired timer only when it is still first in the queue;
|
||||
// the immediate's run time (now - 1) puts it first.
|
||||
t.mock.timers.enable({ apis: ["setImmediate", "setTimeout"] });
|
||||
const log: string[] = [];
|
||||
setTimeout(() => {
|
||||
log.push("timeout");
|
||||
if (log.length === 1) setImmediate(() => log.push("immediate"));
|
||||
}, 0);
|
||||
t.mock.timers.tick(0);
|
||||
assert.deepStrictEqual(log, ["timeout", "immediate", "timeout"]);
|
||||
});
|
||||
|
||||
it("named and namespace imports keep the loop's immediates", async (t) => {
|
||||
t.mock.timers.enable({ apis: ["setImmediate"] });
|
||||
const log: string[] = [];
|
||||
await new Promise<void>((resolve) =>
|
||||
namedSetImmediate(() => {
|
||||
log.push("named");
|
||||
resolve();
|
||||
}),
|
||||
);
|
||||
await new Promise<void>((resolve) =>
|
||||
timersNamespace.setImmediate(() => {
|
||||
log.push("namespace");
|
||||
resolve();
|
||||
}),
|
||||
);
|
||||
assert.deepStrictEqual(log, ["named", "namespace"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("enable() without apis", () => {
|
||||
it("mocks every API", (t) => {
|
||||
t.mock.timers.enable();
|
||||
const log: string[] = [];
|
||||
assert.strictEqual(Date.now(), 0);
|
||||
setTimeout(() => log.push(`timeout at ${Date.now()}`), 2);
|
||||
setImmediate(() => log.push("immediate"));
|
||||
const beat = setInterval(() => {
|
||||
log.push(`interval at ${Date.now()}`);
|
||||
clearInterval(beat);
|
||||
}, 1);
|
||||
const signal = AbortSignal.timeout(3);
|
||||
signal.addEventListener("abort", () => {
|
||||
const reason = signal.reason as { name: string; message: string };
|
||||
log.push(`abort ${reason.name}: ${reason.message}`);
|
||||
});
|
||||
t.mock.timers.tick(3);
|
||||
assert.strictEqual(signal.aborted, true);
|
||||
assert.deepStrictEqual(log, [
|
||||
"immediate",
|
||||
"interval at 3",
|
||||
"timeout at 3",
|
||||
"abort AbortError: This operation was aborted",
|
||||
]);
|
||||
});
|
||||
|
||||
it("[Symbol.dispose]() resets", (t) => {
|
||||
t.mock.timers.enable({ apis: ["setTimeout"] });
|
||||
let fired = 0;
|
||||
setTimeout(() => fired++, 1);
|
||||
t.mock.timers[Symbol.dispose]();
|
||||
assert.strictEqual(failure(() => t.mock.timers.tick(1)).includes("enable MockTimers first"), true);
|
||||
assert.strictEqual(fired, 0);
|
||||
});
|
||||
|
||||
it("mocks nothing with an empty apis list", (t) => {
|
||||
t.mock.timers.enable({ apis: [] });
|
||||
assert.strictEqual(Date.now() > 1e12, true);
|
||||
t.mock.timers.tick(1);
|
||||
});
|
||||
});
|
||||
+348
@@ -0,0 +1,348 @@
|
||||
// mock.timers on the module's `mock` and on t.mock, for setTimeout and
|
||||
// setInterval: enable({ apis, now }), tick, runAll, reset and setTime, the
|
||||
// order due timers run in (equal, zero, negative, fractional and omitted
|
||||
// delays), clears, handle methods, Node's errors, and the reset of
|
||||
// t.mock.timers when its test ends. Node replaces the globals, so calls
|
||||
// through ESM named and namespace imports of node:timers and
|
||||
// node:timers/promises keep the loop's timers.
|
||||
import { describe, it, mock, test } from "node:test";
|
||||
import * as assert from "node:assert";
|
||||
import * as timersNamespace from "node:timers";
|
||||
import { setTimeout as namedSetTimeout } from "node:timers";
|
||||
import { setTimeout as sleep } from "node:timers/promises";
|
||||
|
||||
type Fail = { name: string; code?: string; message: string };
|
||||
const failure = (fn: () => void): string => {
|
||||
try {
|
||||
fn();
|
||||
return "no throw";
|
||||
} catch (error) {
|
||||
const e = error as Fail;
|
||||
return `${e.name} ${e.code ?? "-"} ${e.message}`;
|
||||
}
|
||||
};
|
||||
const NOT_ENABLED =
|
||||
"Error ERR_INVALID_STATE Invalid state: You should enable MockTimers first by calling the .enable function";
|
||||
const negative = (n: string) =>
|
||||
`TypeError ERR_INVALID_ARG_VALUE The argument 'time' ${n}. Received 'positive integer'`;
|
||||
|
||||
describe("order", () => {
|
||||
it("runs due timers by run time, then creation", (t) => {
|
||||
t.mock.timers.enable({ apis: ["setTimeout"] });
|
||||
const log: string[] = [];
|
||||
setTimeout(() => log.push("omitted"));
|
||||
setTimeout(() => log.push("zero"), 0);
|
||||
setTimeout(() => log.push("negative"), -5);
|
||||
setTimeout(() => log.push("fraction"), 0.5);
|
||||
setTimeout(() => log.push("ten a"), 10);
|
||||
setTimeout(() => log.push("five"), 5);
|
||||
setTimeout(() => log.push("ten b"), 10);
|
||||
t.mock.timers.tick(0);
|
||||
log.push("tick 0");
|
||||
t.mock.timers.tick(1);
|
||||
log.push("tick 1");
|
||||
t.mock.timers.tick(9);
|
||||
assert.deepStrictEqual(log, [
|
||||
"omitted",
|
||||
"negative",
|
||||
"zero",
|
||||
"tick 0",
|
||||
"fraction",
|
||||
"tick 1",
|
||||
"five",
|
||||
"ten a",
|
||||
"ten b",
|
||||
]);
|
||||
});
|
||||
|
||||
it("runs timers scheduled by a callback once they are due", (t) => {
|
||||
t.mock.timers.enable({ apis: ["setTimeout"] });
|
||||
const log: string[] = [];
|
||||
setTimeout(() => {
|
||||
log.push("outer");
|
||||
setTimeout(() => log.push("inner 0"), 0);
|
||||
setTimeout(() => log.push("inner 5"), 5);
|
||||
}, 10);
|
||||
t.mock.timers.tick(10);
|
||||
log.push("after 10");
|
||||
t.mock.timers.tick(5);
|
||||
log.push("after 15");
|
||||
t.mock.timers.tick();
|
||||
assert.deepStrictEqual(log, ["outer", "inner 0", "after 10", "inner 5", "after 15"]);
|
||||
});
|
||||
|
||||
it("passes the trailing arguments", (t) => {
|
||||
t.mock.timers.enable({ apis: ["setTimeout"] });
|
||||
const log: string[] = [];
|
||||
setTimeout((a: string, b: number) => log.push(`${a} ${b}`), 1, "x", 2);
|
||||
t.mock.timers.tick(1);
|
||||
assert.deepStrictEqual(log, ["x 2"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("intervals", () => {
|
||||
it("repeats until cleared", (t) => {
|
||||
t.mock.timers.enable({ apis: ["setInterval", "setTimeout"] });
|
||||
const log: string[] = [];
|
||||
let n = 0;
|
||||
const handle = setInterval(() => {
|
||||
n++;
|
||||
log.push(`interval ${n}`);
|
||||
if (n === 3) clearInterval(handle);
|
||||
}, 10);
|
||||
setTimeout(() => log.push("timeout 25"), 25);
|
||||
t.mock.timers.tick(100);
|
||||
assert.deepStrictEqual(log, ["interval 1", "interval 2", "timeout 25", "interval 3"]);
|
||||
});
|
||||
|
||||
it("fires once per elapsed period", (t) => {
|
||||
t.mock.timers.enable({ apis: ["setInterval"] });
|
||||
let n = 0;
|
||||
const handle = setInterval(() => n++, 3);
|
||||
t.mock.timers.tick(10);
|
||||
assert.strictEqual(n, 3);
|
||||
clearInterval(handle);
|
||||
t.mock.timers.tick(10);
|
||||
assert.strictEqual(n, 3);
|
||||
});
|
||||
|
||||
it("clears from another callback", (t) => {
|
||||
t.mock.timers.enable({ apis: ["setInterval", "setTimeout"] });
|
||||
let n = 0;
|
||||
const handle = setInterval(() => n++, 4);
|
||||
setTimeout(() => clearInterval(handle), 9);
|
||||
t.mock.timers.tick(20);
|
||||
assert.strictEqual(n, 2);
|
||||
});
|
||||
|
||||
it("leaves intervals alone when only setTimeout is mocked", async (t) => {
|
||||
t.mock.timers.enable({ apis: ["setTimeout"] });
|
||||
let n = 0;
|
||||
await new Promise<void>((resolve) => {
|
||||
const handle = setInterval(() => {
|
||||
n++;
|
||||
clearInterval(handle);
|
||||
resolve();
|
||||
}, 5);
|
||||
});
|
||||
assert.strictEqual(n, 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("runAll", () => {
|
||||
it("ticks to the last queued timer", (t) => {
|
||||
t.mock.timers.enable({ apis: ["setTimeout"] });
|
||||
const log: string[] = [];
|
||||
setTimeout(() => {
|
||||
log.push("a");
|
||||
setTimeout(() => log.push("b"), 100);
|
||||
}, 10);
|
||||
setTimeout(() => log.push("c"), 20);
|
||||
t.mock.timers.runAll();
|
||||
log.push("first runAll");
|
||||
t.mock.timers.runAll();
|
||||
log.push("second runAll");
|
||||
t.mock.timers.runAll();
|
||||
assert.deepStrictEqual(log, ["a", "c", "first runAll", "b", "second runAll"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("clock", () => {
|
||||
it("starts at now and moves with setTime", (t) => {
|
||||
t.mock.timers.enable({ apis: ["setTimeout"], now: 1000 });
|
||||
const log: string[] = [];
|
||||
setTimeout(() => log.push("a"), 10);
|
||||
t.mock.timers.setTime(5000);
|
||||
setTimeout(() => log.push("b"), 10);
|
||||
t.mock.timers.tick(1);
|
||||
log.push("tick 1");
|
||||
t.mock.timers.tick(9);
|
||||
t.mock.timers.setTime();
|
||||
setTimeout(() => log.push("c"), 1);
|
||||
t.mock.timers.tick(1);
|
||||
assert.deepStrictEqual(log, ["a", "tick 1", "b", "c"]);
|
||||
});
|
||||
|
||||
it("takes now from a Date", (t) => {
|
||||
t.mock.timers.enable({ apis: ["setTimeout"], now: new Date(2000) });
|
||||
const log: string[] = [];
|
||||
setTimeout(() => log.push("a"), 10);
|
||||
t.mock.timers.setTime(2005);
|
||||
t.mock.timers.tick(4);
|
||||
log.push("tick 4");
|
||||
t.mock.timers.tick(1);
|
||||
assert.deepStrictEqual(log, ["tick 4", "a"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("handles", () => {
|
||||
it("ref, unref and refresh leave a mock timer as it is", (t) => {
|
||||
t.mock.timers.enable({ apis: ["setTimeout"] });
|
||||
let fired = 0;
|
||||
const handle = setTimeout(() => fired++, 5);
|
||||
assert.strictEqual(handle.hasRef(), true);
|
||||
assert.strictEqual(handle.unref().hasRef(), true);
|
||||
assert.strictEqual(handle.ref().hasRef(), true);
|
||||
handle.refresh();
|
||||
t.mock.timers.tick(5);
|
||||
t.mock.timers.tick(5);
|
||||
assert.strictEqual(fired, 1);
|
||||
});
|
||||
|
||||
it("clearTimeout of a mock timer", (t) => {
|
||||
t.mock.timers.enable({ apis: ["setTimeout"] });
|
||||
let fired = 0;
|
||||
const handle = setTimeout(() => fired++, 5);
|
||||
clearTimeout(handle);
|
||||
t.mock.timers.tick(10);
|
||||
assert.strictEqual(fired, 0);
|
||||
});
|
||||
|
||||
it("a real timer made before enable keeps running", async (t) => {
|
||||
let fired = 0;
|
||||
const done = new Promise<void>((resolve) => {
|
||||
const real = setTimeout(() => {
|
||||
fired++;
|
||||
resolve();
|
||||
}, 5);
|
||||
t.mock.timers.enable({ apis: ["setTimeout"] });
|
||||
// The mocked clearTimeout leaves the real timer armed.
|
||||
clearTimeout(real);
|
||||
assert.strictEqual(real.hasRef(), true);
|
||||
});
|
||||
await done;
|
||||
assert.strictEqual(fired, 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("errors", () => {
|
||||
it("throws Node's errors", (t) => {
|
||||
const got: string[] = [];
|
||||
got.push(failure(() => t.mock.timers.tick(1)));
|
||||
got.push(failure(() => t.mock.timers.runAll()));
|
||||
got.push(failure(() => t.mock.timers.setTime(-1)));
|
||||
got.push(failure(() => t.mock.timers.setTime(5)));
|
||||
got.push(failure(() => t.mock.timers.enable({ apis: ["setTimeout"], now: -1 })));
|
||||
got.push(failure(() => t.mock.timers.enable({ apis: ["setTimeout"], now: NaN })));
|
||||
got.push(failure(() => t.mock.timers.reset()));
|
||||
t.mock.timers.enable({ apis: ["setTimeout"] });
|
||||
got.push(failure(() => t.mock.timers.enable({ apis: ["setTimeout"] })));
|
||||
got.push(failure(() => t.mock.timers.tick(-1.5)));
|
||||
got.push(failure(() => t.mock.timers.tick(-Infinity)));
|
||||
got.push(failure(() => t.mock.timers.tick(0)));
|
||||
assert.deepStrictEqual(got, [
|
||||
NOT_ENABLED,
|
||||
NOT_ENABLED,
|
||||
negative("-1"),
|
||||
NOT_ENABLED,
|
||||
negative("-1"),
|
||||
"TypeError ERR_INVALID_ARG_VALUE The argument 'now' epoch must be a positive integer received NaN. Received NaN",
|
||||
"no throw",
|
||||
"Error ERR_INVALID_STATE Invalid state: MockTimers is already enabled!",
|
||||
negative("-1.5"),
|
||||
negative("-Infinity"),
|
||||
"no throw",
|
||||
]);
|
||||
});
|
||||
|
||||
it("a throwing callback stays queued", (t) => {
|
||||
t.mock.timers.enable({ apis: ["setTimeout"] });
|
||||
let runs = 0;
|
||||
setTimeout(() => {
|
||||
runs++;
|
||||
if (runs === 1) throw new Error("boom");
|
||||
}, 5);
|
||||
assert.throws(() => t.mock.timers.tick(5), { message: "boom" });
|
||||
t.mock.timers.tick(0);
|
||||
t.mock.timers.tick(0);
|
||||
assert.strictEqual(runs, 2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("imports", () => {
|
||||
it("globalThis.setTimeout is mocked", (t) => {
|
||||
t.mock.timers.enable({ apis: ["setTimeout"] });
|
||||
const log: string[] = [];
|
||||
globalThis.setTimeout(() => log.push("timeout"), 5);
|
||||
t.mock.timers.tick(5);
|
||||
assert.deepStrictEqual(log, ["timeout"]);
|
||||
});
|
||||
|
||||
it("named and namespace imports keep the loop's timers", async (t) => {
|
||||
t.mock.timers.enable({ apis: ["setTimeout"] });
|
||||
const log: string[] = [];
|
||||
await Promise.all([
|
||||
sleep(5).then(() => {
|
||||
log.push("named promise");
|
||||
}),
|
||||
new Promise<void>((resolve) =>
|
||||
namedSetTimeout(() => {
|
||||
log.push("named");
|
||||
resolve();
|
||||
}, 2),
|
||||
),
|
||||
new Promise<void>((resolve) =>
|
||||
timersNamespace.setTimeout(() => {
|
||||
log.push("namespace");
|
||||
resolve();
|
||||
}, 3),
|
||||
),
|
||||
]);
|
||||
t.mock.timers.tick(10);
|
||||
assert.deepStrictEqual(log, ["named", "namespace", "named promise"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("trackers", () => {
|
||||
it("leaves t.mock.timers enabled", (t) => {
|
||||
t.mock.timers.enable({ apis: ["setTimeout"] });
|
||||
setTimeout(() => {
|
||||
throw new Error("never runs");
|
||||
}, 1);
|
||||
});
|
||||
|
||||
it("the previous test's timers were reset", async () => {
|
||||
let fired = false;
|
||||
await new Promise<void>((resolve) =>
|
||||
setTimeout(() => {
|
||||
fired = true;
|
||||
resolve();
|
||||
}, 1),
|
||||
);
|
||||
assert.strictEqual(fired, true);
|
||||
});
|
||||
|
||||
it("t.mock takes setTimeout over from mock and gives it back", (t) => {
|
||||
const log: string[] = [];
|
||||
mock.timers.enable({ apis: ["setTimeout"] });
|
||||
setTimeout(() => log.push("module"), 5);
|
||||
t.mock.timers.enable({ apis: ["setTimeout"] });
|
||||
setTimeout(() => log.push("test"), 5);
|
||||
mock.timers.tick(5);
|
||||
log.push("module ticked");
|
||||
t.mock.timers.tick(5);
|
||||
t.mock.timers.reset();
|
||||
setTimeout(() => log.push("module again"), 1);
|
||||
mock.timers.tick(1);
|
||||
mock.timers.reset();
|
||||
assert.deepStrictEqual(log, ["module", "module ticked", "test", "module again"]);
|
||||
});
|
||||
|
||||
it("mock.reset() resets mock.timers", () => {
|
||||
mock.timers.enable({ apis: ["setInterval"] });
|
||||
mock.reset();
|
||||
assert.strictEqual(failure(() => mock.timers.tick(1)), NOT_ENABLED);
|
||||
});
|
||||
});
|
||||
|
||||
test("timers outside node:test bodies", () => {
|
||||
// A fresh enable after reset starts the clock at 0 again.
|
||||
mock.timers.enable({ apis: ["setTimeout"], now: 100 });
|
||||
mock.timers.reset();
|
||||
mock.timers.enable({ apis: ["setTimeout"] });
|
||||
const log: string[] = [];
|
||||
setTimeout(() => log.push("at 1"), 1);
|
||||
mock.timers.tick(1);
|
||||
mock.timers.reset();
|
||||
assert.deepStrictEqual(log, ["at 1"]);
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"f": 1,
|
||||
"list": [
|
||||
"a"
|
||||
]
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
// @exit: 1
|
||||
// t.assert.snapshot and t.assert.fileSnapshot in compare mode, against the
|
||||
// snapshot.ts.snapshot file `node --test-update-snapshots` wrote (then
|
||||
// edited so that one snapshot differs and one is missing). Values serialize
|
||||
// with JSON.stringify(value, null, 2) unless options.serializers replace
|
||||
// it; each test numbers its snapshots under its full name; a differing
|
||||
// snapshot fails as assert.strictEqual does, and missing snapshots and
|
||||
// files and throwing serializers throw ERR_INVALID_STATE.
|
||||
import { describe, test } from "node:test";
|
||||
|
||||
describe("values", () => {
|
||||
test("records and arrays", (t) => {
|
||||
t.assert.snapshot({ zeta: 1, alpha: [1, "two", { b: true, a: null }] });
|
||||
t.assert.snapshot([{ id: 3, name: "n" }]);
|
||||
});
|
||||
test("strings", (t) => {
|
||||
t.assert.snapshot("plain");
|
||||
t.assert.snapshot("tick ` dollar ${x} backslash \\ newline \n tab \t");
|
||||
t.assert.snapshot("unicode é 漢 😀");
|
||||
});
|
||||
test("scalars", (t) => {
|
||||
t.assert.snapshot(42);
|
||||
t.assert.snapshot(-0.5);
|
||||
t.assert.snapshot(true);
|
||||
t.assert.snapshot(null);
|
||||
t.assert.snapshot(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
test("name with `backticks` and ${braces}", (t) => {
|
||||
t.assert.snapshot({ ok: true });
|
||||
});
|
||||
|
||||
describe("serializers", () => {
|
||||
test("run in order", (t) => {
|
||||
t.assert.snapshot({ n: 5 }, { serializers: [(v: { n: number }) => v.n * 2, (v: number) => `n is ${v}`] });
|
||||
t.assert.snapshot("x", { serializers: [(v: string) => v.toUpperCase()] });
|
||||
t.assert.snapshot(7, { serializers: [(v: number) => v + 1] });
|
||||
});
|
||||
test("a throwing serializer", (t) => {
|
||||
t.assert.snapshot(1, {
|
||||
serializers: [
|
||||
() => {
|
||||
throw new Error("no");
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
test("a circular value", (t) => {
|
||||
const loop: { self?: unknown } = {};
|
||||
loop.self = loop;
|
||||
t.assert.snapshot(loop);
|
||||
});
|
||||
});
|
||||
|
||||
test("counts toward the plan", { plan: 2 }, (t) => {
|
||||
t.assert.snapshot(1);
|
||||
t.assert.ok(true);
|
||||
});
|
||||
|
||||
test("differs from the stored snapshot", (t) => {
|
||||
t.assert.snapshot({ count: 2 });
|
||||
});
|
||||
|
||||
test("missing from the snapshot file", (t) => {
|
||||
t.assert.snapshot("new");
|
||||
});
|
||||
|
||||
test("file snapshots", (t) => {
|
||||
t.assert.fileSnapshot({ f: 1, list: ["a"] }, "tests/fixtures/node-test/cases/snapshot-files/value.json");
|
||||
t.assert.fileSnapshot("text", "tests/fixtures/node-test/cases/snapshot-files/missing.txt");
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
exports[`counts toward the plan 1`] = `
|
||||
1
|
||||
`;
|
||||
|
||||
exports[`differs from the stored snapshot 1`] = `
|
||||
{
|
||||
"count": 1
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`name with \`backticks\` and \${braces} 1`] = `
|
||||
{
|
||||
"ok": true
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`serializers > run in order 1`] = `
|
||||
n is 10
|
||||
`;
|
||||
|
||||
exports[`serializers > run in order 2`] = `
|
||||
X
|
||||
`;
|
||||
|
||||
exports[`serializers > run in order 3`] = `
|
||||
8
|
||||
`;
|
||||
|
||||
exports[`values > records and arrays 1`] = `
|
||||
{
|
||||
"zeta": 1,
|
||||
"alpha": [
|
||||
1,
|
||||
"two",
|
||||
{
|
||||
"b": true,
|
||||
"a": null
|
||||
}
|
||||
]
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`values > records and arrays 2`] = `
|
||||
[
|
||||
{
|
||||
"id": 3,
|
||||
"name": "n"
|
||||
}
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`values > scalars 1`] = `
|
||||
42
|
||||
`;
|
||||
|
||||
exports[`values > scalars 2`] = `
|
||||
-0.5
|
||||
`;
|
||||
|
||||
exports[`values > scalars 3`] = `
|
||||
true
|
||||
`;
|
||||
|
||||
exports[`values > scalars 4`] = `
|
||||
null
|
||||
`;
|
||||
|
||||
exports[`values > scalars 5`] = `
|
||||
undefined
|
||||
`;
|
||||
|
||||
exports[`values > strings 1`] = `
|
||||
"plain"
|
||||
`;
|
||||
|
||||
exports[`values > strings 2`] = `
|
||||
"tick \` dollar \${x} backslash \\\\ newline \\n tab \\t"
|
||||
`;
|
||||
|
||||
exports[`values > strings 3`] = `
|
||||
"unicode é 漢 😀"
|
||||
`;
|
||||
@@ -0,0 +1,27 @@
|
||||
// Run with --test-update-snapshots (before the program arguments): the
|
||||
// snapshot assertions record their values, the run writes
|
||||
// snapshot-update.ts.snapshot with sorted keys and out/value.json, and the
|
||||
// flag is in process.execArgv instead of process.argv.
|
||||
import { describe, test } from "node:test";
|
||||
|
||||
console.log(JSON.stringify(process.argv.slice(2)), JSON.stringify(process.execArgv));
|
||||
|
||||
describe("suite", () => {
|
||||
test("values", (t) => {
|
||||
t.assert.snapshot({ b: 1, a: "x`${y}\\z\n" });
|
||||
t.assert.snapshot([1, 2]);
|
||||
t.assert.snapshot("é 😀 \u0001");
|
||||
});
|
||||
test("serializers", (t) => {
|
||||
t.assert.snapshot(3, { serializers: [(v: number) => v * 2] });
|
||||
});
|
||||
});
|
||||
|
||||
test("zz last", (t) => {
|
||||
t.assert.snapshot(undefined);
|
||||
});
|
||||
|
||||
test("aa `first` ${x}", (t) => {
|
||||
t.assert.snapshot(null);
|
||||
t.assert.fileSnapshot({ f: [1] }, "out/value.json");
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
mock-timers-fences.ts:20:22 - error SC2020: 'mock.timers.enable with a non-literal apis list' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
19 | const apis: ("setTimeout" | "setInterval")[] = ["setTimeout"];
|
||||
20 | mock.timers.enable({ apis }); // apis list that is not a literal
|
||||
| ^~~~
|
||||
21 |
|
||||
|
||||
hint: list the mocked APIs as string literals: enable({ apis: ['setTimeout', 'Date'] })
|
||||
|
||||
mock-timers-fences.ts:23:20 - error SC2020: 'mock.timers.enable with a non-literal options object' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
22 | const options = { apis: ["setTimeout"] as const };
|
||||
23 | mock.timers.enable(options); // options object that is not a literal
|
||||
| ^~~~~~~
|
||||
24 |
|
||||
|
||||
hint: list the mocked APIs as string literals: enable({ apis: ['setTimeout', 'Date'] })
|
||||
|
||||
mock-timers-fences.ts:25:7 - error SC2020: 'MockTimers' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
24 |
|
||||
25 | const timers = mock.timers; // the object as a value
|
||||
| ^~~~~~
|
||||
26 | timers.tick(1);
|
||||
|
||||
hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations)
|
||||
|
||||
mock-timers-fences.ts:25:16 - error SC2020: 'MockTracker.timers' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
24 |
|
||||
25 | const timers = mock.timers; // the object as a value
|
||||
| ^~~~~~~~~~~
|
||||
26 | timers.tick(1);
|
||||
|
||||
hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations)
|
||||
|
||||
mock-timers-fences.ts:26:1 - error SC2020: 'MockTimers.tick' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
25 | const timers = mock.timers; // the object as a value
|
||||
26 | timers.tick(1);
|
||||
| ^~~~~~~~~~~
|
||||
27 |
|
||||
|
||||
hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations)
|
||||
@@ -0,0 +1,26 @@
|
||||
snapshot-fences.ts:14:24 - error SC2020: 't.assert.snapshot options that are not an object literal' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
13 | const options = { serializers: [(v: unknown) => String(v)] };
|
||||
14 | t.assert.snapshot(1, options); // options that are not an object literal
|
||||
| ^~~~~~~
|
||||
15 |
|
||||
|
||||
hint: pass the options as an object literal: { serializers: [(value) => ...] }
|
||||
|
||||
snapshot-fences.ts:17:26 - error SC2020: 't.assert.snapshot with a non-literal serializers list' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
16 | const serializers = [(v: unknown) => String(v)];
|
||||
17 | t.assert.snapshot(1, { serializers }); // a serializers list that is not an array literal
|
||||
| ^~~~~~~~~~~
|
||||
18 | t.assert.fileSnapshot(1, "out.txt", { serializers: [...serializers] }); // a spread serializer
|
||||
|
||||
hint: pass the options as an object literal: { serializers: [(value) => ...] }
|
||||
|
||||
snapshot-fences.ts:18:55 - error SC2020: 't.assert.fileSnapshot with a spread serializer' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
17 | t.assert.snapshot(1, { serializers }); // a serializers list that is not an array literal
|
||||
18 | t.assert.fileSnapshot(1, "out.txt", { serializers: [...serializers] }); // a spread serializer
|
||||
| ^~~~~~~~~~~~~~
|
||||
19 | });
|
||||
|
||||
hint: pass the options as an object literal: { serializers: [(value) => ...] }
|
||||
@@ -32,13 +32,4 @@ utility-types.ts:19:14 - error SC2004: uses of 'dateBag' inherit the blocker on
|
||||
| ^~~~~~~
|
||||
20 | console.log(boot);
|
||||
|
||||
hint: the declaration of 'dateBag' did not compile — fix the diagnostic reported there and these sites clear with it
|
||||
|
||||
utility-types.ts:22:33 - error SC1101: converting typed values to 'unknown' is not supported yet
|
||||
|
||||
21 |
|
||||
22 | const stamped: Readonly<Date> = new Date(0);
|
||||
| ^~~~~~~~~~~
|
||||
23 | console.log(stamped.getTime());
|
||||
|
||||
hint: numbers, strings, booleans, JSON-safe records/arrays/unions (a deep copy), functions, program class instances, and supported native handles convert into 'unknown' slots; functions, exact-class round trips, and handles preserve identity, while this value's type has no dynamic representation yet
|
||||
hint: the declaration of 'dateBag' did not compile — fix the diagnostic reported there and these sites clear with it
|
||||
@@ -1841,6 +1841,7 @@
|
||||
"5648-dns-lookup-forms.ts": 2992,
|
||||
"5649-dns-service-servers.ts": 2712,
|
||||
"5650-dns-lookup-errors-untyped.cjs": 2822,
|
||||
"5681-array-pop-and-length.ts": 3754,
|
||||
"5727-thread-slots-basics.ts": 3100,
|
||||
"5728-thread-slots-workers.ts": 2500,
|
||||
"5729-thread-slots-values.ts": 2550,
|
||||
@@ -1854,6 +1855,11 @@
|
||||
"5807-worker-error-values.ts": 3304,
|
||||
"5808-worker-unhandled-rejections.ts": 3430,
|
||||
"5809-process-exit-and-rejections.cjs": 3294,
|
||||
"5810-mock-method-records.ts": 3976,
|
||||
"5811-mock-tracker-restore.ts": 3698,
|
||||
"5812-dyn-box-async-fields.ts": 3522,
|
||||
"5813-mock-timers-module.ts": 3766,
|
||||
"5814-mock-timers-date.ts": 3760,
|
||||
"5831-util-types-brands.ts": 3347,
|
||||
"5832-util-types-brands-dynamic.js": 3250,
|
||||
"5833-url-search-params-iterators.ts": 2678,
|
||||
@@ -1862,8 +1868,12 @@
|
||||
"5836-url-search-params-tuple-pairs.ts": 2118,
|
||||
"5837-util-format-json-circular.ts": 3117,
|
||||
"5838-qs-stringify-bigint.ts": 2207,
|
||||
"5841-json-typed-dyn-members.ts": 5467,
|
||||
"5842-qs-parse-null-prototype.ts": 1774,
|
||||
"5843-qs-parse-null-prototype-js.mjs": 1559,
|
||||
"5844-date-unknown-values.ts": 3969,
|
||||
"5846-events-esm-default-max.ts": 2633,
|
||||
"5847-events-cjs-default-max.cjs": 2823,
|
||||
"5857-regexp-unicode-sets.ts": 3350,
|
||||
"5858-disposable-stack.ts": 3050,
|
||||
"5859-disposable-stack-js.js": 3100,
|
||||
@@ -1889,6 +1899,7 @@
|
||||
"5912-dyn-class-conversion-methods.ts": 3251,
|
||||
"5913-prototype-type-error-messages.ts": 2484,
|
||||
"5914-dyn-in-class-prototype-members.ts": 3169,
|
||||
"5915-dyn-error-string-conversions.ts": 3200,
|
||||
"5922-matchall-groups-callbacks.ts": 3650,
|
||||
"5923-matchall-groups-js.js": 3970,
|
||||
"5939-js-let-object-reassigned-wider.js": 1945,
|
||||
@@ -1903,10 +1914,14 @@
|
||||
"5970-net-connect-host-name.ts": 2400,
|
||||
"5971-buffer-chunks-to-string.ts": 2700,
|
||||
"5972-net-set-encoding-listeners.ts": 2550,
|
||||
"5974-typeof-module-exports.cjs": 2000,
|
||||
"5990-worker-uncloneable.mjs": 3283,
|
||||
"5991-worker-async-dispose.mjs": 5699,
|
||||
"5992-worker-thread-name-lifetime.mjs": 2687,
|
||||
"600-closures-basic.ts": 2349,
|
||||
"601-closures-loops.ts": 1788,
|
||||
"602-closures-identity-recursion.ts": 2372,
|
||||
"6020-class-extends-function-constructor.js": 4300,
|
||||
"603-closures-rc-stress.ts": 1832,
|
||||
"604-closures-for-of.ts": 2639,
|
||||
"605-closures-forward-capture-tdz.ts": 3277,
|
||||
@@ -2416,6 +2431,7 @@
|
||||
"native-worker-clone-errors.ts": 2347,
|
||||
"native-worker-commonjs.cjs": 2188,
|
||||
"native-worker-delayed-messages.ts": 3114,
|
||||
"native-worker-discarded-heaps.ts": 4430,
|
||||
"native-worker-environment.ts": 3538,
|
||||
"native-worker-error.ts": 2810,
|
||||
"native-worker-exception-state.ts": 3938,
|
||||
@@ -4528,6 +4544,7 @@
|
||||
"5648-dns-lookup-forms.ts": 3416,
|
||||
"5649-dns-service-servers.ts": 4916,
|
||||
"5650-dns-lookup-errors-untyped.cjs": 3889,
|
||||
"5681-array-pop-and-length.ts": 3216,
|
||||
"5727-thread-slots-basics.ts": 4500,
|
||||
"5728-thread-slots-workers.ts": 5000,
|
||||
"5729-thread-slots-values.ts": 4400,
|
||||
@@ -4541,6 +4558,11 @@
|
||||
"5807-worker-error-values.ts": 35248,
|
||||
"5808-worker-unhandled-rejections.ts": 33209,
|
||||
"5809-process-exit-and-rejections.cjs": 30261,
|
||||
"5810-mock-method-records.ts": 6363,
|
||||
"5811-mock-tracker-restore.ts": 4503,
|
||||
"5812-dyn-box-async-fields.ts": 5278,
|
||||
"5813-mock-timers-module.ts": 4227,
|
||||
"5814-mock-timers-date.ts": 17708,
|
||||
"5831-util-types-brands.ts": 4380,
|
||||
"5832-util-types-brands-dynamic.js": 3658,
|
||||
"5833-url-search-params-iterators.ts": 5339,
|
||||
@@ -4549,8 +4571,12 @@
|
||||
"5836-url-search-params-tuple-pairs.ts": 3193,
|
||||
"5837-util-format-json-circular.ts": 3408,
|
||||
"5838-qs-stringify-bigint.ts": 3519,
|
||||
"5841-json-typed-dyn-members.ts": 3238,
|
||||
"5842-qs-parse-null-prototype.ts": 7077,
|
||||
"5843-qs-parse-null-prototype-js.mjs": 5395,
|
||||
"5844-date-unknown-values.ts": 4543,
|
||||
"5846-events-esm-default-max.ts": 3102,
|
||||
"5847-events-cjs-default-max.cjs": 3039,
|
||||
"5857-regexp-unicode-sets.ts": 3900,
|
||||
"5858-disposable-stack.ts": 3750,
|
||||
"5859-disposable-stack-js.js": 3600,
|
||||
@@ -4576,6 +4602,7 @@
|
||||
"5912-dyn-class-conversion-methods.ts": 4424,
|
||||
"5913-prototype-type-error-messages.ts": 4068,
|
||||
"5914-dyn-in-class-prototype-members.ts": 4327,
|
||||
"5915-dyn-error-string-conversions.ts": 4247,
|
||||
"5922-matchall-groups-callbacks.ts": 5960,
|
||||
"5923-matchall-groups-js.js": 6650,
|
||||
"5939-js-let-object-reassigned-wider.js": 4436,
|
||||
@@ -4590,10 +4617,14 @@
|
||||
"5970-net-connect-host-name.ts": 2800,
|
||||
"5971-buffer-chunks-to-string.ts": 3100,
|
||||
"5972-net-set-encoding-listeners.ts": 3000,
|
||||
"5974-typeof-module-exports.cjs": 2500,
|
||||
"5990-worker-uncloneable.mjs": 22432,
|
||||
"5991-worker-async-dispose.mjs": 25768,
|
||||
"5992-worker-thread-name-lifetime.mjs": 28932,
|
||||
"600-closures-basic.ts": 2534,
|
||||
"601-closures-loops.ts": 2297,
|
||||
"602-closures-identity-recursion.ts": 2427,
|
||||
"6020-class-extends-function-constructor.js": 7500,
|
||||
"603-closures-rc-stress.ts": 2589,
|
||||
"604-closures-for-of.ts": 3514,
|
||||
"605-closures-forward-capture-tdz.ts": 4638,
|
||||
@@ -5103,6 +5134,7 @@
|
||||
"native-worker-clone-errors.ts": 3629,
|
||||
"native-worker-commonjs.cjs": 3085,
|
||||
"native-worker-delayed-messages.ts": 28752,
|
||||
"native-worker-discarded-heaps.ts": 39440,
|
||||
"native-worker-environment.ts": 27133,
|
||||
"native-worker-error.ts": 2958,
|
||||
"native-worker-exception-state.ts": 3945,
|
||||
|
||||
@@ -764,6 +764,38 @@ console.log("recovered");
|
||||
expect(r.stdout).toBe("TypeError true\narray: TypeError\nfunc: TypeError\nrecovered\n");
|
||||
});
|
||||
|
||||
test("a non-null assertion on a null object result throws at the assertion, even when the binding is never read", async () => {
|
||||
// Node lets the null ride until a member read; the compiled program
|
||||
// throws the stranded TypeError where `!` extracts the object.
|
||||
const r = await compileAndRun(
|
||||
"asserted-null-object-result",
|
||||
`interface Info {
|
||||
name: string;
|
||||
base: Info | null;
|
||||
callable: boolean;
|
||||
}
|
||||
function baseOf(info: Info): Info | null {
|
||||
return info.base;
|
||||
}
|
||||
function describe(info: Info): string {
|
||||
const base = baseOf(info)!;
|
||||
if (info.callable) return info.name + ": callable base";
|
||||
return info.name + " extends " + base.name;
|
||||
}
|
||||
const root: Info = { name: "Root", base: null, callable: false };
|
||||
console.log(describe({ name: "Child", base: root, callable: false }));
|
||||
try {
|
||||
console.log("unreachable", describe({ name: "Wrapped", base: null, callable: true }));
|
||||
} catch (e) {
|
||||
console.log((e as Error).name, (e as Error).message.includes("not representable in the target union"));
|
||||
}
|
||||
console.log("recovered");
|
||||
`,
|
||||
);
|
||||
expect(r.exitCode).toBe(0);
|
||||
expect(r.stdout).toBe("Child extends Root\nTypeError true\nrecovered\n");
|
||||
});
|
||||
|
||||
test("a checker-approved function mismatch strands per piece: mismatched params compile at the flow, void results trap after the call", async () => {
|
||||
// funcCoerceAdapter's stranded dispositions. Param strand: an array
|
||||
// literal of arrows collapses to one element signature — the
|
||||
|
||||
@@ -65,6 +65,7 @@ export const programs = [
|
||||
"nullish-long-chain.ts",
|
||||
"class-array-optional-return.ts",
|
||||
"contextual-array-union-write.ts",
|
||||
"6020-class-extends-function-constructor.js",
|
||||
];
|
||||
|
||||
interface LowerRequest {
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
* node-test.test.ts's function, extracted so the Linux lane applies the
|
||||
* IDENTICAL scrub to both of its in-container lanes. Node's spec reporter
|
||||
* embeds real durations in every line (raw byte-compare is impossible for
|
||||
* ANY node:test program); stack frames and the inspect property block a
|
||||
* frame opens are runner-internal output the compiled runtime
|
||||
* deliberately omits (SEMANTICS.md). Everything else must match
|
||||
* byte-exactly. */
|
||||
* ANY node:test program); stack frames, the "... N lines matching cause
|
||||
* stack trace ..." line inspect folds an error cause's shared frames into,
|
||||
* and the inspect property block a frame opens are runner-internal output
|
||||
* the compiled runtime deliberately omits (SEMANTICS.md). Everything else
|
||||
* must match byte-exactly. */
|
||||
|
||||
/** The documented normalization — durations, frames, the property block. */
|
||||
export function normalizeNodeTestOutput(stdout: string): string {
|
||||
@@ -16,6 +17,7 @@ export function normalizeNodeTestOutput(stdout: string): string {
|
||||
if (/^\s*\}\s*$/.test(line)) inPropBlock = false;
|
||||
continue;
|
||||
}
|
||||
if (/^\s+\.\.\. \d+ lines? matching cause stack trace \.\.\.$/.test(line)) continue;
|
||||
if (/^\s+at /.test(line)) {
|
||||
if (line.trimEnd().endsWith("{")) inPropBlock = true;
|
||||
continue;
|
||||
|
||||
@@ -11,10 +11,12 @@
|
||||
* NORMALIZATIONS (both lanes):
|
||||
* - "(1.234ms)" duration parentheticals → "(Xms)"
|
||||
* - the "ℹ duration_ms N" summary line → "ℹ duration_ms X"
|
||||
* - stack frames (" at ...") and the inspect property block a frame
|
||||
* opens ("... {" through its closing "}") are dropped — Node prints
|
||||
* its runner-internal frames and an error-property dump the compiled
|
||||
* runtime deliberately omits (SEMANTICS.md).
|
||||
* - stack frames (" at ..."), the "... N lines matching cause stack
|
||||
* trace ..." line that folds an error cause's shared frames, and the
|
||||
* inspect property block a frame opens ("... {" through its closing
|
||||
* "}") are dropped — Node prints its runner-internal frames and an
|
||||
* error-property dump the compiled runtime deliberately omits
|
||||
* (SEMANTICS.md).
|
||||
*
|
||||
* Fixtures never console.log inside test bodies or hooks: Node's
|
||||
* reporter stream lags console output RACILY (its ordering varies run to
|
||||
@@ -25,7 +27,8 @@
|
||||
* the RC audit over every case. */
|
||||
import { spawn } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { globSync, mkdirSync, readFileSync } from "node:fs";
|
||||
import { copyFileSync, globSync, mkdirSync, mkdtempSync, readFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { compile } from "@scriptc/compiler";
|
||||
@@ -41,11 +44,12 @@ interface ProgramRun {
|
||||
exitCode: number;
|
||||
}
|
||||
|
||||
function runLane(cmd: string, args: string[]): Promise<ProgramRun> {
|
||||
function runLane(cmd: string, args: string[], cwd = repoRoot): Promise<ProgramRun> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// cwd pinned to the repo root: both reporters render "test at" paths
|
||||
// relative to the working directory, so the lanes must share one.
|
||||
const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"], cwd: repoRoot });
|
||||
// cwd pinned to the repo root by default: both reporters render "test
|
||||
// at" paths relative to the working directory, so the lanes must share
|
||||
// one.
|
||||
const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"], cwd });
|
||||
const out: Buffer[] = [];
|
||||
let errText = "";
|
||||
const timer = setTimeout(() => {
|
||||
@@ -125,3 +129,48 @@ describe(`node:test differential (${cases.length} programs${sanitize ? ", saniti
|
||||
120_000,
|
||||
);
|
||||
});
|
||||
|
||||
/* Snapshot update mode: Node's --test-update-snapshots flag, which the
|
||||
* compiled binary takes as its leading argument. Each lane runs the
|
||||
* fixture from its own temporary copy (the snapshot file sits beside the
|
||||
* entry file and fileSnapshot paths resolve against the working
|
||||
* directory), writes the snapshot files, and then passes in compare mode
|
||||
* against them. The written files and the printed argv/execArgv match. */
|
||||
describe(`node:test snapshot update mode${sanitize ? " (sanitized)" : ""}`, () => {
|
||||
test("--test-update-snapshots writes the files Node writes", async () => {
|
||||
const fixture = join(fixturesRoot, "update/snapshot-update.ts");
|
||||
const lanes = await Promise.all(
|
||||
(["node", "native"] as const).map(async (lane) => {
|
||||
const dir = mkdtempSync(join(tmpdir(), `scriptc-snapshot-${lane}-`));
|
||||
const entry = join(dir, "snapshot-update.ts");
|
||||
copyFileSync(fixture, entry);
|
||||
const binary = lane === "native" ? await build(entry) : null;
|
||||
const run = (update: boolean) =>
|
||||
binary
|
||||
? runLane(binary, [...(update ? ["--test-update-snapshots"] : []), "one", "two"], dir)
|
||||
: runLane(
|
||||
"node",
|
||||
[...(update ? ["--test-update-snapshots"] : []), entry, "one", "two"],
|
||||
dir,
|
||||
);
|
||||
const update = await run(true);
|
||||
const compare = await run(false);
|
||||
return {
|
||||
update,
|
||||
compare,
|
||||
snapshot: readFileSync(`${entry}.snapshot`, "utf8"),
|
||||
file: readFileSync(join(dir, "out/value.json"), "utf8"),
|
||||
};
|
||||
}),
|
||||
);
|
||||
const [node, native] = lanes as [(typeof lanes)[number], (typeof lanes)[number]];
|
||||
expect(native.snapshot).toBe(node.snapshot);
|
||||
expect(native.file).toBe(node.file);
|
||||
expect(normalize(native.update.stdout)).toBe(normalize(node.update.stdout));
|
||||
expect(normalize(native.compare.stdout)).toBe(normalize(node.compare.stdout));
|
||||
for (const lane of lanes) {
|
||||
expect(lane.update.exitCode).toBe(0);
|
||||
expect(lane.compare.exitCode).toBe(0);
|
||||
}
|
||||
}, 120_000);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user