Merge main

This commit is contained in:
Chris Tate
2026-10-18 08:45:06 -05:00
121 changed files with 6143 additions and 559 deletions
+143
View File
@@ -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);
+91
View File
@@ -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);
+83
View File
@@ -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));
+57
View File
@@ -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);
+59
View File
@@ -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);
}
}
+35
View File
@@ -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
+19
View File
@@ -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
});
+3 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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");
});
+82
View File
@@ -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 é 漢 😀"
`;
+27
View File
@@ -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
+32
View File
@@ -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,
+32
View File
@@ -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
+1
View File
@@ -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 {
+6 -4
View File
@@ -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;
+58 -9
View File
@@ -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);
});