mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-14 14:32:02 +08:00
Accept mock.fn() without an original
node:test changes, matching Node 24.22:
- mock.fn() and t.mock.fn() without an original return undefined and
record the arguments of each call, whatever the callers pass. The
original and the initial implementation are a function that returns
undefined, so restore() brings that function back.
- mock.fn<F>() with an explicit function type records F's parameters
when F's return type is void or includes undefined. A return type
without undefined, such as mock.fn<() => number>(), throws SC2020.
mock.fn()'s default type, (...args: any[]) => undefined, maps to the
checked-dynamic rest ABI used for program-declared (...args: unknown[])
functions: the calls log records the argument array, and the mock's
wrapper spreads it into the implementation.
A function whose fixed parameters are all unknown or any, or that has
none, now converts to a checked-dynamic rest signature without fixed
parameters, such as (...args: unknown[]) => void: each call passes the
arguments in order, undefined for the ones the caller left out. This is
how () => {} reaches mockImplementation and mockImplementationOnce, and
it applies to any such slot in TypeScript programs, which threw SC1090
before.
The mock-fn-default fixture and corpus program 5816 compare with Node,
plain and with SCRIPTC_SAN=1.
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
// A function with only `unknown` parameters, or none, flows into a
|
||||
// `(...args: unknown[]) => R` slot: each call passes the callers'
|
||||
// arguments in order, undefined for a parameter the caller left out, and
|
||||
// drops the extra ones, as JS calls do. Sync and async functions, stored
|
||||
// in variables and arrays and passed as arguments.
|
||||
type Handler = (...args: unknown[]) => void;
|
||||
type AsyncHandler = (...args: unknown[]) => Promise<unknown>;
|
||||
|
||||
const none: Handler = () => console.log("none");
|
||||
const one: Handler = (a: unknown) => console.log("one", a);
|
||||
const two: Handler = (a: unknown, b: unknown) => console.log("two", a, b);
|
||||
|
||||
none();
|
||||
none(1, 2, 3);
|
||||
one();
|
||||
one("x");
|
||||
one("x", "y");
|
||||
two(1);
|
||||
two(1, [2], { three: 3 });
|
||||
|
||||
const handlers: Handler[] = [none, one, two];
|
||||
for (const h of handlers) h("all", true);
|
||||
|
||||
function dispatch(h: Handler, ...args: unknown[]): void {
|
||||
h(...args);
|
||||
}
|
||||
dispatch(two, "spread", null);
|
||||
dispatch(one);
|
||||
|
||||
const first: AsyncHandler = async (value: unknown) => value;
|
||||
const pair: AsyncHandler = async (a: unknown, b: unknown) => [a, b];
|
||||
|
||||
async function main(): Promise<void> {
|
||||
console.log(await first(10, 20));
|
||||
console.log(await first());
|
||||
console.log(await pair("p"));
|
||||
console.log(await pair("p", "q", "r"));
|
||||
}
|
||||
main().then(() => console.log("done"));
|
||||
@@ -30,8 +30,3 @@ console.log(typeof widened);
|
||||
// `instanceof Promise` on a value that is never a promise.
|
||||
const record = { v: 1 };
|
||||
console.log(record instanceof Promise);
|
||||
|
||||
// A variadic `unknown[]` slot takes closures with the same variadic ABI.
|
||||
type HandlerFn = (...args: unknown[]) => Promise<unknown>;
|
||||
const fixed: HandlerFn = async (first: unknown) => first;
|
||||
fixed(1, 2).then((v) => console.log(v));
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// node:test mocks lower for mock.fn(original[, implementation]),
|
||||
// node:test mocks lower for mock.fn([original[, implementation]]),
|
||||
// mock.method(object, methodName[, implementation]) on records, their
|
||||
// t.mock twins, and the trackers' restoreAll and reset. The rest of the
|
||||
// mock surface is refused, each with a named diagnostic: mock.fn() without
|
||||
// an original; mock.method on a class instance or a builtin object, with
|
||||
// an original for a function type whose return type does not include
|
||||
// undefined; mock.method on a class instance or a builtin object, with
|
||||
// an options object, or with an implementation of another type when the
|
||||
// result is used; and functions whose union return type has no undefined
|
||||
// arm (the calls log records undefined for calls that throw).
|
||||
@@ -11,8 +12,11 @@ import { mock, test } from "node:test";
|
||||
const add = mock.fn((a: number, b: number) => a + b); // lowers
|
||||
add(1, 2);
|
||||
|
||||
const empty = mock.fn(); // no original function
|
||||
empty();
|
||||
const empty = mock.fn(); // lowers
|
||||
empty(1, "two");
|
||||
|
||||
const counted = mock.fn<() => number>(); // no original, returns number
|
||||
counted();
|
||||
|
||||
const obj = { value: () => 1 };
|
||||
mock.method(obj, "value"); // lowers
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
// mock.fn() and t.mock.fn() without an original: the mock returns
|
||||
// undefined and records the arguments of each call, whatever its callers
|
||||
// pass. mockImplementation and mockImplementationOnce replace its
|
||||
// behavior, and restore() brings back the function that returns undefined.
|
||||
// mock.fn<F>() records F's parameters, for a function type whose return
|
||||
// type is void or includes undefined.
|
||||
import assert from "node:assert";
|
||||
import { mock, test } from "node:test";
|
||||
|
||||
test("records calls with any arguments", () => {
|
||||
const fn = mock.fn();
|
||||
fn(1, "two");
|
||||
fn();
|
||||
fn(true, null, [3]);
|
||||
assert.strictEqual(fn.mock.callCount(), 3);
|
||||
const [first, second, third] = fn.mock.calls;
|
||||
assert.deepStrictEqual(first!.arguments, [1, "two"]);
|
||||
assert.strictEqual(second!.arguments.length, 0);
|
||||
assert.deepStrictEqual(third!.arguments, [true, null, [3]]);
|
||||
assert.ok(first!.result === undefined);
|
||||
assert.ok(first!.error === undefined);
|
||||
assert.ok(first!.this === undefined);
|
||||
});
|
||||
|
||||
test("changes implementations", () => {
|
||||
const seen: string[] = [];
|
||||
const fn = mock.fn();
|
||||
fn.mock.mockImplementation((...args: unknown[]) => {
|
||||
seen.push(`impl ${args.length}`);
|
||||
return undefined;
|
||||
});
|
||||
fn.mock.mockImplementationOnce(() => {
|
||||
seen.push("once");
|
||||
return undefined;
|
||||
});
|
||||
fn("a");
|
||||
fn("a", "b");
|
||||
fn.mock.restore();
|
||||
fn("c");
|
||||
assert.deepStrictEqual(seen, ["once", "impl 2"]);
|
||||
assert.strictEqual(fn.mock.callCount(), 3);
|
||||
fn.mock.resetCalls();
|
||||
assert.strictEqual(fn.mock.callCount(), 0);
|
||||
});
|
||||
|
||||
test("records a throwing implementation", () => {
|
||||
const fn = mock.fn();
|
||||
fn.mock.mockImplementation(() => {
|
||||
throw new Error("boom");
|
||||
});
|
||||
assert.throws(() => fn(7), { message: "boom" });
|
||||
const call = fn.mock.calls[0]!;
|
||||
assert.deepStrictEqual(call.arguments, [7]);
|
||||
assert.ok(call.error instanceof Error);
|
||||
});
|
||||
|
||||
test("t.mock.fn() and typed mock.fn<F>()", (t) => {
|
||||
const fn = t.mock.fn();
|
||||
fn("x");
|
||||
assert.strictEqual(fn.mock.callCount(), 1);
|
||||
const typed = t.mock.fn<(a: number, b: string) => void>();
|
||||
typed(1, "x");
|
||||
typed(2, "y");
|
||||
assert.strictEqual(typed.mock.callCount(), 2);
|
||||
assert.strictEqual(typed.mock.calls[1]!.arguments[0], 2);
|
||||
assert.strictEqual(typed.mock.calls[1]!.arguments[1], "y");
|
||||
const maybe = t.mock.fn<() => number | undefined>();
|
||||
assert.ok(maybe() === undefined);
|
||||
maybe.mock.mockImplementation(() => 4);
|
||||
assert.ok(maybe() === 4);
|
||||
assert.ok(maybe.mock.calls[1]!.result === 4);
|
||||
});
|
||||
@@ -55,11 +55,4 @@ maybe-promise-fences.ts:32:13 - error SC1090: 'instanceof Promise' on '{ v: numb
|
||||
31 | const record = { v: 1 };
|
||||
32 | console.log(record instanceof Promise);
|
||||
| ^~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
33 |
|
||||
|
||||
maybe-promise-fences.ts:36:26 - error SC1090: '(unknown) => Promise<unknown>' values where '() => Promise<unknown>' is expected is not supported yet
|
||||
|
||||
35 | type HandlerFn = (...args: unknown[]) => Promise<unknown>;
|
||||
36 | const fixed: HandlerFn = async (first: unknown) => first;
|
||||
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
37 | fixed(1, 2).then((v) => console.log(v));
|
||||
33 |
|
||||
@@ -1,62 +1,62 @@
|
||||
mock-fences.ts:14:15 - error SC2020: 'mock.fn of this function type' is part of the standard library types but has no scriptc lowering yet
|
||||
mock-fences.ts:18:17 - error SC2020: 'mock.fn() of a function returning 'number'' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
13 |
|
||||
14 | const empty = mock.fn(); // no original function
|
||||
| ^~~~~~~~~
|
||||
15 | empty();
|
||||
17 |
|
||||
18 | const counted = mock.fn<() => number>(); // no original, returns number
|
||||
| ^~~~~~~~~~~~~~~~~~~~~~~
|
||||
19 | counted();
|
||||
|
||||
hint: mock functions lower for functions with fixed parameters, or a single rest array parameter, and an implementation of the same type
|
||||
hint: mock.fn() without an original returns undefined: pass an original, or use a function type whose return type includes undefined
|
||||
|
||||
mock-fences.ts:27:13 - error SC2020: 'mock.method on 'Counter'' is part of the standard library types but has no scriptc lowering yet
|
||||
mock-fences.ts:31:13 - error SC2020: 'mock.method on 'Counter'' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
26 | }
|
||||
27 | mock.method(new Counter(), "count"); // class instance
|
||||
30 | }
|
||||
31 | mock.method(new Counter(), "count"); // class instance
|
||||
| ^~~~~~~~~~~~~
|
||||
28 | mock.method(console, "log"); // builtin object
|
||||
32 | mock.method(console, "log"); // builtin object
|
||||
|
||||
hint: mock.method lowers for objects whose type maps to a record (object literals and interface-typed objects) and a function-valued field named by a string literal
|
||||
|
||||
mock-fences.ts:28:13 - error SC2020: 'mock.method on this object' is part of the standard library types but has no scriptc lowering yet
|
||||
mock-fences.ts:32:13 - error SC2020: 'mock.method on this object' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
27 | mock.method(new Counter(), "count"); // class instance
|
||||
28 | mock.method(console, "log"); // builtin object
|
||||
31 | mock.method(new Counter(), "count"); // class instance
|
||||
32 | mock.method(console, "log"); // builtin object
|
||||
| ^~~~~~~
|
||||
29 | mock.method(obj, "value", { times: 1 }); // options
|
||||
33 | mock.method(obj, "value", { times: 1 }); // options
|
||||
|
||||
hint: mock.method lowers for objects whose type maps to a record (object literals and interface-typed objects) and a function-valued field named by a string literal
|
||||
|
||||
mock-fences.ts:29:27 - error SC2020: 'mock.method with an options object' is part of the standard library types but has no scriptc lowering yet
|
||||
mock-fences.ts:33:27 - error SC2020: 'mock.method with an options object' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
28 | mock.method(console, "log"); // builtin object
|
||||
29 | mock.method(obj, "value", { times: 1 }); // options
|
||||
32 | mock.method(console, "log"); // builtin object
|
||||
33 | mock.method(obj, "value", { times: 1 }); // options
|
||||
| ^~~~~~~~~~~~
|
||||
30 | const other = mock.method(obj, "value", () => "one"); // implementation of another type
|
||||
34 | const other = mock.method(obj, "value", () => "one"); // implementation of another type
|
||||
|
||||
hint: mock options (times, getter and setter) have no lowering
|
||||
|
||||
mock-fences.ts:30:41 - error SC2020: 'mock.method with an implementation of another type' is part of the standard library types but has no scriptc lowering yet
|
||||
mock-fences.ts:34:41 - error SC2020: 'mock.method with an implementation of another type' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
29 | mock.method(obj, "value", { times: 1 }); // options
|
||||
30 | const other = mock.method(obj, "value", () => "one"); // implementation of another type
|
||||
33 | mock.method(obj, "value", { times: 1 }); // options
|
||||
34 | const other = mock.method(obj, "value", () => "one"); // implementation of another type
|
||||
| ^~~~~~~~~~~
|
||||
31 | other();
|
||||
35 | other();
|
||||
|
||||
hint: give the implementation the method's parameter and return types, or discard the result
|
||||
|
||||
mock-fences.ts:35:31 - error SC2020: 't.mock.method with an options object' is part of the standard library types but has no scriptc lowering yet
|
||||
mock-fences.ts:39:31 - error SC2020: 't.mock.method with an options object' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
34 | t.mock.method(obj, "value"); // lowers
|
||||
35 | t.mock.method(obj, "value", { times: 2 }); // options
|
||||
38 | t.mock.method(obj, "value"); // lowers
|
||||
39 | t.mock.method(obj, "value", { times: 2 }); // options
|
||||
| ^~~~~~~~~~~~
|
||||
36 | });
|
||||
40 | });
|
||||
|
||||
hint: mock options (times, getter and setter) have no lowering
|
||||
|
||||
mock-fences.ts:38:14 - error SC2020: 'mock.fn of a function returning 'number | string'' is part of the standard library types but has no scriptc lowering yet
|
||||
mock-fences.ts:42:14 - error SC2020: 'mock.fn of a function returning 'number | string'' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
37 |
|
||||
38 | const pick = mock.fn((flag: boolean): number | string => (flag ? 1 : "one")); // union without undefined
|
||||
41 |
|
||||
42 | const pick = mock.fn((flag: boolean): number | string => (flag ? 1 : "one")); // union without undefined
|
||||
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
39 | pick(true);
|
||||
43 | pick(true);
|
||||
|
||||
hint: a union return type needs an undefined arm here (the calls log records undefined for calls that throw)
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3028, "san": 5470 }
|
||||
Reference in New Issue
Block a user