mirror of
https://github.com/bendtherules/ask262.git
synced 2026-08-19 00:31:06 +00:00
Squashed 'engine262/' content from commit ae71998
git-subtree-dir: engine262 git-subtree-split: ae71998cc5a8315700555135b1ac202a0d6d0b31
This commit is contained in:
+177
@@ -0,0 +1,177 @@
|
||||
import fs from 'node:fs';
|
||||
import { loadImportedModuleSync } from '../lib-src/node/module.mts';
|
||||
import { supportColor, type SkipReason } from './tui.mts';
|
||||
import {
|
||||
Agent, ManagedRealm, type OrdinaryObject, SourceTextModuleRecord, OrdinaryObjectCreate, createTest262Intrinsics,
|
||||
Value,
|
||||
} from '#self';
|
||||
|
||||
export interface Attrs {
|
||||
description: string;
|
||||
features?: string[];
|
||||
includes: string[];
|
||||
flags: {
|
||||
async?: boolean;
|
||||
module?: boolean;
|
||||
onlyStrict?: boolean;
|
||||
noStrict?: boolean;
|
||||
raw?: boolean;
|
||||
};
|
||||
negative: {
|
||||
type: string;
|
||||
phase: string;
|
||||
};
|
||||
}
|
||||
|
||||
export class Test {
|
||||
constructor(file: string, specifier: string, engineFeatures: readonly string[], attrs: Attrs, currentRunFlags: string, contents: string) {
|
||||
this.file = file;
|
||||
this.specifier = specifier;
|
||||
this.engineFeatures = engineFeatures;
|
||||
this.attrs = attrs;
|
||||
this.content = contents;
|
||||
this.currentTestFlag = currentRunFlags;
|
||||
}
|
||||
|
||||
startTime: number | null = null;
|
||||
|
||||
endTime: number | null = null;
|
||||
|
||||
getRuntimeSeconds(): number {
|
||||
if (this.startTime === null) {
|
||||
return 0;
|
||||
}
|
||||
return ~~((Date.now() - this.startTime) / 1000);
|
||||
}
|
||||
|
||||
id = Math.random();
|
||||
|
||||
file: string;
|
||||
|
||||
specifier: string;
|
||||
|
||||
attrs: Attrs;
|
||||
|
||||
engineFeatures: readonly string[];
|
||||
|
||||
content: string;
|
||||
|
||||
currentTestFlag: string;
|
||||
|
||||
status: 'pending' | 'skipped' | 'running' | 'passed' | 'failed' = 'pending';
|
||||
|
||||
skipReason: SkipReason | null = null;
|
||||
|
||||
skipFeature: string | null = null;
|
||||
|
||||
withDifferentTestFlag(newFlag: string, newContent = this.content) {
|
||||
return new Test(this.file, this.specifier, this.engineFeatures, this.attrs, newFlag, newContent);
|
||||
}
|
||||
}
|
||||
|
||||
export type SupervisorToWorker = Exclude<Test, 'withDifferentTestFlag'>
|
||||
export type WorkerToSupervisor_Running = {
|
||||
status: 'RUNNING';
|
||||
testId: number;
|
||||
};
|
||||
|
||||
export type WorkerToSupervisor_Pass = {
|
||||
status: 'PASS';
|
||||
file: string;
|
||||
flags: string;
|
||||
testId: number;
|
||||
};
|
||||
|
||||
export interface Stack {
|
||||
specifier?: string | null | undefined;
|
||||
source?: string;
|
||||
line: number;
|
||||
column: number;
|
||||
}
|
||||
|
||||
export type WorkerToSupervisor_Failed = {
|
||||
status: 'FAIL';
|
||||
file: string;
|
||||
flags: string;
|
||||
testId: number;
|
||||
description: string;
|
||||
error: string;
|
||||
stack: Stack[]
|
||||
};
|
||||
|
||||
export type WorkerToSupervisor =
|
||||
| WorkerToSupervisor_Running
|
||||
| WorkerToSupervisor_Pass
|
||||
| WorkerToSupervisor_Failed
|
||||
|
||||
export function readList(path: string | URL) {
|
||||
const source = fs.readFileSync(path, 'utf8');
|
||||
return source
|
||||
.split('\n')
|
||||
.filter((line) => line && !line.startsWith('#') && !line.startsWith(';'))
|
||||
.map((line) => line.split('#')[0].split(';')[0].trim());
|
||||
}
|
||||
|
||||
export interface CreateAgentOptions {
|
||||
features?: readonly string[];
|
||||
}
|
||||
|
||||
export function createAgent({ features = [] }: CreateAgentOptions) {
|
||||
const agent = new Agent({
|
||||
features,
|
||||
supportedImportAttributes: ['type'],
|
||||
loadImportedModule: loadImportedModuleSync,
|
||||
onDebugger() {
|
||||
// attach an empty debugger to make sure our debugger infrastructure does not break the engine
|
||||
agent.resumeEvaluate({ noBreakpoint: true });
|
||||
},
|
||||
});
|
||||
return agent;
|
||||
}
|
||||
|
||||
export interface Test262CreateRealm {
|
||||
realm: ManagedRealm;
|
||||
$262: OrdinaryObject;
|
||||
resolverCache: Map<string, SourceTextModuleRecord>;
|
||||
setPrintHandle: (callback: ((str: string, value: Value) => void) | undefined) => void;
|
||||
}
|
||||
export interface CreateRealmOptions {
|
||||
printCompatMode?: boolean;
|
||||
specifier?: string;
|
||||
}
|
||||
|
||||
export function createRealm({ printCompatMode = false, specifier }: CreateRealmOptions = {}): Test262CreateRealm {
|
||||
const resolverCache = new Map();
|
||||
|
||||
const realm = new ManagedRealm({
|
||||
resolverCache,
|
||||
specifier,
|
||||
});
|
||||
|
||||
return realm.scope(() => {
|
||||
const $262 = OrdinaryObjectCreate(realm.Intrinsics['%Object.prototype%']);
|
||||
const { setPrintHandle } = createTest262Intrinsics(realm, printCompatMode);
|
||||
return {
|
||||
realm,
|
||||
$262,
|
||||
resolverCache,
|
||||
setPrintHandle,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function fatal_exit(message: string): never {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
export function link(text: string, url: string | URL) {
|
||||
if (supportColor) {
|
||||
const OSC = '\u001B]';
|
||||
const BEL = '\u0007';
|
||||
return `${OSC}8;;${url}${BEL}${text}${OSC}8;;${BEL}`;
|
||||
} else {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { expect, test } from 'vitest';
|
||||
import {
|
||||
Agent, evalQ, Get, isPromiseObject, JSStringValue, ManagedRealm, NormalCompletion, setSurroundingAgent,
|
||||
skipDebugger,
|
||||
ThrowCompletion,
|
||||
unwrapCompletion,
|
||||
Value,
|
||||
type PromiseObject,
|
||||
} from '#self';
|
||||
|
||||
test('WeakRef (script)', () => {
|
||||
const agent = new Agent();
|
||||
setSurroundingAgent(agent);
|
||||
const realm = new ManagedRealm();
|
||||
const result = realm.evaluateScript(`
|
||||
const w = new WeakRef({});
|
||||
Promise.resolve()
|
||||
.then(() => {
|
||||
if (typeof w.deref() !== 'object') {
|
||||
throw new Error();
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
if (typeof w.deref() !== 'undefined') {
|
||||
throw new Error();
|
||||
}
|
||||
})
|
||||
.then(() => 'pass');
|
||||
`) as NormalCompletion<PromiseObject>;
|
||||
expect(result).toBeInstanceOf(NormalCompletion);
|
||||
expect(isPromiseObject(result.Value)).toBe(true);
|
||||
expect(result.Value.PromiseState).toBe('fulfilled');
|
||||
if (!(result.Value.PromiseResult instanceof JSStringValue)) {
|
||||
throw new Error('Expected JSStringValue');
|
||||
}
|
||||
expect(result.Value.PromiseResult.stringValue()).toBe('pass');
|
||||
});
|
||||
|
||||
test('WeakRef (module)', () => {
|
||||
const agent = new Agent();
|
||||
setSurroundingAgent(agent);
|
||||
const realm = new ManagedRealm();
|
||||
realm.scope(() => {
|
||||
const module = realm.compileModule(`
|
||||
const w = new WeakRef({});
|
||||
globalThis.result = Promise.resolve()
|
||||
.then(() => {
|
||||
if (typeof w.deref() !== 'object') {
|
||||
throw new Error('should be object');
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
if (typeof w.deref() !== 'undefined') {
|
||||
throw new Error('should be undefined');
|
||||
}
|
||||
})
|
||||
.then(() => 'pass');
|
||||
`, { specifier: 'test.mjs' });
|
||||
if (module instanceof ThrowCompletion) {
|
||||
throw new Error('Module compilation failed');
|
||||
}
|
||||
const completion = evalQ((_Q, X) => {
|
||||
const m = X(module);
|
||||
m.LoadRequestedModules();
|
||||
X(m.Link());
|
||||
skipDebugger(m.Evaluate());
|
||||
const result = X(skipDebugger(Get(realm.GlobalObject, Value('result')))) as PromiseObject;
|
||||
expect(isPromiseObject(result)).toBe(true);
|
||||
expect(result.PromiseState).toBe('fulfilled');
|
||||
if (!(result.PromiseResult instanceof JSStringValue)) {
|
||||
throw new Error('Expected JSStringValue');
|
||||
}
|
||||
expect(result.PromiseResult.stringValue()).toBe('pass');
|
||||
});
|
||||
unwrapCompletion(completion);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
/* eslint-disable quotes */
|
||||
import { expect, test } from 'vitest';
|
||||
import {
|
||||
Agent, evalQ, ManagedRealm, NormalCompletion, NumberValue, R, setSurroundingAgent,
|
||||
surroundingAgent,
|
||||
UndefinedValue,
|
||||
Value,
|
||||
type ValueCompletion,
|
||||
} from '#self';
|
||||
|
||||
test('debugger statement should return undefined when no debugger is attached', async () => {
|
||||
const agent = new Agent();
|
||||
setSurroundingAgent(agent);
|
||||
const realm = new ManagedRealm();
|
||||
const result = realm.evaluateScript('debugger;') as NormalCompletion<UndefinedValue>;
|
||||
expect(result).toBeInstanceOf(NormalCompletion);
|
||||
expect(result.Value).toBe(Value.undefined);
|
||||
});
|
||||
|
||||
test('debugger statement should return the value passed to resumeEvaluate', async () => {
|
||||
const agent = new Agent({
|
||||
onDebugger() {
|
||||
},
|
||||
});
|
||||
setSurroundingAgent(agent);
|
||||
const realm = new ManagedRealm();
|
||||
evalQ((_Q, X) => {
|
||||
const script = X(realm.compileScript('debugger;'));
|
||||
let completion!: ValueCompletion;
|
||||
realm.evaluate(script, (c) => {
|
||||
completion = c;
|
||||
});
|
||||
// start the evaluation
|
||||
X(surroundingAgent.resumeEvaluate({}));
|
||||
// paused at the debugger statement, resume with a value
|
||||
X(surroundingAgent.resumeEvaluate({
|
||||
debuggerStatementCompletion: NormalCompletion(Value(42)),
|
||||
}));
|
||||
expect(completion).toBeDefined();
|
||||
const value = X(completion) as NumberValue;
|
||||
expect(value).toBeInstanceOf(NumberValue);
|
||||
expect(R(value)).toBe(42);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
import { expect, test } from 'vitest';
|
||||
import {
|
||||
Agent, isPromiseObject, JSStringValue, ManagedRealm, NormalCompletion, setSurroundingAgent,
|
||||
type PromiseObject,
|
||||
} from '#self';
|
||||
|
||||
test('stack', () => {
|
||||
const agent = new Agent();
|
||||
setSurroundingAgent(agent);
|
||||
const realm = new ManagedRealm();
|
||||
const result = realm.evaluateScript(`
|
||||
function x() { throw new Error('owo'); }
|
||||
function y() { x(); }
|
||||
try {
|
||||
y();
|
||||
} catch (e) {
|
||||
e.stack;
|
||||
}
|
||||
`) as NormalCompletion<JSStringValue>;
|
||||
expect(result).toBeInstanceOf(NormalCompletion);
|
||||
expect(result.Value).toBeInstanceOf(JSStringValue);
|
||||
expect(result.Value.stringValue()).toMatchInlineSnapshot(`
|
||||
"Error: owo
|
||||
at x (<anonymous>:2:36)
|
||||
at y (<anonymous>:3:20)
|
||||
at <anonymous>:5:7"
|
||||
`);
|
||||
});
|
||||
|
||||
test('async stack', () => {
|
||||
const agent = new Agent();
|
||||
setSurroundingAgent(agent);
|
||||
const realm = new ManagedRealm();
|
||||
const result = realm.evaluateScript(`
|
||||
async function x() { await 1; throw new Error('owo'); }
|
||||
async function y() { await x(); }
|
||||
y().catch((e) => e.stack);
|
||||
`) as NormalCompletion<PromiseObject>;
|
||||
expect(result).toBeInstanceOf(NormalCompletion);
|
||||
expect(isPromiseObject(result.Value)).toBe(true);
|
||||
expect(result.Value.PromiseState).toBe('fulfilled');
|
||||
if (!(result.Value.PromiseResult instanceof JSStringValue)) {
|
||||
throw new Error('Expected JSStringValue');
|
||||
}
|
||||
expect(result.Value.PromiseResult.stringValue()).toMatchInlineSnapshot(`
|
||||
"Error: owo
|
||||
at async x (<anonymous>:2:51)
|
||||
at async y (<anonymous>:3:32)"
|
||||
`);
|
||||
});
|
||||
|
||||
test('native stack', () => {
|
||||
const agent = new Agent();
|
||||
setSurroundingAgent(agent);
|
||||
const realm = new ManagedRealm();
|
||||
const result = realm.evaluateScript(`
|
||||
function x() { Reflect.get(); }
|
||||
try {
|
||||
x();
|
||||
} catch (e) {
|
||||
e.stack;
|
||||
}
|
||||
`) as NormalCompletion<JSStringValue>;
|
||||
expect(result).toBeInstanceOf(NormalCompletion);
|
||||
expect(result.Value).toBeInstanceOf(JSStringValue);
|
||||
expect(result.Value.stringValue()).toMatchInlineSnapshot(`
|
||||
"TypeError: undefined is not an object
|
||||
at get (native)
|
||||
at x (<anonymous>:2:20)
|
||||
at <anonymous>:4:7"
|
||||
`);
|
||||
});
|
||||
@@ -0,0 +1,119 @@
|
||||
import { assert, expect, test } from 'vitest';
|
||||
import {
|
||||
AbstractModuleRecord, Agent, Call, JSStringValue, ManagedRealm, NewPromiseCapability, NormalCompletion, PromiseCapabilityRecord, setSurroundingAgent, skipDebugger, Value, type PromiseObject,
|
||||
} from '#self';
|
||||
|
||||
test('Import attributes', () => {
|
||||
let attributes!: Map<string, string>;
|
||||
let calls = 0;
|
||||
|
||||
const agent = new Agent({
|
||||
supportedImportAttributes: ['fruit', 'animal'],
|
||||
loadImportedModule: (_referrer, _specifier, attrs, _hostDefined, finish) => {
|
||||
calls += 1;
|
||||
attributes = attrs;
|
||||
finish(realm.compileModule(''));
|
||||
},
|
||||
});
|
||||
setSurroundingAgent(agent);
|
||||
const realm = new ManagedRealm();
|
||||
|
||||
realm.evaluateModule('import "test" with {}', 'case 1');
|
||||
expect([...attributes]).lengthOf(0);
|
||||
|
||||
realm.evaluateModule('import "test" with { fruit: "banana" }', 'case 2');
|
||||
expect([...attributes]).deep.equal([['fruit', 'banana']]);
|
||||
|
||||
realm.evaluateModule('import "test" with { fruit: "banana", animal: "monkey" }', 'case 3');
|
||||
expect([...attributes]).deep.equal([['animal', 'monkey'], ['fruit', 'banana']]);
|
||||
|
||||
realm.evaluateModule('import "test" with { animal: "monkey", fruit: "banana" }', 'case 4');
|
||||
expect([...attributes]).deep.equal([['animal', 'monkey'], ['fruit', 'banana']]);
|
||||
|
||||
calls = 0;
|
||||
realm.evaluateModule('import "test" with { fruit: "banana" }; import "test" with { fruit: "banana" }', 'case 5');
|
||||
expect(calls).toBe(1);
|
||||
|
||||
calls = 0;
|
||||
realm.evaluateModule('import "test" with { fruit: "banana" }; import "test" with { animal: "monkey" }', 'case 6');
|
||||
expect(calls).toBe(2);
|
||||
|
||||
calls = 0;
|
||||
realm.evaluateModule('import "test" with { fruit: "banana", animal: "monkey" }; import "test" with { animal: "monkey", fruit: "banana" };', 'case 7');
|
||||
expect(calls).toBe(1);
|
||||
|
||||
calls = 0;
|
||||
realm.evaluateModule('import "test" with { animal: "monkey" }; import "test" with { animal: "elephant" };', 'case 8');
|
||||
expect(calls).toBe(2);
|
||||
|
||||
calls = 0;
|
||||
realm.evaluateModule('import "test"; import "test" with {};', 'case 9');
|
||||
expect(calls).toBe(1);
|
||||
});
|
||||
|
||||
test('Custom module records', () => {
|
||||
let evaluationPromise: PromiseObject;
|
||||
|
||||
class CustomModuleRecord extends AbstractModuleRecord {
|
||||
_pc(): PromiseCapabilityRecord {
|
||||
const it = NewPromiseCapability(this.Realm.Intrinsics['%Promise%']);
|
||||
const completion = skipDebugger(it) as NormalCompletion<PromiseCapabilityRecord>;
|
||||
if (completion.Type !== 'normal') {
|
||||
throw new Error('Expected normal completion');
|
||||
}
|
||||
return completion.Value;
|
||||
}
|
||||
|
||||
override LoadRequestedModules(): PromiseObject {
|
||||
const pc = this._pc();
|
||||
Call(pc.Resolve, Value.undefined, []);
|
||||
return pc.Promise;
|
||||
}
|
||||
|
||||
override Link() {}
|
||||
|
||||
override* Evaluate() {
|
||||
const pc = this._pc();
|
||||
yield* Call(pc.Reject, Value.undefined, [Value('error!')]);
|
||||
evaluationPromise = pc.Promise;
|
||||
return evaluationPromise;
|
||||
}
|
||||
|
||||
override GetExportedNames(): readonly JSStringValue[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
override ResolveExport(): never {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
}
|
||||
|
||||
const agent = new Agent({
|
||||
loadImportedModule(referrer, specifier, _attributes, _hostDefined, finish) {
|
||||
if (specifier !== 'dep') {
|
||||
throw new Error('Invalid specifier');
|
||||
}
|
||||
finish(new CustomModuleRecord({
|
||||
Realm: (referrer as AbstractModuleRecord).Realm,
|
||||
Environment: undefined,
|
||||
Namespace: undefined,
|
||||
HostDefined: {},
|
||||
}));
|
||||
},
|
||||
});
|
||||
setSurroundingAgent(agent);
|
||||
|
||||
const calls: unknown[] = [];
|
||||
|
||||
const realm = new ManagedRealm({
|
||||
promiseRejectionTracker(promise, operation) {
|
||||
calls.push([promise, operation]);
|
||||
},
|
||||
});
|
||||
|
||||
realm.evaluateModule('import "dep"', 'entrypoint');
|
||||
|
||||
assert(calls.length >= 2); // there is a third call, for the promise of the entrypoint
|
||||
assert.deepStrictEqual(calls[0], [evaluationPromise!, 'reject'], "first call should be 'reject'");
|
||||
assert.deepStrictEqual(calls[1], [evaluationPromise!, 'handle'], "second call should be 'handle'");
|
||||
});
|
||||
@@ -0,0 +1,93 @@
|
||||
import { expect, test } from 'vitest';
|
||||
import { createAgent, createRealm } from '../base.mts';
|
||||
import {
|
||||
CreateArrayFromList, CreateBuiltinFunction, CreateDataProperty, EnsureCompletion, FEATURES, NormalCompletion, setSurroundingAgent, skipDebugger, ToString, UndefinedValue, Value, type Arguments,
|
||||
} from '#self';
|
||||
|
||||
test('Every built-in function should have a section property', () => {
|
||||
const agent = createAgent({
|
||||
features: FEATURES.map((f) => f.name),
|
||||
});
|
||||
setSurroundingAgent(agent);
|
||||
const { realm } = createRealm();
|
||||
realm.scope(() => {
|
||||
skipDebugger(CreateDataProperty(
|
||||
realm.GlobalObject,
|
||||
Value('fail'),
|
||||
CreateBuiltinFunction(([path = Value.undefined]: Arguments) => {
|
||||
const o = EnsureCompletion(skipDebugger(ToString(path)));
|
||||
if (o.Type === 'throw') {
|
||||
return o;
|
||||
}
|
||||
throw new Error(`${o.Value.stringValue()} did not have a section`);
|
||||
}, 1, Value(''), []),
|
||||
));
|
||||
const targets: Value[] = [];
|
||||
Object.entries(realm.Intrinsics)
|
||||
.forEach(([k, v]) => {
|
||||
targets.push(CreateArrayFromList([Value(k), v]));
|
||||
});
|
||||
skipDebugger(CreateDataProperty(
|
||||
realm.GlobalObject,
|
||||
Value('targets'),
|
||||
CreateArrayFromList(targets),
|
||||
));
|
||||
});
|
||||
const result = realm.evaluateScript(`
|
||||
'use strict';
|
||||
|
||||
{
|
||||
const targets = globalThis.targets;
|
||||
delete globalThis.targets;
|
||||
const fail = globalThis.fail;
|
||||
delete globalThis.fail;
|
||||
|
||||
const topQueue = new Set();
|
||||
const scanned = new Set();
|
||||
const scan = (ns, path) => {
|
||||
if (scanned.has(ns)) {
|
||||
return;
|
||||
}
|
||||
scanned.add(ns);
|
||||
if (typeof ns === 'function') {
|
||||
if ($262.spec(ns) === undefined) {
|
||||
fail(path);
|
||||
}
|
||||
}
|
||||
if (typeof ns !== 'function' && (typeof ns !== 'object' || ns === null)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const descriptors = Object.getOwnPropertyDescriptors(ns);
|
||||
Reflect.ownKeys(descriptors)
|
||||
.forEach((name) => {
|
||||
const desc = descriptors[name];
|
||||
const p = typeof name === 'symbol'
|
||||
? path + '[Symbol(' + name.description + ')]'
|
||||
: path + '.' + name;
|
||||
if ('value' in desc) {
|
||||
if (!topQueue.has(desc.value)) {
|
||||
scan(desc.value, p);
|
||||
}
|
||||
} else {
|
||||
if (!topQueue.has(desc.get)) {
|
||||
scan(desc.get, p);
|
||||
}
|
||||
if (!topQueue.has(desc.set)) {
|
||||
scan(desc.set, p);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
targets.forEach((t) => {
|
||||
topQueue.add(t[1]);
|
||||
});
|
||||
targets.forEach((t) => {
|
||||
scan(t[1], t[0]);
|
||||
});
|
||||
}
|
||||
`) as NormalCompletion<UndefinedValue>;
|
||||
expect(result).toBeInstanceOf(NormalCompletion);
|
||||
expect(result.Value).toBe(Value.undefined);
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
import mathematicalValue from './mathematical-value.mjs';
|
||||
import safeFunctionWithQ from './safe-function-with-q.mjs';
|
||||
import noFloatingGenerator from './no-floating-generator.mjs';
|
||||
|
||||
export const rules = {
|
||||
'mathematical-value': mathematicalValue,
|
||||
'safe-function-with-q': safeFunctionWithQ,
|
||||
'no-floating-generator': noFloatingGenerator,
|
||||
};
|
||||
@@ -0,0 +1,206 @@
|
||||
import path from 'node:path';
|
||||
import type { Rule, Scope } from 'eslint';
|
||||
import type * as ESTree from 'estree';
|
||||
|
||||
export default {
|
||||
meta: {
|
||||
fixable: 'code',
|
||||
hasSuggestions: true,
|
||||
},
|
||||
create(context) {
|
||||
type FixableCallExpression = ESTree.CallExpression & { callee: ESTree.MemberExpression & { computed: false, property: ESTree.Identifier } };
|
||||
|
||||
let needsImportForR: { node: FixableCallExpression, fixable: boolean, reachable: boolean, name: string }[];
|
||||
let importSpecifiersForR: ESTree.ImportSpecifier[];
|
||||
let importForAllModule: ESTree.ImportDeclaration | undefined;
|
||||
let importForSpecTypesModule: ESTree.ImportDeclaration | undefined;
|
||||
let lastImport: ESTree.ImportDeclaration | undefined;
|
||||
const pathToAbstractOps = `${path.resolve(context.cwd, 'src/abstract-ops').replaceAll('\\', '/')}/`;
|
||||
const pathToSpecTypesModule = path.resolve(pathToAbstractOps, 'spec-types.mjs').replaceAll('\\', '/');
|
||||
const pathToAllModule = path.resolve(pathToAbstractOps, 'all.mjs').replaceAll('\\', '/');
|
||||
|
||||
return {
|
||||
Program() {
|
||||
needsImportForR = [];
|
||||
importSpecifiersForR = [];
|
||||
importForAllModule = undefined;
|
||||
importForSpecTypesModule = undefined;
|
||||
lastImport = undefined;
|
||||
},
|
||||
'Program:exit': function Program_exit(node) {
|
||||
for (const importName of ['R', 'MathematicalValue']) {
|
||||
const needsImportForRNonFixable = needsImportForR.filter((entry) => !entry.fixable && entry.reachable && entry.name === importName);
|
||||
if (needsImportForRNonFixable.length) {
|
||||
// If some calls aren't fixable and there are no imports of 'R', report the need to import 'R'.
|
||||
// Include a fix, if possible.
|
||||
const importNamePart = importName === 'R' ? 'R' : `R (imported as ${importName})`;
|
||||
const importSpecifier = importName === 'R' ? 'R' : `R as ${importName}`;
|
||||
const fixable = !lookup(context.sourceCode.getScope(node), importName);
|
||||
const fix: Rule.ReportFixer = function* fix(fixer) {
|
||||
if (importForSpecTypesModule) {
|
||||
const last = importForSpecTypesModule.specifiers.at(-1)!;
|
||||
yield fixer.insertTextAfter(last, `, ${importSpecifier}`);
|
||||
} else if (importForAllModule) {
|
||||
const last = importForAllModule.specifiers.at(-1)!;
|
||||
yield fixer.insertTextAfter(last, `, ${importSpecifier}`);
|
||||
} else {
|
||||
const filename = path.resolve(context.filename).replaceAll('\\', '/');
|
||||
let relativePath;
|
||||
if (filename.startsWith(pathToAbstractOps)) {
|
||||
relativePath = path.relative(path.dirname(filename), pathToSpecTypesModule).replaceAll('\\', '/');
|
||||
} else {
|
||||
relativePath = path.relative(path.dirname(filename), pathToAllModule).replaceAll('\\', '/');
|
||||
}
|
||||
if (!path.isAbsolute(relativePath)
|
||||
&& !relativePath.startsWith('../')
|
||||
&& !relativePath.startsWith('./')) {
|
||||
relativePath = `./${relativePath}`;
|
||||
}
|
||||
if (lastImport) {
|
||||
yield fixer.insertTextAfter(lastImport, `\nimport { ${importSpecifier} } from ${JSON.stringify(relativePath)};`);
|
||||
} else {
|
||||
yield fixer.insertTextAfterRange([0, 0], `import { ${importSpecifier} } from ${JSON.stringify(relativePath)};\n`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
context.report({
|
||||
node: needsImportForRNonFixable[0].node,
|
||||
message: `Import ${importNamePart} to convert mathematical values`,
|
||||
fix: fixable ? fix : undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const { node: callNode, fixable, name } of needsImportForR) {
|
||||
const fix: Rule.ReportFixer = function* fix(fixer) {
|
||||
// foo.numberValue()
|
||||
// -> foo
|
||||
yield fixer.removeRange([callNode.callee.object.range![1], callNode.range![1]]);
|
||||
|
||||
// foo
|
||||
// -> R(foo)
|
||||
yield fixer.insertTextBefore(callNode, `${name}(`);
|
||||
yield fixer.insertTextAfter(callNode, ')');
|
||||
};
|
||||
|
||||
// Report the need to use 'R'. Include a fix, if possible.
|
||||
const namePart = name === 'R' ? 'R' : `R (imported as ${name})`;
|
||||
const methodNamePart = callNode.callee.property.name;
|
||||
context.report({
|
||||
node: callNode.callee,
|
||||
message: `Use ${namePart}, not .${methodNamePart}(), to get a mathematical value`,
|
||||
fix: fixable ? fix : undefined,
|
||||
});
|
||||
}
|
||||
},
|
||||
ImportDeclaration(node) {
|
||||
lastImport = node;
|
||||
switch (isImportOfRModule(node)) {
|
||||
case 'spec-types':
|
||||
importForSpecTypesModule ??= node;
|
||||
break;
|
||||
case 'all':
|
||||
importForAllModule ??= node;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
ImportSpecifier(node) {
|
||||
if (isImportOfR(node)) {
|
||||
importSpecifiersForR.push(node);
|
||||
}
|
||||
},
|
||||
CallExpression(node) {
|
||||
if (node.callee.type === 'MemberExpression'
|
||||
&& node.callee.computed === false
|
||||
&& node.callee.property.type === 'Identifier') {
|
||||
if (node.callee.property.name === 'numberValue'
|
||||
|| node.callee.property.name === 'bigintValue') {
|
||||
const { fixable, reachable, name } = getUsableReferenceToR(context.sourceCode.getScope(node));
|
||||
needsImportForR.push({
|
||||
node: node as FixableCallExpression, fixable, reachable, name,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
function lookup(scope: Scope.Scope | null, name: string) {
|
||||
while (scope) {
|
||||
const v = scope.set.get(name);
|
||||
if (v) {
|
||||
return v;
|
||||
}
|
||||
scope = scope.upper;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function isImportOfRModule(node: ESTree.ImportDeclaration) {
|
||||
if (!node.specifiers.length) {
|
||||
// `import {} from ...` not currently usable
|
||||
}
|
||||
if (node.specifiers.length && node.specifiers[0].type === 'ImportNamespaceSpecifier') {
|
||||
// `import * as ns from ...` not currently usable
|
||||
return false;
|
||||
}
|
||||
if (node.specifiers.length && node.specifiers[0].type === 'ImportDefaultSpecifier') {
|
||||
// `import X from ...` and `import X, {} from ...` not currently usable
|
||||
return false;
|
||||
}
|
||||
const importPath = path.resolve(path.dirname(context.filename), node.source.value as string).replaceAll('\\', '/');
|
||||
if (importPath === pathToAllModule) {
|
||||
return 'all';
|
||||
} else if (importPath === pathToSpecTypesModule) {
|
||||
return 'spec-types';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isImportOfR(node: ESTree.ImportSpecifier & Rule.NodeParentExtension) {
|
||||
if ((node.imported as ESTree.Identifier).name === 'R'
|
||||
&& node.parent.type === 'ImportDeclaration') {
|
||||
return !!isImportOfRModule(node.parent);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function getUsableReferenceToR(scope: Scope.Scope) {
|
||||
let candidate;
|
||||
for (const spec of importSpecifiersForR) {
|
||||
const varDecl = lookup(scope, spec.local.name);
|
||||
if (varDecl?.defs.some((def) => def.type === 'ImportBinding' && def.node === spec)) {
|
||||
if (spec.local.name === 'R') {
|
||||
// prefer 'R' if it is found
|
||||
return { fixable: true, reachable: true, name: spec.local.name };
|
||||
}
|
||||
if (spec.local.name === 'MathematicalValue') {
|
||||
candidate = 'MathematicalValue';
|
||||
} else {
|
||||
candidate ??= spec.local.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if we found a candidate, return it
|
||||
if (candidate) {
|
||||
return { fixable: true, reachable: true, name: candidate };
|
||||
}
|
||||
|
||||
// if no imports were found, try R
|
||||
if (!lookup(scope, 'R')) {
|
||||
return { fixable: false, reachable: true, name: 'R' };
|
||||
}
|
||||
|
||||
// if R isn't reachable, try MathematicalValue
|
||||
if (!lookup(scope, 'MathematicalValue')) {
|
||||
return { fixable: false, reachable: true, name: 'MathematicalValue' };
|
||||
}
|
||||
|
||||
// no imports were found or usable
|
||||
return { fixable: false, reachable: false, name: 'R' };
|
||||
}
|
||||
},
|
||||
} satisfies Rule.RuleModule;
|
||||
@@ -0,0 +1,69 @@
|
||||
import type { Rule } from 'eslint';
|
||||
import type { ParserServices } from '@typescript-eslint/parser';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import ts from 'typescript';
|
||||
|
||||
declare module 'typescript' {
|
||||
interface Type {
|
||||
typeArguments?: ts.Type[];
|
||||
}
|
||||
}
|
||||
|
||||
const rule = {
|
||||
meta: {
|
||||
messages: {
|
||||
floating: 'Generator is not stepped. It should be yield* evaluator',
|
||||
},
|
||||
fixable: 'code',
|
||||
},
|
||||
create(context) {
|
||||
const services = getParserServices(context);
|
||||
if (!services.program) {
|
||||
throw new Error('No ts program found');
|
||||
}
|
||||
const checker = services.program.getTypeChecker();
|
||||
const GeneratorSymbol = checker.resolveName('Generator', undefined, ts.SymbolFlags.Interface, false);
|
||||
const AsyncGeneratorSymbol = checker.resolveName('AsyncGenerator', undefined, ts.SymbolFlags.Interface, false);
|
||||
if (!GeneratorSymbol || !AsyncGeneratorSymbol) {
|
||||
throw new Error('Cannot find necessary symbols');
|
||||
}
|
||||
|
||||
return {
|
||||
'ExpressionStatement[expression.type="CallExpression"]':
|
||||
(function VisitCallExpression({ expression }) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const callNode = services.esTreeNodeToTSNodeMap.get(expression as any);
|
||||
if (!ts.isCallExpression(callNode)) {
|
||||
return;
|
||||
}
|
||||
const callType = checker.getTypeAtLocation(callNode);
|
||||
if (callType?.getSymbol() === GeneratorSymbol) {
|
||||
context.report({
|
||||
node: expression,
|
||||
messageId: 'floating',
|
||||
* fix(fixer) {
|
||||
yield fixer.insertTextBefore(expression, 'yield* ');
|
||||
},
|
||||
});
|
||||
}
|
||||
} satisfies Rule.RuleListener['ExpressionStatement']),
|
||||
};
|
||||
},
|
||||
} satisfies Rule.RuleModule;
|
||||
|
||||
export default rule;
|
||||
|
||||
function getParserServices(context: Rule.RuleContext): ParserServices {
|
||||
if (
|
||||
context.sourceCode.parserServices?.esTreeNodeToTSNodeMap == null
|
||||
|| context.sourceCode.parserServices.tsNodeToESTreeNodeMap == null
|
||||
) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
if (context.sourceCode.parserServices.program == null) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
return context.sourceCode.parserServices;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "@engine262/eslint-plugin",
|
||||
"version": "0.0.0",
|
||||
"main": "./lib/index.mjs",
|
||||
"private": true
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
import { resolve } from 'node:path';
|
||||
import type { Rule } from 'eslint';
|
||||
import type { ParserServices } from '@typescript-eslint/parser';
|
||||
import type { TSESTree } from '@typescript-eslint/types';
|
||||
import type * as ESTree from 'estree';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import ts from 'typescript';
|
||||
|
||||
const __dirname = import.meta.dirname;
|
||||
|
||||
declare module 'typescript' {
|
||||
interface Type {
|
||||
typeArguments?: ts.Type[];
|
||||
}
|
||||
}
|
||||
|
||||
const rule = {
|
||||
meta: {
|
||||
messages: {
|
||||
noAbruptCompletion: 'The function return type does not include AbruptCompletion',
|
||||
noThrowCompletion: 'The function return type does not include ThrowCompletion',
|
||||
noNeedToUseQ: 'Unnecessary Q() call',
|
||||
evaluator: 'It should be Q(yield* evaluator) instead of Q(evaluator)',
|
||||
},
|
||||
fixable: 'code',
|
||||
},
|
||||
create(context) {
|
||||
const services = getParserServices(context);
|
||||
if (!services.program) {
|
||||
throw new Error('No ts program found');
|
||||
}
|
||||
const checker = services.program.getTypeChecker();
|
||||
const CompletionFile = services.program.getSourceFile(resolve(__dirname, '../../../src/completion.mts'));
|
||||
const PromiseFile = services.program.getSourceFile(resolve(__dirname, '../../../src/intrinsics/Promise.mts'));
|
||||
if (!CompletionFile || !PromiseFile) {
|
||||
throw new Error('Cannot load src/completion.mts or src/intrinsics/Promise.mts');
|
||||
}
|
||||
const AbruptCompletion = CompletionFile.statements.find((s) => ts.isTypeAliasDeclaration(s) && s.name.text === 'AbruptCompletion');
|
||||
const ThrowCompletion = CompletionFile.statements.find((s) => ts.isTypeAliasDeclaration(s) && s.name.text === 'ThrowCompletion');
|
||||
const PromiseObject = PromiseFile.statements.find((s) => ts.isInterfaceDeclaration(s) && s.name.text === 'PromiseObject');
|
||||
const GeneratorSymbol = checker.resolveName('Generator', undefined, ts.SymbolFlags.Interface, false);
|
||||
if (!AbruptCompletion || !PromiseObject || !ThrowCompletion || !GeneratorSymbol) {
|
||||
throw new Error('Cannot find necessary symbols');
|
||||
}
|
||||
const AbruptCompletionType = checker.getTypeAtLocation(AbruptCompletion);
|
||||
const ThrowCompletionType = checker.getTypeAtLocation(ThrowCompletion);
|
||||
const PromiseObjectType = checker.getTypeAtLocation(PromiseObject);
|
||||
const reported = new WeakSet();
|
||||
|
||||
return {
|
||||
// eslint-disable-next-line func-names
|
||||
"CallExpression[callee.name='Q'],[callee.name='ReturnIfAbrupt'],[callee.name='IfAbruptRejectPromise'],[callee.name='IfAbruptCloseIterator']":
|
||||
(function (node) { // eslint-disable-line func-names
|
||||
const firstArg = node.arguments[0];
|
||||
if (firstArg?.type === 'SpreadElement') {
|
||||
return;
|
||||
}
|
||||
|
||||
const containingFunction = ts.findAncestor(services.esTreeNodeToTSNodeMap.get(node as TSESTree.Node), ts.isFunctionLike);
|
||||
if (!containingFunction) {
|
||||
throw new Error('Cannot find containing function');
|
||||
}
|
||||
|
||||
const firstArgType = checker.getTypeAtLocation(services.esTreeNodeToTSNodeMap.get(firstArg as TSESTree.Node));
|
||||
if (firstArgType?.getSymbol() === GeneratorSymbol) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'evaluator',
|
||||
* fix(fixer) {
|
||||
yield fixer.insertTextBefore(firstArg, 'yield* ');
|
||||
},
|
||||
});
|
||||
}
|
||||
const containingFunctionType = checker.getTypeAtLocation(containingFunction);
|
||||
if ((containingFunctionType.flags & ts.TypeFlags.Any) || (containingFunctionType.flags & ts.TypeFlags.Any)) {
|
||||
throw new Error('Unexpected any');
|
||||
}
|
||||
|
||||
let returnType = containingFunctionType.getCallSignatures().at(-1)?.getReturnType();
|
||||
if (ts.isMethodDeclaration(containingFunction) && ts.isIdentifier(containingFunction.name) && ts.isExpression(containingFunction.parent)) {
|
||||
const contextualObjectType = checker.getContextualType(containingFunction.parent);
|
||||
const currentFunctionName = containingFunction.name;
|
||||
if (contextualObjectType) {
|
||||
const contextualPropertySymbol = contextualObjectType.getProperty(currentFunctionName.text);
|
||||
if (contextualPropertySymbol) {
|
||||
let contextualPropertyType = checker.getTypeOfSymbol(contextualPropertySymbol);
|
||||
if (contextualPropertyType.isUnion()) {
|
||||
const excludeUndefined = contextualPropertyType.types.find((x) => x.flags & ~ts.TypeFlags.Undefined);
|
||||
if (excludeUndefined) {
|
||||
contextualPropertyType = excludeUndefined;
|
||||
}
|
||||
}
|
||||
returnType = contextualPropertyType.getCallSignatures().at(-1)?.getReturnType();
|
||||
// returnType && console.log('returnType', checker.typeToString(returnType));
|
||||
}
|
||||
}
|
||||
}
|
||||
// internal api. no api to insatiate the global Generator type.
|
||||
if (returnType?.getSymbol() === GeneratorSymbol && returnType?.typeArguments?.[1]) {
|
||||
returnType = returnType.typeArguments[1];
|
||||
}
|
||||
if (!returnType) {
|
||||
throw new Error('Cannot find return type');
|
||||
}
|
||||
|
||||
const f = (node.callee as ESTree.Identifier).name;
|
||||
let ExpectedReturnType;
|
||||
// const ExpectedReturnType = f === 'IfAbruptRejectPromise' ? PromiseObjectType : AbruptCompletionType;
|
||||
if (checker.isTypeAssignableTo(AbruptCompletionType, firstArgType)) {
|
||||
ExpectedReturnType = AbruptCompletionType;
|
||||
}
|
||||
if (checker.isTypeAssignableTo(ThrowCompletionType, firstArgType)) {
|
||||
ExpectedReturnType = ThrowCompletionType;
|
||||
}
|
||||
if (f === 'IfAbruptRejectPromise') {
|
||||
ExpectedReturnType = PromiseObjectType;
|
||||
}
|
||||
if (!ExpectedReturnType) {
|
||||
// context.report({
|
||||
// node,
|
||||
// messageId: 'noNeedToUseQ',
|
||||
// });
|
||||
return;
|
||||
}
|
||||
if (reported.has(containingFunction)) {
|
||||
return;
|
||||
}
|
||||
reported.add(containingFunction);
|
||||
if (!checker.isTypeAssignableTo(ExpectedReturnType, returnType)) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: ExpectedReturnType === AbruptCompletionType ? 'noAbruptCompletion' : 'noThrowCompletion',
|
||||
});
|
||||
}
|
||||
} satisfies Rule.RuleListener['CallExpression']),
|
||||
};
|
||||
},
|
||||
} satisfies Rule.RuleModule;
|
||||
|
||||
export default rule;
|
||||
|
||||
function getParserServices(context: Rule.RuleContext): ParserServices {
|
||||
if (
|
||||
context.sourceCode.parserServices?.esTreeNodeToTSNodeMap == null
|
||||
|| context.sourceCode.parserServices.tsNodeToESTreeNodeMap == null
|
||||
) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
if (context.sourceCode.parserServices.program == null) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
return context.sourceCode.parserServices;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./lib",
|
||||
"declaration": false,
|
||||
"sourceMap": false,
|
||||
"declarationMap": false,
|
||||
"erasableSyntaxOnly": true,
|
||||
"allowImportingTsExtensions": false,
|
||||
"strict": false
|
||||
},
|
||||
"include": ["./"]
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`preview evaluation > [] 1`] = `
|
||||
{
|
||||
"className": "Array",
|
||||
"description": "Array(0)",
|
||||
"objectId": "default:1",
|
||||
"preview": {
|
||||
"description": "Array(0)",
|
||||
"overflow": false,
|
||||
"properties": [],
|
||||
"subtype": "array",
|
||||
"type": "object",
|
||||
},
|
||||
"subtype": "array",
|
||||
"type": "object",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`preview evaluation > [1, 2, 3].map(x => x + 1) 1`] = `
|
||||
{
|
||||
"className": "Array",
|
||||
"description": "Array(3)",
|
||||
"objectId": "default:4",
|
||||
"preview": {
|
||||
"description": "Array(3)",
|
||||
"overflow": false,
|
||||
"properties": [
|
||||
{
|
||||
"name": "0",
|
||||
"type": "number",
|
||||
"value": "2",
|
||||
},
|
||||
{
|
||||
"name": "1",
|
||||
"type": "number",
|
||||
"value": "3",
|
||||
},
|
||||
{
|
||||
"name": "2",
|
||||
"type": "number",
|
||||
"value": "4",
|
||||
},
|
||||
],
|
||||
"subtype": "array",
|
||||
"type": "object",
|
||||
},
|
||||
"subtype": "array",
|
||||
"type": "object",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`preview evaluation > { let a = 1; a } 1`] = `
|
||||
{
|
||||
"description": "1",
|
||||
"type": "number",
|
||||
"value": 1,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`preview evaluation > { var a = 1; a } 1`] = `"side effect"`;
|
||||
|
||||
exports[`preview evaluation > 1 1`] = `
|
||||
{
|
||||
"description": "1",
|
||||
"type": "number",
|
||||
"value": 1,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`preview evaluation > globalThis.x = 1 1`] = `"side effect"`;
|
||||
@@ -0,0 +1,95 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`code in ShadowRealm 1`] = `
|
||||
[
|
||||
[
|
||||
{
|
||||
"columnNumber": 6,
|
||||
"functionName": "<anonymous>",
|
||||
"lineNumber": 0,
|
||||
"scriptId": "1",
|
||||
"url": "<anonymous>",
|
||||
},
|
||||
undefined,
|
||||
{
|
||||
"columnNumber": 15,
|
||||
"functionName": "<anonymous>",
|
||||
"lineNumber": 1,
|
||||
"scriptId": "0",
|
||||
"url": "<anonymous>",
|
||||
},
|
||||
],
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`code in eval 1`] = `
|
||||
[
|
||||
[
|
||||
{
|
||||
"columnNumber": 6,
|
||||
"functionName": "<anonymous>",
|
||||
"lineNumber": 0,
|
||||
"scriptId": "1",
|
||||
"url": "<anonymous>",
|
||||
},
|
||||
{
|
||||
"columnNumber": 5,
|
||||
"functionName": "<anonymous>",
|
||||
"lineNumber": 0,
|
||||
"scriptId": "0",
|
||||
"url": "<anonymous>",
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
"columnNumber": 12,
|
||||
"functionName": "f",
|
||||
"lineNumber": 1,
|
||||
"scriptId": "2",
|
||||
"url": "<anonymous>",
|
||||
},
|
||||
{
|
||||
"columnNumber": 4,
|
||||
"functionName": "<anonymous>",
|
||||
"lineNumber": 3,
|
||||
"scriptId": "2",
|
||||
"url": "<anonymous>",
|
||||
},
|
||||
{
|
||||
"columnNumber": 5,
|
||||
"functionName": "<anonymous>",
|
||||
"lineNumber": 1,
|
||||
"scriptId": "0",
|
||||
"url": "<anonymous>",
|
||||
},
|
||||
],
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`code in new Function 1`] = `
|
||||
[
|
||||
[
|
||||
{
|
||||
"columnNumber": 14,
|
||||
"functionName": "x",
|
||||
"lineNumber": 4,
|
||||
"scriptId": "1",
|
||||
"url": "<anonymous>",
|
||||
},
|
||||
{
|
||||
"columnNumber": 8,
|
||||
"functionName": "y",
|
||||
"lineNumber": 7,
|
||||
"scriptId": "1",
|
||||
"url": "<anonymous>",
|
||||
},
|
||||
{
|
||||
"columnNumber": 17,
|
||||
"functionName": "<anonymous>",
|
||||
"lineNumber": 1,
|
||||
"scriptId": "0",
|
||||
"url": "<anonymous>",
|
||||
},
|
||||
],
|
||||
]
|
||||
`;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,278 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
/* eslint-disable quotes */
|
||||
import { expect, test } from 'vitest';
|
||||
import { TestInspector } from './utils.mts';
|
||||
import { Agent, ManagedRealm, setSurroundingAgent } from '#self';
|
||||
|
||||
test('compile script (for invalid code break line)', async () => {
|
||||
const agent = new Agent();
|
||||
setSurroundingAgent(agent);
|
||||
const inspector = new TestInspector();
|
||||
const realm = new ManagedRealm();
|
||||
inspector.attachAgent(agent, [realm]);
|
||||
|
||||
const result = await inspector.runtime.compileScript({
|
||||
expression: 'function f() {',
|
||||
persistScript: false,
|
||||
sourceURL: '',
|
||||
executionContextId: 0,
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
{
|
||||
"exceptionDetails": {
|
||||
"columnNumber": 0,
|
||||
"exception": {
|
||||
"className": "SyntaxError",
|
||||
"description": "SyntaxError: Unexpected end of input",
|
||||
"objectId": "default:2",
|
||||
"preview": {
|
||||
"description": "
|
||||
function f() {
|
||||
^
|
||||
SyntaxError: Unexpected end of source",
|
||||
"entries": undefined,
|
||||
"overflow": false,
|
||||
"properties": [
|
||||
{
|
||||
"name": "message",
|
||||
"type": "string",
|
||||
"value": "Unexpected end of source",
|
||||
},
|
||||
{
|
||||
"name": "stack",
|
||||
"type": "string",
|
||||
"value": "
|
||||
function f() {
|
||||
^
|
||||
SyntaxError: Unexpected end of source",
|
||||
},
|
||||
],
|
||||
"subtype": "error",
|
||||
"type": "object",
|
||||
},
|
||||
"subtype": "error",
|
||||
"type": "object",
|
||||
},
|
||||
"exceptionId": 1,
|
||||
"lineNumber": 0,
|
||||
"scriptId": undefined,
|
||||
"stackTrace": {
|
||||
"callFrames": [],
|
||||
},
|
||||
"text": "Uncaught",
|
||||
"url": undefined,
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('preview evaluation', async () => {
|
||||
const agent = new Agent();
|
||||
setSurroundingAgent(agent);
|
||||
const inspector = new TestInspector();
|
||||
const realm = new ManagedRealm();
|
||||
inspector.attachAgent(agent, [realm]);
|
||||
|
||||
for (const code of [
|
||||
`1`,
|
||||
`[]`,
|
||||
`{ var a = 1; a }`,
|
||||
`{ let a = 1; a }`,
|
||||
`[1, 2, 3].map(x => x + 1)`,
|
||||
`globalThis.x = 1`,
|
||||
]) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const result: any = await inspector.perview(code);
|
||||
if (result.exceptionDetails?.exception.preview.properties[0].value === 'Preview evaluator cannot evaluate side-effecting code') {
|
||||
expect('side effect').toMatchSnapshot(code);
|
||||
} else {
|
||||
expect(result).toMatchSnapshot(code);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('get local lexical names', async () => {
|
||||
const agent = new Agent();
|
||||
setSurroundingAgent(agent);
|
||||
const inspector = new TestInspector();
|
||||
const realm = new ManagedRealm();
|
||||
inspector.attachAgent(agent, [realm]);
|
||||
|
||||
expect(await inspector.runtime.globalLexicalScopeNames({
|
||||
executionContextId: 0,
|
||||
})).toMatchInlineSnapshot(`
|
||||
{
|
||||
"names": [
|
||||
"Infinity",
|
||||
"NaN",
|
||||
"undefined",
|
||||
"globalThis",
|
||||
"eval",
|
||||
"isFinite",
|
||||
"isNaN",
|
||||
"parseFloat",
|
||||
"parseInt",
|
||||
"decodeURI",
|
||||
"decodeURIComponent",
|
||||
"encodeURI",
|
||||
"encodeURIComponent",
|
||||
"AggregateError",
|
||||
"Array",
|
||||
"ArrayBuffer",
|
||||
"Boolean",
|
||||
"BigInt",
|
||||
"BigInt64Array",
|
||||
"BigUint64Array",
|
||||
"DataView",
|
||||
"Date",
|
||||
"Error",
|
||||
"EvalError",
|
||||
"FinalizationRegistry",
|
||||
"Float32Array",
|
||||
"Float64Array",
|
||||
"Function",
|
||||
"Int8Array",
|
||||
"Int16Array",
|
||||
"Int32Array",
|
||||
"Iterator",
|
||||
"Map",
|
||||
"Number",
|
||||
"Object",
|
||||
"Promise",
|
||||
"Proxy",
|
||||
"RangeError",
|
||||
"ReferenceError",
|
||||
"RegExp",
|
||||
"Set",
|
||||
"ShadowRealm",
|
||||
"String",
|
||||
"Symbol",
|
||||
"SyntaxError",
|
||||
"TypeError",
|
||||
"Uint8Array",
|
||||
"Uint8ClampedArray",
|
||||
"Uint16Array",
|
||||
"Uint32Array",
|
||||
"URIError",
|
||||
"WeakMap",
|
||||
"WeakRef",
|
||||
"WeakSet",
|
||||
"JSON",
|
||||
"Math",
|
||||
"Reflect",
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('call function on', async () => {
|
||||
const agent = new Agent();
|
||||
setSurroundingAgent(agent);
|
||||
const inspector = new TestInspector();
|
||||
const realm = new ManagedRealm();
|
||||
inspector.attachAgent(agent, [realm]);
|
||||
|
||||
inspector.flush();
|
||||
await inspector.eval('const a = { x: 1 }; a');
|
||||
expect(inspector.flush()).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"method": "Debugger.scriptParsed",
|
||||
"params": {
|
||||
"buildId": "",
|
||||
"endColumn": 21,
|
||||
"endLine": 1,
|
||||
"executionContextId": 0,
|
||||
"hash": "",
|
||||
"isModule": false,
|
||||
"scriptId": "0",
|
||||
"startColumn": 0,
|
||||
"startLine": 0,
|
||||
"url": "vm:///0",
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": 0,
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {
|
||||
"expression": "const a = { x: 1 }; a",
|
||||
"uniqueContextId": "0",
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": 0,
|
||||
"result": {
|
||||
"exceptionDetails": undefined,
|
||||
"result": {
|
||||
"className": "Object",
|
||||
"description": "Object",
|
||||
"objectId": "default:1",
|
||||
"preview": {
|
||||
"description": "Object",
|
||||
"entries": undefined,
|
||||
"overflow": false,
|
||||
"properties": [
|
||||
{
|
||||
"name": "x",
|
||||
"type": "number",
|
||||
"value": "1",
|
||||
},
|
||||
],
|
||||
"subtype": undefined,
|
||||
"type": "object",
|
||||
},
|
||||
"subtype": undefined,
|
||||
"type": "object",
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
`);
|
||||
expect(await inspector.runtime.callFunctionOn({
|
||||
functionDeclaration: 'function (x) { return this.x + x }',
|
||||
arguments: [{ value: 1 }],
|
||||
executionContextId: 0,
|
||||
objectId: 'default:1',
|
||||
})).toMatchInlineSnapshot(`
|
||||
{
|
||||
"description": "2",
|
||||
"type": "number",
|
||||
"value": 2,
|
||||
}
|
||||
`);
|
||||
expect(await inspector.runtime.callFunctionOn({
|
||||
functionDeclaration: 'function (x) { return [this.x, x, 2, 3] }',
|
||||
arguments: [{ value: 1 }],
|
||||
executionContextId: 0,
|
||||
objectId: 'default:1',
|
||||
returnByValue: true,
|
||||
})).toMatchInlineSnapshot(`
|
||||
{
|
||||
"type": "object",
|
||||
"value": [
|
||||
1,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('private field jailbreak', async () => {
|
||||
const agent = new Agent();
|
||||
setSurroundingAgent(agent);
|
||||
const inspector = new TestInspector();
|
||||
const realm = new ManagedRealm();
|
||||
inspector.attachAgent(agent, [realm]);
|
||||
|
||||
await inspector.eval('class A { #x = 1; }; globalThis.a = new A();');
|
||||
await inspector.debugger.engine262_setEvaluateMode({ mode: 'console' });
|
||||
expect(await inspector.runtime.evaluate({ expression: 'a.#x', uniqueContextId: '0' })).toMatchInlineSnapshot(`
|
||||
{
|
||||
"description": "1",
|
||||
"type": "number",
|
||||
"value": 1,
|
||||
}
|
||||
`);
|
||||
});
|
||||
@@ -0,0 +1,489 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
/* eslint-disable quotes */
|
||||
import { expect, test } from 'vitest';
|
||||
import { TestInspector } from './utils.mts';
|
||||
import { Agent, ManagedRealm, setSurroundingAgent } from '#self';
|
||||
|
||||
test('evaluate on frame', async () => {
|
||||
const agent = new Agent();
|
||||
setSurroundingAgent(agent);
|
||||
const inspector = new TestInspector();
|
||||
const realm = new ManagedRealm();
|
||||
inspector.attachAgent(agent, [realm]);
|
||||
|
||||
const paused = inspector.eval(`
|
||||
'use strict';
|
||||
function f() {
|
||||
const a = 1;
|
||||
debugger;
|
||||
}
|
||||
function y() {
|
||||
const a = 0;
|
||||
f();
|
||||
}
|
||||
y();
|
||||
`);
|
||||
expect(inspector.flush()).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"method": "Runtime.executionContextCreated",
|
||||
"params": {
|
||||
"context": {
|
||||
"id": 0,
|
||||
"name": "engine262",
|
||||
"origin": "vm://repl",
|
||||
"uniqueId": "0",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"method": "Debugger.scriptParsed",
|
||||
"params": {
|
||||
"buildId": "",
|
||||
"endColumn": 2,
|
||||
"endLine": 12,
|
||||
"executionContextId": 0,
|
||||
"hash": "",
|
||||
"isModule": false,
|
||||
"scriptId": "0",
|
||||
"startColumn": 0,
|
||||
"startLine": 0,
|
||||
"url": "vm:///0",
|
||||
},
|
||||
},
|
||||
{
|
||||
"method": "Debugger.paused",
|
||||
"params": {
|
||||
"callFrames": [
|
||||
{
|
||||
"callFrameId": "3",
|
||||
"canBeRestarted": false,
|
||||
"functionLocation": {
|
||||
"columnNumber": 17,
|
||||
"lineNumber": 2,
|
||||
"scriptId": "0",
|
||||
},
|
||||
"functionName": "f",
|
||||
"location": {
|
||||
"columnNumber": 6,
|
||||
"lineNumber": 4,
|
||||
"scriptId": "0",
|
||||
},
|
||||
"scopeChain": [
|
||||
{
|
||||
"object": {
|
||||
"className": "Object",
|
||||
"description": "Object",
|
||||
"objectId": "default:1",
|
||||
"preview": {
|
||||
"description": "Object",
|
||||
"entries": undefined,
|
||||
"overflow": false,
|
||||
"properties": [
|
||||
{
|
||||
"name": "a",
|
||||
"type": "number",
|
||||
"value": "1",
|
||||
},
|
||||
],
|
||||
"subtype": undefined,
|
||||
"type": "object",
|
||||
},
|
||||
"subtype": undefined,
|
||||
"type": "object",
|
||||
},
|
||||
"type": "local",
|
||||
},
|
||||
{
|
||||
"object": {
|
||||
"className": "Object",
|
||||
"description": "Object",
|
||||
"objectId": "default:2",
|
||||
"preview": {
|
||||
"description": "Object",
|
||||
"entries": undefined,
|
||||
"overflow": true,
|
||||
"properties": [
|
||||
{
|
||||
"name": "Infinity",
|
||||
"type": "number",
|
||||
"value": "Infinity",
|
||||
},
|
||||
{
|
||||
"name": "NaN",
|
||||
"type": "number",
|
||||
"value": "NaN",
|
||||
},
|
||||
{
|
||||
"name": "undefined",
|
||||
"type": "undefined",
|
||||
"value": "undefined",
|
||||
},
|
||||
{
|
||||
"name": "globalThis",
|
||||
"subtype": undefined,
|
||||
"type": "object",
|
||||
"value": "Object",
|
||||
},
|
||||
{
|
||||
"name": "eval",
|
||||
"type": "function",
|
||||
"value": "",
|
||||
},
|
||||
{
|
||||
"name": "isFinite",
|
||||
"type": "function",
|
||||
"value": "",
|
||||
},
|
||||
],
|
||||
"subtype": undefined,
|
||||
"type": "object",
|
||||
},
|
||||
"subtype": undefined,
|
||||
"type": "object",
|
||||
},
|
||||
"type": "global",
|
||||
},
|
||||
],
|
||||
"this": {
|
||||
"type": "undefined",
|
||||
},
|
||||
"url": "",
|
||||
},
|
||||
{
|
||||
"callFrameId": "2",
|
||||
"canBeRestarted": false,
|
||||
"functionLocation": {
|
||||
"columnNumber": 17,
|
||||
"lineNumber": 6,
|
||||
"scriptId": "0",
|
||||
},
|
||||
"functionName": "y",
|
||||
"location": {
|
||||
"columnNumber": 6,
|
||||
"lineNumber": 8,
|
||||
"scriptId": "0",
|
||||
},
|
||||
"scopeChain": [
|
||||
{
|
||||
"object": {
|
||||
"className": "Object",
|
||||
"description": "Object",
|
||||
"objectId": "default:3",
|
||||
"preview": {
|
||||
"description": "Object",
|
||||
"entries": undefined,
|
||||
"overflow": false,
|
||||
"properties": [
|
||||
{
|
||||
"name": "a",
|
||||
"type": "number",
|
||||
"value": "0",
|
||||
},
|
||||
],
|
||||
"subtype": undefined,
|
||||
"type": "object",
|
||||
},
|
||||
"subtype": undefined,
|
||||
"type": "object",
|
||||
},
|
||||
"type": "local",
|
||||
},
|
||||
{
|
||||
"object": {
|
||||
"className": "Object",
|
||||
"description": "Object",
|
||||
"objectId": "default:2",
|
||||
"preview": {
|
||||
"description": "Object",
|
||||
"entries": undefined,
|
||||
"overflow": true,
|
||||
"properties": [
|
||||
{
|
||||
"name": "Infinity",
|
||||
"type": "number",
|
||||
"value": "Infinity",
|
||||
},
|
||||
{
|
||||
"name": "NaN",
|
||||
"type": "number",
|
||||
"value": "NaN",
|
||||
},
|
||||
{
|
||||
"name": "undefined",
|
||||
"type": "undefined",
|
||||
"value": "undefined",
|
||||
},
|
||||
{
|
||||
"name": "globalThis",
|
||||
"subtype": undefined,
|
||||
"type": "object",
|
||||
"value": "Object",
|
||||
},
|
||||
{
|
||||
"name": "eval",
|
||||
"type": "function",
|
||||
"value": "",
|
||||
},
|
||||
{
|
||||
"name": "isFinite",
|
||||
"type": "function",
|
||||
"value": "",
|
||||
},
|
||||
],
|
||||
"subtype": undefined,
|
||||
"type": "object",
|
||||
},
|
||||
"subtype": undefined,
|
||||
"type": "object",
|
||||
},
|
||||
"type": "global",
|
||||
},
|
||||
],
|
||||
"this": {
|
||||
"type": "undefined",
|
||||
},
|
||||
"url": "",
|
||||
},
|
||||
{
|
||||
"callFrameId": "1",
|
||||
"canBeRestarted": false,
|
||||
"functionLocation": undefined,
|
||||
"functionName": "<anonymous>",
|
||||
"location": {
|
||||
"columnNumber": 4,
|
||||
"lineNumber": 10,
|
||||
"scriptId": "0",
|
||||
},
|
||||
"scopeChain": [
|
||||
{
|
||||
"object": {
|
||||
"className": "Object",
|
||||
"description": "Object",
|
||||
"objectId": "default:2",
|
||||
"preview": {
|
||||
"description": "Object",
|
||||
"entries": undefined,
|
||||
"overflow": true,
|
||||
"properties": [
|
||||
{
|
||||
"name": "Infinity",
|
||||
"type": "number",
|
||||
"value": "Infinity",
|
||||
},
|
||||
{
|
||||
"name": "NaN",
|
||||
"type": "number",
|
||||
"value": "NaN",
|
||||
},
|
||||
{
|
||||
"name": "undefined",
|
||||
"type": "undefined",
|
||||
"value": "undefined",
|
||||
},
|
||||
{
|
||||
"name": "globalThis",
|
||||
"subtype": undefined,
|
||||
"type": "object",
|
||||
"value": "Object",
|
||||
},
|
||||
{
|
||||
"name": "eval",
|
||||
"type": "function",
|
||||
"value": "",
|
||||
},
|
||||
{
|
||||
"name": "isFinite",
|
||||
"type": "function",
|
||||
"value": "",
|
||||
},
|
||||
],
|
||||
"subtype": undefined,
|
||||
"type": "object",
|
||||
},
|
||||
"subtype": undefined,
|
||||
"type": "object",
|
||||
},
|
||||
"type": "global",
|
||||
},
|
||||
],
|
||||
"this": {
|
||||
"className": "Object",
|
||||
"description": "Object",
|
||||
"objectId": "default:2",
|
||||
"preview": {
|
||||
"description": "Object",
|
||||
"entries": undefined,
|
||||
"overflow": true,
|
||||
"properties": [
|
||||
{
|
||||
"name": "Infinity",
|
||||
"type": "number",
|
||||
"value": "Infinity",
|
||||
},
|
||||
{
|
||||
"name": "NaN",
|
||||
"type": "number",
|
||||
"value": "NaN",
|
||||
},
|
||||
{
|
||||
"name": "undefined",
|
||||
"type": "undefined",
|
||||
"value": "undefined",
|
||||
},
|
||||
{
|
||||
"name": "globalThis",
|
||||
"subtype": undefined,
|
||||
"type": "object",
|
||||
"value": "Object",
|
||||
},
|
||||
{
|
||||
"name": "eval",
|
||||
"type": "function",
|
||||
"value": "",
|
||||
},
|
||||
{
|
||||
"name": "isFinite",
|
||||
"type": "function",
|
||||
"value": "",
|
||||
},
|
||||
],
|
||||
"subtype": undefined,
|
||||
"type": "object",
|
||||
},
|
||||
"subtype": undefined,
|
||||
"type": "object",
|
||||
},
|
||||
"url": "",
|
||||
},
|
||||
],
|
||||
"reason": "debugCommand",
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": 0,
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {
|
||||
"expression": "
|
||||
'use strict';
|
||||
function f() {
|
||||
const a = 1;
|
||||
debugger;
|
||||
}
|
||||
function y() {
|
||||
const a = 0;
|
||||
f();
|
||||
}
|
||||
y();
|
||||
",
|
||||
"uniqueContextId": "0",
|
||||
},
|
||||
},
|
||||
]
|
||||
`);
|
||||
expect(await inspector.debugger.evaluateOnCallFrame({
|
||||
callFrameId: "3",
|
||||
expression: 'a',
|
||||
})).toMatchInlineSnapshot(`
|
||||
{
|
||||
"description": "1",
|
||||
"type": "number",
|
||||
"value": 1,
|
||||
}
|
||||
`);
|
||||
expect(await inspector.debugger.evaluateOnCallFrame({
|
||||
callFrameId: "2",
|
||||
expression: 'a',
|
||||
})).toMatchInlineSnapshot(`
|
||||
{
|
||||
"description": "0",
|
||||
"type": "number",
|
||||
"value": 0,
|
||||
}
|
||||
`);
|
||||
expect(await inspector.debugger.evaluateOnCallFrame({
|
||||
callFrameId: "1",
|
||||
expression: 'a',
|
||||
})).toMatchInlineSnapshot(`
|
||||
{
|
||||
"exceptionDetails": {
|
||||
"columnNumber": 0,
|
||||
"exception": {
|
||||
"className": "SyntaxError",
|
||||
"description": "ReferenceError: 'a' is not defined
|
||||
at <anonymous>:1:1
|
||||
at <anonymous>:11:5",
|
||||
"objectId": "default:5",
|
||||
"preview": {
|
||||
"description": "ReferenceError: 'a' is not defined
|
||||
at <anonymous>:1:1
|
||||
at <anonymous>:11:5",
|
||||
"entries": undefined,
|
||||
"overflow": false,
|
||||
"properties": [
|
||||
{
|
||||
"name": "message",
|
||||
"type": "string",
|
||||
"value": "'a' is not defined",
|
||||
},
|
||||
],
|
||||
"subtype": "error",
|
||||
"type": "object",
|
||||
},
|
||||
"subtype": "error",
|
||||
"type": "object",
|
||||
},
|
||||
"exceptionId": 4,
|
||||
"lineNumber": 0,
|
||||
"scriptId": "0",
|
||||
"stackTrace": {
|
||||
"callFrames": [
|
||||
{
|
||||
"columnNumber": 0,
|
||||
"functionName": "<anonymous>",
|
||||
"lineNumber": 0,
|
||||
"scriptId": "0",
|
||||
"url": "<anonymous>",
|
||||
},
|
||||
{
|
||||
"columnNumber": 4,
|
||||
"functionName": "<anonymous>",
|
||||
"lineNumber": 10,
|
||||
"scriptId": "0",
|
||||
"url": "<anonymous>",
|
||||
},
|
||||
],
|
||||
},
|
||||
"text": "Uncaught",
|
||||
"url": "<anonymous>",
|
||||
},
|
||||
"result": {
|
||||
"className": "SyntaxError",
|
||||
"description": "ReferenceError: 'a' is not defined
|
||||
at <anonymous>:1:1
|
||||
at <anonymous>:11:5",
|
||||
"objectId": "default:5",
|
||||
"preview": {
|
||||
"description": "ReferenceError: 'a' is not defined
|
||||
at <anonymous>:1:1
|
||||
at <anonymous>:11:5",
|
||||
"entries": undefined,
|
||||
"overflow": false,
|
||||
"properties": [
|
||||
{
|
||||
"name": "message",
|
||||
"type": "string",
|
||||
"value": "'a' is not defined",
|
||||
},
|
||||
],
|
||||
"subtype": "error",
|
||||
"type": "object",
|
||||
},
|
||||
"subtype": "error",
|
||||
"type": "object",
|
||||
},
|
||||
}
|
||||
`);
|
||||
await inspector.debugger.resume();
|
||||
await paused;
|
||||
});
|
||||
@@ -0,0 +1,224 @@
|
||||
import { expect, test } from 'vitest';
|
||||
import { TestInspector } from './utils.mts';
|
||||
import {
|
||||
Agent, ManagedRealm, runJobQueue, setSurroundingAgent,
|
||||
} from '#self';
|
||||
import { createConsole } from '#self/inspector';
|
||||
|
||||
test('console', async () => {
|
||||
const agent = new Agent();
|
||||
setSurroundingAgent(agent);
|
||||
const inspector = new TestInspector();
|
||||
const realm = new ManagedRealm();
|
||||
inspector.attachAgent(agent, [realm]);
|
||||
|
||||
let count = 0;
|
||||
createConsole(realm, {
|
||||
log(args) {
|
||||
count += args.length;
|
||||
},
|
||||
});
|
||||
|
||||
inspector.flush();
|
||||
await inspector.eval('console.log("hello", "world")');
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const a: any = inspector.flush();
|
||||
a[1].params.timestamp = 0;
|
||||
expect(a).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"method": "Debugger.scriptParsed",
|
||||
"params": {
|
||||
"buildId": "",
|
||||
"endColumn": 29,
|
||||
"endLine": 1,
|
||||
"executionContextId": 0,
|
||||
"hash": "",
|
||||
"isModule": false,
|
||||
"scriptId": "0",
|
||||
"startColumn": 0,
|
||||
"startLine": 0,
|
||||
"url": "vm:///0",
|
||||
},
|
||||
},
|
||||
{
|
||||
"method": "Runtime.consoleAPICalled",
|
||||
"params": {
|
||||
"args": [
|
||||
{
|
||||
"type": "string",
|
||||
"value": "hello",
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"value": "world",
|
||||
},
|
||||
],
|
||||
"executionContextId": 0,
|
||||
"timestamp": 0,
|
||||
"type": "log",
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": 0,
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {
|
||||
"expression": "console.log("hello", "world")",
|
||||
"uniqueContextId": "0",
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": 0,
|
||||
"result": {
|
||||
"exceptionDetails": undefined,
|
||||
"result": {
|
||||
"type": "undefined",
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
`);
|
||||
expect(count).eq(2);
|
||||
|
||||
await inspector.perview('console.log("hello", "world")');
|
||||
expect(count).eq(2);
|
||||
});
|
||||
|
||||
test('unhandled promise rejection', async () => {
|
||||
const agent = new Agent();
|
||||
setSurroundingAgent(agent);
|
||||
const inspector = new TestInspector();
|
||||
const realm = new ManagedRealm();
|
||||
inspector.attachAgent(agent, [realm]);
|
||||
|
||||
inspector.flush();
|
||||
await inspector.eval('var a = Promise.reject(new Error())');
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const i: any = inspector.flush();
|
||||
i[1].params.timestamp = 0;
|
||||
expect(i).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"method": "Debugger.scriptParsed",
|
||||
"params": {
|
||||
"buildId": "",
|
||||
"endColumn": 35,
|
||||
"endLine": 1,
|
||||
"executionContextId": 0,
|
||||
"hash": "",
|
||||
"isModule": false,
|
||||
"scriptId": "0",
|
||||
"startColumn": 0,
|
||||
"startLine": 0,
|
||||
"url": "vm:///0",
|
||||
},
|
||||
},
|
||||
{
|
||||
"method": "Runtime.exceptionThrown",
|
||||
"params": {
|
||||
"exceptionDetails": {
|
||||
"columnNumber": 0,
|
||||
"exception": {
|
||||
"className": "Promise",
|
||||
"description": "Promise",
|
||||
"objectId": "default:2",
|
||||
"preview": {
|
||||
"description": "Promise",
|
||||
"entries": undefined,
|
||||
"overflow": false,
|
||||
"properties": [
|
||||
{
|
||||
"name": "[[PromiseState]]",
|
||||
"type": "string",
|
||||
"value": "rejected",
|
||||
},
|
||||
{
|
||||
"name": "[[PromiseResult]]",
|
||||
"subtype": "error",
|
||||
"type": "object",
|
||||
"value": "Error
|
||||
at <anonymous>:1:28",
|
||||
},
|
||||
],
|
||||
"subtype": "promise",
|
||||
"type": "object",
|
||||
},
|
||||
"subtype": "promise",
|
||||
"type": "object",
|
||||
},
|
||||
"exceptionId": 1,
|
||||
"lineNumber": 0,
|
||||
"scriptId": undefined,
|
||||
"stackTrace": undefined,
|
||||
"text": "Uncaught (in promise)",
|
||||
"url": undefined,
|
||||
},
|
||||
"timestamp": 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": 0,
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {
|
||||
"expression": "var a = Promise.reject(new Error())",
|
||||
"uniqueContextId": "0",
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": 0,
|
||||
"result": {
|
||||
"exceptionDetails": undefined,
|
||||
"result": {
|
||||
"type": "undefined",
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
`);
|
||||
|
||||
await inspector.eval('void a.catch(() => {});');
|
||||
runJobQueue();
|
||||
expect(inspector.flush()).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"method": "Debugger.scriptParsed",
|
||||
"params": {
|
||||
"buildId": "",
|
||||
"endColumn": 23,
|
||||
"endLine": 1,
|
||||
"executionContextId": 0,
|
||||
"hash": "",
|
||||
"isModule": false,
|
||||
"scriptId": "1",
|
||||
"startColumn": 0,
|
||||
"startLine": 0,
|
||||
"url": "vm:///1",
|
||||
},
|
||||
},
|
||||
{
|
||||
"method": "Runtime.exceptionRevoked",
|
||||
"params": {
|
||||
"exceptionId": 1,
|
||||
"reason": "Handler added to rejected promise",
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {
|
||||
"expression": "void a.catch(() => {});",
|
||||
"uniqueContextId": "0",
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"result": {
|
||||
"exceptionDetails": undefined,
|
||||
"result": {
|
||||
"type": "undefined",
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
@@ -0,0 +1,90 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
/* eslint-disable quotes */
|
||||
import { expect, test } from 'vitest';
|
||||
import { TestInspector } from './utils.mts';
|
||||
import {
|
||||
Agent, Construct, CreateBuiltinFunction, Descriptor, evalQ, getHostDefinedErrorStack, ManagedRealm, setSurroundingAgent,
|
||||
surroundingAgent,
|
||||
Value,
|
||||
type ShadowRealmObject,
|
||||
} from '#self';
|
||||
|
||||
test('code in eval', async () => {
|
||||
const agent = new Agent();
|
||||
setSurroundingAgent(agent);
|
||||
const inspector = new TestInspector();
|
||||
const realm = new ManagedRealm();
|
||||
inspector.attachAgent(agent, [realm]);
|
||||
const messages: unknown[] = [];
|
||||
realm.scope(() => {
|
||||
realm.GlobalObject.properties.set('e', new Descriptor({
|
||||
Value: CreateBuiltinFunction.from(function* e(e = Value.undefined) {
|
||||
messages.push(getHostDefinedErrorStack(e)?.map((f) => f.toCallFrame()));
|
||||
}),
|
||||
}));
|
||||
});
|
||||
|
||||
await inspector.eval([
|
||||
`e(new Error());`,
|
||||
`function f() {
|
||||
e(new Error());
|
||||
};
|
||||
f();`,
|
||||
].map((code) => `eval(\`${code}\`)`).join('\n'));
|
||||
expect(messages).matchSnapshot();
|
||||
});
|
||||
|
||||
test('code in new Function', async () => {
|
||||
const agent = new Agent();
|
||||
setSurroundingAgent(agent);
|
||||
const inspector = new TestInspector();
|
||||
const realm = new ManagedRealm();
|
||||
inspector.attachAgent(agent, [realm]);
|
||||
const messages: unknown[] = [];
|
||||
realm.scope(() => {
|
||||
realm.GlobalObject.properties.set('e', new Descriptor({
|
||||
Value: CreateBuiltinFunction.from(function* e(e = Value.undefined) {
|
||||
messages.push(getHostDefinedErrorStack(e)?.map((f) => f.toCallFrame()));
|
||||
}),
|
||||
}));
|
||||
});
|
||||
|
||||
await inspector.eval(`
|
||||
new Function(\`
|
||||
function x() {
|
||||
e(new Error());
|
||||
}
|
||||
function y() {
|
||||
x()
|
||||
}
|
||||
return y\`)()()
|
||||
`);
|
||||
expect(messages).matchSnapshot();
|
||||
});
|
||||
|
||||
test('code in ShadowRealm', async () => {
|
||||
const agent = new Agent();
|
||||
setSurroundingAgent(agent);
|
||||
const inspector = new TestInspector();
|
||||
const realm = new ManagedRealm();
|
||||
inspector.attachAgent(agent, [realm]);
|
||||
const messages: unknown[] = [];
|
||||
realm.scope(() => {
|
||||
evalQ((_Q, X) => {
|
||||
const shadowRealm = X(Construct(surroundingAgent.intrinsic('%ShadowRealm%'))) as ShadowRealmObject;
|
||||
realm.GlobalObject.properties.set('r', new Descriptor({
|
||||
Value: shadowRealm,
|
||||
}));
|
||||
shadowRealm.ShadowRealm.GlobalObject.properties.set('e', new Descriptor({
|
||||
Value: CreateBuiltinFunction.from(function* e(e = Value.undefined) {
|
||||
messages.push(getHostDefinedErrorStack(e)?.map((f) => f.toCallFrame()));
|
||||
}),
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
await inspector.eval(`
|
||||
r.evaluate('e(new Error())');
|
||||
`);
|
||||
expect(messages).matchSnapshot();
|
||||
});
|
||||
@@ -0,0 +1,341 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import { expect, test } from 'vitest';
|
||||
import type Protocol from 'devtools-protocol';
|
||||
import { TestInspector } from './utils.mts';
|
||||
import { Agent, ManagedRealm, setSurroundingAgent } from '#self';
|
||||
|
||||
test('primitive values', async () => {
|
||||
const agent = new Agent();
|
||||
setSurroundingAgent(agent);
|
||||
const inspector = new TestInspector();
|
||||
const realm = new ManagedRealm();
|
||||
inspector.attachAgent(agent, [realm]);
|
||||
|
||||
for (const value of [
|
||||
'undefined',
|
||||
'null',
|
||||
'false',
|
||||
'true',
|
||||
'42',
|
||||
'-42',
|
||||
'42n',
|
||||
'-42n',
|
||||
'0',
|
||||
'-0',
|
||||
'Infinity',
|
||||
'-Infinity',
|
||||
'NaN',
|
||||
'"engine262"',
|
||||
'Symbol()',
|
||||
'Symbol("desc")',
|
||||
'Symbol.for("symbol")',
|
||||
'Symbol.iterator',
|
||||
]) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
expect(await inspector.eval(value)).toMatchSnapshot(value);
|
||||
}
|
||||
});
|
||||
|
||||
test('functions', async () => {
|
||||
const agent = new Agent();
|
||||
setSurroundingAgent(agent);
|
||||
const inspector = new TestInspector();
|
||||
const realm = new ManagedRealm();
|
||||
inspector.attachAgent(agent, [realm]);
|
||||
|
||||
for (const value of [
|
||||
// function declaration
|
||||
'function f() { /* comment */ }; f',
|
||||
'function* f() { /* comment */ }; f',
|
||||
'function *f() { /* comment */ }; f',
|
||||
'async function f() { /* comment */ }; f',
|
||||
'async function* f() { /* comment */ }; f',
|
||||
'async function *f() { /* comment */ }; f',
|
||||
// function expression
|
||||
'(function f() { /* comment */ })',
|
||||
'(function* f() { /* comment */ })',
|
||||
'(function *f() { /* comment */ })',
|
||||
'(async function f() { /* comment */ })',
|
||||
'(async function* f() { /* comment */ })',
|
||||
'(async function *f() { /* comment */ })',
|
||||
// arrow expression
|
||||
'(() => { /* comment */ })',
|
||||
'(() => 42)',
|
||||
'(async () => { /* comment */ })',
|
||||
'(async () => 42)',
|
||||
// computed function name
|
||||
'var a = 1; ({ [a]() {} })[a]',
|
||||
'({ *[Symbol.iterator]() {} })[Symbol.iterator]',
|
||||
// getter & setter
|
||||
'var o = { get f() { /* comment */ } }; Reflect.getOwnPropertyDescriptor(o, "f").get',
|
||||
'var o = { set f(v) { /* comment */ } }; Reflect.getOwnPropertyDescriptor(o, "f").set',
|
||||
// getter & setter with computed name
|
||||
'var o = { get [Symbol.iterator]() { /* comment */ } }; Reflect.getOwnPropertyDescriptor(o, Symbol.iterator).get',
|
||||
'var o = { set [Symbol.iterator](v) { /* comment */ } }; Reflect.getOwnPropertyDescriptor(o, Symbol.iterator).set',
|
||||
// built-in function
|
||||
'Array.prototype.map',
|
||||
// built-in getter
|
||||
'Reflect.getOwnPropertyDescriptor(Function.prototype, "caller").get',
|
||||
// method
|
||||
'class C { static method() {} }; C.method',
|
||||
'class C { constructor() {}; #f }; C.prototype.constructor',
|
||||
]) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
expect(await inspector.eval(value), value).toMatchSnapshot(value);
|
||||
}
|
||||
});
|
||||
|
||||
async function snapshotObject(inspector: TestInspector, value: string) {
|
||||
const result = await inspector.eval(value);
|
||||
expect(result).toMatchSnapshot(value);
|
||||
const properties = await inspector.runtime.getProperties({ objectId: (result as any).objectId!, ownProperties: true, generatePreview: true }) as Protocol.Protocol.Runtime.GetPropertiesResponse;
|
||||
properties.internalProperties = properties.internalProperties?.filter((prop) => prop.name !== '[[Prototype]]');
|
||||
expect(properties).toMatchSnapshot(`${value} properties`);
|
||||
}
|
||||
|
||||
test('array', async () => {
|
||||
const agent = new Agent();
|
||||
setSurroundingAgent(agent);
|
||||
const inspector = new TestInspector();
|
||||
const realm = new ManagedRealm();
|
||||
inspector.attachAgent(agent, [realm]);
|
||||
|
||||
for (const value of [
|
||||
'[]',
|
||||
'[1]',
|
||||
'Array(10)',
|
||||
'[,,,]',
|
||||
'var a = [1,,2]; a.x = 1; a',
|
||||
]) {
|
||||
await snapshotObject(inspector, value);
|
||||
}
|
||||
});
|
||||
|
||||
test('regex', async () => {
|
||||
const agent = new Agent();
|
||||
setSurroundingAgent(agent);
|
||||
const inspector = new TestInspector();
|
||||
const realm = new ManagedRealm();
|
||||
inspector.attachAgent(agent, [realm]);
|
||||
|
||||
for (const value of [
|
||||
'/cat/',
|
||||
'/cat/g',
|
||||
'/cat/i',
|
||||
'var a = /cat/; a.lastIndex = 1; a',
|
||||
]) {
|
||||
expect(await inspector.eval(value)).toMatchSnapshot(value);
|
||||
}
|
||||
});
|
||||
|
||||
test('date', async () => {
|
||||
const agent = new Agent();
|
||||
setSurroundingAgent(agent);
|
||||
const inspector = new TestInspector();
|
||||
const realm = new ManagedRealm();
|
||||
inspector.attachAgent(agent, [realm]);
|
||||
|
||||
for (const value of [
|
||||
'new Date(0)',
|
||||
'new Date(NaN)',
|
||||
'new Date(-1)',
|
||||
'new Date(9999999999999)',
|
||||
]) {
|
||||
expect(await inspector.eval(value)).toMatchSnapshot(value);
|
||||
}
|
||||
});
|
||||
|
||||
test('map and set', async () => {
|
||||
const agent = new Agent();
|
||||
setSurroundingAgent(agent);
|
||||
const inspector = new TestInspector();
|
||||
const realm = new ManagedRealm();
|
||||
inspector.attachAgent(agent, [realm]);
|
||||
|
||||
for (const value of [
|
||||
'new Map',
|
||||
'new Map([["a", 1], ["b", 2]])',
|
||||
'var x = new Map([["a", 1], ["b", 2]]); x.x = 1; x',
|
||||
'new Set',
|
||||
'new Set(["a", 1, "b", 2])',
|
||||
'var x = new Set(["a", 1, "b", 2]); x.x = 1; x',
|
||||
'new WeakMap',
|
||||
'new WeakMap([[{}, 1], [{}, 2]])',
|
||||
'var x = new WeakMap([[{}, 1], [{}, 2]]); x.x = 1; x',
|
||||
'new WeakSet',
|
||||
'new WeakSet([{}, {}])',
|
||||
'var x = new WeakSet([{}, {}]); x.x = 1; x',
|
||||
]) {
|
||||
await snapshotObject(inspector, value);
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: generator
|
||||
|
||||
test('error', async () => {
|
||||
const agent = new Agent();
|
||||
setSurroundingAgent(agent);
|
||||
const inspector = new TestInspector();
|
||||
const realm = new ManagedRealm();
|
||||
inspector.attachAgent(agent, [realm]);
|
||||
|
||||
for (const value of [
|
||||
'new Error()',
|
||||
'new Error("message")',
|
||||
'new Error("message", { cause: new Error("cause") })',
|
||||
'new RangeError()',
|
||||
'new (class MyError extends Error {})()',
|
||||
// TODO: className should not be syntaxError
|
||||
'new (class MyError extends Error { constructor() { super(); this.message = "hello" } })()',
|
||||
]) {
|
||||
expect(await inspector.eval(value)).toMatchSnapshot(value);
|
||||
}
|
||||
});
|
||||
|
||||
test('proxy', async () => {
|
||||
const agent = new Agent();
|
||||
setSurroundingAgent(agent);
|
||||
const inspector = new TestInspector();
|
||||
const realm = new ManagedRealm();
|
||||
inspector.attachAgent(agent, [realm]);
|
||||
|
||||
for (const value of [
|
||||
'new Proxy({}, {})',
|
||||
'new Proxy({}, { get: () => {} })',
|
||||
'new Proxy({ a: 1 }, {})',
|
||||
'new Proxy(Function, {})',
|
||||
'new Proxy(() => {}, {})',
|
||||
'var a = Proxy.revocable({}, {}); a.revoke(); a.proxy',
|
||||
]) {
|
||||
await snapshotObject(inspector, value);
|
||||
}
|
||||
});
|
||||
|
||||
test('promise', async () => {
|
||||
const agent = new Agent();
|
||||
setSurroundingAgent(agent);
|
||||
const inspector = new TestInspector();
|
||||
const realm = new ManagedRealm();
|
||||
inspector.attachAgent(agent, [realm]);
|
||||
|
||||
for (const value of [
|
||||
'new Promise(() => {})',
|
||||
'var a = new Promise(() => {}); a.x = 1; a',
|
||||
'Promise.resolve()',
|
||||
'Promise.resolve(42)',
|
||||
'Promise.reject()',
|
||||
'Promise.reject(42)',
|
||||
]) {
|
||||
await snapshotObject(inspector, value);
|
||||
}
|
||||
});
|
||||
|
||||
test('typed array', async () => {
|
||||
const agent = new Agent();
|
||||
setSurroundingAgent(agent);
|
||||
const inspector = new TestInspector();
|
||||
const realm = new ManagedRealm();
|
||||
inspector.attachAgent(agent, [realm]);
|
||||
|
||||
for (const value of [
|
||||
'new Uint8Array()',
|
||||
'new Uint8Array(10)',
|
||||
'new Uint8Array([1, 2, 3])',
|
||||
'var x = new Uint8Array(10); x.a = 1; x',
|
||||
'new Int32Array()',
|
||||
'new Int32Array(10)',
|
||||
'new Int32Array([1, 2, 3])',
|
||||
// TODO: test with detached arraybuffer
|
||||
]) {
|
||||
await snapshotObject(inspector, value);
|
||||
}
|
||||
});
|
||||
|
||||
test('array buffer', async () => {
|
||||
const agent = new Agent();
|
||||
setSurroundingAgent(agent);
|
||||
const inspector = new TestInspector();
|
||||
const realm = new ManagedRealm();
|
||||
inspector.attachAgent(agent, [realm]);
|
||||
|
||||
for (const value of [
|
||||
'new ArrayBuffer(0)',
|
||||
'new ArrayBuffer(10)',
|
||||
'var x = new ArrayBuffer(10); x.a = 1; x',
|
||||
// TODO: test with detached arraybuffer
|
||||
]) {
|
||||
await snapshotObject(inspector, value);
|
||||
}
|
||||
});
|
||||
|
||||
test('data view', async () => {
|
||||
const agent = new Agent();
|
||||
setSurroundingAgent(agent);
|
||||
const inspector = new TestInspector();
|
||||
const realm = new ManagedRealm();
|
||||
inspector.attachAgent(agent, [realm]);
|
||||
|
||||
for (const value of [
|
||||
'new DataView(new ArrayBuffer(0))',
|
||||
'new DataView(new ArrayBuffer(10))',
|
||||
'var x = new DataView(new ArrayBuffer(10), 0); x.a = 1; x',
|
||||
]) {
|
||||
await snapshotObject(inspector, value);
|
||||
}
|
||||
});
|
||||
|
||||
test('module namespace', async () => {
|
||||
const agent = new Agent();
|
||||
setSurroundingAgent(agent);
|
||||
const inspector = new TestInspector();
|
||||
const realm = new ManagedRealm();
|
||||
inspector.attachAgent(agent, [realm]);
|
||||
await inspector.debugger.engine262_setEvaluateMode({ mode: 'module' });
|
||||
|
||||
for (const value of [
|
||||
// Note: our inspector will return the module namespace object after evaluation
|
||||
'',
|
||||
'export const a = 1',
|
||||
'export const b = 2; export { b as c }',
|
||||
'export default 42',
|
||||
'export default function() {}',
|
||||
]) {
|
||||
await snapshotObject(inspector, value);
|
||||
}
|
||||
});
|
||||
|
||||
test('shadow realm', async () => {
|
||||
const agent = new Agent();
|
||||
setSurroundingAgent(agent);
|
||||
const inspector = new TestInspector();
|
||||
const realm = new ManagedRealm();
|
||||
inspector.attachAgent(agent, [realm]);
|
||||
|
||||
await snapshotObject(inspector, 'new ShadowRealm');
|
||||
expect(await inspector.eval('new ShadowRealm().evaluate("(() => {})")')).toMatchSnapshot('ShadowRealm function');
|
||||
});
|
||||
|
||||
test('normal object', async () => {
|
||||
const agent = new Agent();
|
||||
setSurroundingAgent(agent);
|
||||
const inspector = new TestInspector();
|
||||
const realm = new ManagedRealm();
|
||||
inspector.attachAgent(agent, [realm]);
|
||||
|
||||
for (const value of [
|
||||
'({})',
|
||||
'({ a: 1 })',
|
||||
'({ a: 1, b: 2 })',
|
||||
'({ __proto__: null })',
|
||||
'({ __proto__: { a: 1 } })',
|
||||
'({ [Symbol.iterator]: () => {} })',
|
||||
'({ f() {} })',
|
||||
'({ get f() {}, set f(v) {} })',
|
||||
'{ class T { #priv = 1 }; new T }',
|
||||
'{ class T { #priv = 1; normal = 2 }; new T }',
|
||||
'({ a: 1n, b: undefined, c: null, d: true, e: Symbol.iterator, f: [] })',
|
||||
]) {
|
||||
await snapshotObject(inspector, value);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
import type { DebuggerContext, DebuggerNamespace, RuntimeNamespace } from '../../lib/inspector/types.d.mts';
|
||||
import { Inspector } from '#self/inspector';
|
||||
|
||||
export class TestInspector extends Inspector {
|
||||
messages: object[] = [];
|
||||
|
||||
flush() {
|
||||
const old = this.messages;
|
||||
this.messages = [];
|
||||
return old;
|
||||
}
|
||||
|
||||
onInspectorMessage?: (message: object) => void;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
protected override send(data: any): void {
|
||||
this.messages.push(data);
|
||||
if ('id' in data) {
|
||||
let meaningfulData = data;
|
||||
if (data.result) {
|
||||
meaningfulData = data.result;
|
||||
if (!data.result.exceptionDetails) {
|
||||
if (data.result.result && Object.keys(data.result).length <= 2) {
|
||||
meaningfulData = data.result.result;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.#callbacks.at(data.id)?.resolve(meaningfulData);
|
||||
}
|
||||
this.onInspectorMessage?.(data);
|
||||
}
|
||||
|
||||
protected override onMessage(id: number, method: string, params: object | void): void {
|
||||
super.onMessage(id, method, params);
|
||||
this.messages.push({ id, method, params });
|
||||
}
|
||||
|
||||
debugger: {
|
||||
[T in keyof DebuggerNamespace]-?: (params: DebuggerNamespace[T] extends undefined | ((params: infer O, context: DebuggerContext) => unknown) ? O : void) => Promise<object>;
|
||||
};
|
||||
|
||||
runtime: {
|
||||
[T in keyof RuntimeNamespace]-?: (params: RuntimeNamespace[T] extends undefined | ((params: infer O, context: DebuggerContext) => unknown) ? O : void) => Promise<object>;
|
||||
};
|
||||
|
||||
#callbacks: PromiseWithResolvers<unknown>[] = [];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const object = (namespace: string) => Object.create(
|
||||
new Proxy({}, {
|
||||
get: (_, p, receiver) => {
|
||||
if (typeof p === 'symbol') {
|
||||
return undefined;
|
||||
}
|
||||
const f = (params: object) => {
|
||||
this.onMessage(this.#callbacks.length, `${namespace}.${p}`, params);
|
||||
const promise = Promise.withResolvers();
|
||||
this.#callbacks.push(promise);
|
||||
return promise.promise;
|
||||
};
|
||||
Reflect.defineProperty(receiver, p, { configurable: true, value: f });
|
||||
return f;
|
||||
},
|
||||
}),
|
||||
);
|
||||
this.runtime = object('Runtime');
|
||||
this.debugger = object('Debugger');
|
||||
}
|
||||
|
||||
// helpers
|
||||
eval(expression: string) {
|
||||
return this.runtime.evaluate({
|
||||
expression,
|
||||
uniqueContextId: '0',
|
||||
});
|
||||
}
|
||||
|
||||
perview(expression: string) {
|
||||
return this.runtime.evaluate({
|
||||
expression,
|
||||
uniqueContextId: '0',
|
||||
throwOnSideEffect: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
Submodule
+1
Submodule test/json/JSONTestSuite added at 1ef36fa012
@@ -0,0 +1,100 @@
|
||||
/* eslint-disable no-console */
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { styleText } from 'node:util';
|
||||
import { globSync } from 'tinyglobby';
|
||||
import { createTestReporter, annotateFileWithURL } from '../tui.mts';
|
||||
import { Test } from '../base.mts';
|
||||
import {
|
||||
Agent,
|
||||
setSurroundingAgent,
|
||||
ManagedRealm,
|
||||
AbruptCompletion,
|
||||
inspect,
|
||||
} from '#self';
|
||||
|
||||
const failed = [
|
||||
// stack overflow for us
|
||||
'n_structure_100000_opening_arrays.json',
|
||||
'n_structure_open_array_object.json',
|
||||
];
|
||||
|
||||
const BASE_DIR = path.resolve(import.meta.dirname, 'JSONTestSuite');
|
||||
|
||||
const agent = new Agent();
|
||||
setSurroundingAgent(agent);
|
||||
|
||||
const reporter = createTestReporter();
|
||||
reporter.start();
|
||||
|
||||
function test(filename: string) {
|
||||
const realm = new ManagedRealm();
|
||||
|
||||
const source = fs.readFileSync(filename, 'utf8');
|
||||
const test = new Test(filename, filename, [], null!, '', source);
|
||||
reporter.addTest(test);
|
||||
|
||||
if (failed.includes(path.basename(filename))) {
|
||||
reporter.skipTest(test.id, 'skip-list');
|
||||
return;
|
||||
}
|
||||
|
||||
reporter.updateWorker(0, test.id);
|
||||
let result;
|
||||
try {
|
||||
result = realm.evaluateScript(`JSON.parse(${JSON.stringify(source)});`);
|
||||
} catch (error) {
|
||||
reporter.updateWorker(0, null);
|
||||
console.error(filename, error);
|
||||
fail(filename, test.id, '');
|
||||
return;
|
||||
}
|
||||
reporter.updateWorker(0, null);
|
||||
|
||||
const testName = path.basename(filename);
|
||||
|
||||
if (!result || result instanceof AbruptCompletion) {
|
||||
if (testName.startsWith('n_')) {
|
||||
reporter.testPassed(test.id);
|
||||
} else if (testName.startsWith('i_')) {
|
||||
reporter.testPassed(test.id);
|
||||
} else {
|
||||
console.error(inspect(result));
|
||||
fail(filename, test.id, '');
|
||||
}
|
||||
} else {
|
||||
if (testName.startsWith('n_')) {
|
||||
fail(filename, test.id, 'Expected failure but got success');
|
||||
} else {
|
||||
reporter.testPassed(test.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const tests = globSync(
|
||||
'test_{parsing,transform}/**/*.json',
|
||||
{ cwd: BASE_DIR, absolute: true },
|
||||
);
|
||||
|
||||
for (const t of tests) {
|
||||
test(t);
|
||||
await new Promise<void>((resolve) => {
|
||||
setImmediate(resolve);
|
||||
});
|
||||
}
|
||||
|
||||
reporter.allTestsDiscovered();
|
||||
reporter.exit();
|
||||
setTimeout(() => {
|
||||
process.exit();
|
||||
});
|
||||
|
||||
function fail(file: string, testId: number, message: string) {
|
||||
process.exitCode = 1;
|
||||
|
||||
// FAILED filename.js
|
||||
const line1 = `${styleText('red', `FAILED ${annotateFileWithURL(file)}`)}\n`;
|
||||
reporter.stdout(line1, message);
|
||||
reporter.testFailed(testId);
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
#####################
|
||||
### Failed Tests ###
|
||||
#####################
|
||||
|
||||
# Comments start with `#` or `;`.
|
||||
# Paths are relative to the `test262/test` directory.
|
||||
# Paths may contain globs.
|
||||
|
||||
# TODO: import defer
|
||||
language/expressions/dynamic-import/import-defer/import-defer-transitive-async-module/main.js
|
||||
language/expressions/dynamic-import/import-defer/sync/main.js
|
||||
language/expressions/dynamic-import/import-defer/sync-dependency-of-deferred-async-module/main.js
|
||||
|
||||
# Non-strict mode
|
||||
# Annex B
|
||||
## RegExp.prototype.compile
|
||||
staging/sm/RegExp/compile-lastIndex.js
|
||||
staging/sm/RegExp/constructor-ordering.js
|
||||
staging/sm/RegExp/flags-param-handling.js
|
||||
staging/sm/RegExp/match-local-tolength-recompilation.js
|
||||
staging/sm/RegExp/prototype.js
|
||||
staging/sm/RegExp/replace-compile-elembase.js
|
||||
staging/sm/RegExp/replace-compile.js
|
||||
staging/sm/RegExp/replace-local-tolength-recompilation.js
|
||||
staging/sm/String/matchAll.js
|
||||
# SourceCharacterIdentityEscape
|
||||
language/literals/regexp/S7.8.5_A1.4_T2.js
|
||||
language/literals/regexp/S7.8.5_A2.4_T2.js
|
||||
# IdentityEscape + ExtendedPatternCharacter
|
||||
staging/sm/RegExp/unicode-braced.js
|
||||
built-ins/String/prototype/split/separator-regexp.js # references /\k<x>/, /\XA0/, /\X/
|
||||
# Block
|
||||
staging/sm/lexical-environment/block-scoped-functions-annex-b-arguments.js
|
||||
staging/sm/lexical-environment/block-scoped-functions-annex-b-eval.js
|
||||
staging/sm/lexical-environment/block-scoped-functions-annex-b-if.js
|
||||
staging/sm/lexical-environment/block-scoped-functions-annex-b-label.js
|
||||
staging/sm/lexical-environment/block-scoped-functions-annex-b-notapplicable.js
|
||||
staging/sm/lexical-environment/block-scoped-functions-annex-b-parameter.js
|
||||
staging/sm/lexical-environment/block-scoped-functions-annex-b-same-name.js
|
||||
staging/sm/lexical-environment/block-scoped-functions-annex-b-with.js
|
||||
staging/sm/lexical-environment/block-scoped-functions-annex-b.js
|
||||
staging/sm/lexical-environment/block-scoped-functions-deprecated-redecl.js
|
||||
|
||||
# TODO: https://github.com/tc39/ecma262/issues/3592
|
||||
built-ins/RegExp/regexp-modifiers/add-ignoreCase-affects-slash-upper-p.js
|
||||
|
||||
# Failed tests appended by --update-failed-tests
|
||||
built-ins/String/prototype/localeCompare/15.5.4.9_CE.js
|
||||
staging/sm/Array/toLocaleString-01.js
|
||||
staging/sm/async-functions/await-in-arrow-parameters.js
|
||||
staging/sm/async-functions/await-in-parameters-of-async-func.js
|
||||
staging/sm/async-functions/property.js
|
||||
staging/sm/AsyncGenerators/for-await-bad-syntax.js
|
||||
staging/sm/Atomics/cross-compartment.js
|
||||
staging/sm/Atomics/detached-buffers.js
|
||||
staging/sm/class/newTargetDefaults.js
|
||||
staging/sm/class/superCallBadNewTargetPrototype.js
|
||||
staging/sm/class/superPropDestructuring.js
|
||||
staging/sm/Date/setTime-argument-shortcircuiting.js
|
||||
staging/sm/Date/time-components-negative-zero.js
|
||||
staging/sm/Date/toISOString-01.js
|
||||
staging/sm/destructuring/bug1396261.js
|
||||
staging/sm/destructuring/order-super.js
|
||||
staging/sm/expressions/destructuring-pattern-parenthesized.js
|
||||
staging/sm/expressions/nullish-coalescing.js
|
||||
staging/sm/expressions/optional-chain.js
|
||||
staging/sm/extensions/arguments-property-access-in-function.js
|
||||
staging/sm/extensions/censor-strict-caller.js
|
||||
staging/sm/extensions/clone-v1-typed-array-data.dat
|
||||
staging/sm/extensions/function-caller-skips-eval-frames.js
|
||||
staging/sm/extensions/function-properties.js
|
||||
staging/sm/extensions/recursion.js
|
||||
staging/sm/fields/await-identifier-module-3.js
|
||||
staging/sm/fields/await-identifier-script.js
|
||||
staging/sm/Function/function-caller-restrictions.js
|
||||
staging/sm/Function/function-name-for.js
|
||||
staging/sm/Function/function-toString-builtin-name.js
|
||||
staging/sm/Function/function-toString-builtin.js
|
||||
staging/sm/Function/invalid-parameter-list.js
|
||||
staging/sm/generators/runtime.js
|
||||
staging/sm/generators/syntax.js
|
||||
staging/sm/generators/yield-star-throw-htmldda.js
|
||||
staging/sm/lexical-environment/for-loop.js
|
||||
staging/sm/lexical-environment/var-in-catch-body-annex-b-eval.js
|
||||
staging/sm/Math/f16round.js
|
||||
staging/sm/misc/explicit-undefined-optional-argument.js
|
||||
staging/sm/misc/future-reserved-words.js
|
||||
staging/sm/Proxy/revoked-get-function-realm-typeerror.js
|
||||
staging/sm/regress/regress-554955-5.js
|
||||
staging/sm/regress/regress-577648-1.js
|
||||
staging/sm/regress/regress-577648-2.js
|
||||
staging/sm/regress/regress-584355.js
|
||||
staging/sm/regress/regress-586482-1.js
|
||||
staging/sm/regress/regress-586482-2.js
|
||||
staging/sm/regress/regress-586482-3.js
|
||||
staging/sm/regress/regress-586482-4.js
|
||||
staging/sm/regress/regress-586482-5.js
|
||||
staging/sm/regress/regress-602621.js
|
||||
staging/sm/statements/for-in-with-declaration.js
|
||||
staging/sm/statements/regress-642975.js
|
||||
staging/sm/strict/strict-function-statements.js
|
||||
staging/sm/String/unicode-braced.js
|
||||
staging/sm/syntax/declaration-forbidden-in-label.js
|
||||
staging/sm/syntax/linefeed-at-eof-in-unterminated-string-or-template.js
|
||||
staging/sm/TypedArray/sort-negative-nan.js
|
||||
staging/sm/TypedArray/toString.js
|
||||
@@ -0,0 +1,27 @@
|
||||
# https://github.com/tc39/test262/blob/main/features.txt
|
||||
|
||||
# Start with `-` to skip feature
|
||||
# Map feature to engine262 feature using `feature = engine262featurename`
|
||||
# e.g.:
|
||||
# import-defer = import-defer
|
||||
decorators = decorators
|
||||
Temporal = temporal
|
||||
|
||||
# Update with test262 on Feb 2025, to be investigated/implemented
|
||||
-Float16Array
|
||||
-arraybuffer-transfer
|
||||
-explicit-resource-management
|
||||
-source-phase-imports
|
||||
-source-phase-imports-module-source
|
||||
-immutable-arraybuffer
|
||||
|
||||
# Added before Feb 2025
|
||||
|
||||
-Atomics
|
||||
-Atomics.waitAsync
|
||||
-Atomics.pause
|
||||
-caller
|
||||
-SharedArrayBuffer
|
||||
-tail-call-optimization
|
||||
-Temporal
|
||||
-resizable-arraybuffer
|
||||
@@ -0,0 +1,124 @@
|
||||
#####################
|
||||
### Skipped Tests ###
|
||||
#####################
|
||||
|
||||
# Comments start with `#` or `;`.
|
||||
# Paths are relative to the `test262/test` directory.
|
||||
# Paths may contain globs.
|
||||
|
||||
annexB
|
||||
intl402
|
||||
|
||||
# fix CI?
|
||||
harness/nativeFunctionMatcher.js
|
||||
|
||||
# Our date parser is now calling the host algorithm, may unstable based on the host.
|
||||
built-ins/Date/parse/without-utc-offset.js
|
||||
|
||||
# Decorators (not merged yet) https://github.com/tc39/test262/pull/4103/
|
||||
## wrong test, we are correct
|
||||
language/expressions/class/decorator/class/error/class-deco-invalid-return-arrow.js
|
||||
language/statements/class/decorator/class/error/class-deco-invalid-return-arrow.js
|
||||
language/expressions/class/decorator/class/class-deco-returns-proxy.js
|
||||
language/statements/class/decorator/class/class-deco-returns-proxy.js
|
||||
|
||||
# TODO (Feb 2026)
|
||||
language/import/import-defer/evaluation-triggers/ignore-super-property-set-exported.js
|
||||
language/import/import-defer/evaluation-triggers/ignore-super-property-set-not-exported.js
|
||||
language/module-code/ambiguous-export-bindings/namespace-unambiguous-if-import-star-as-and-export.js
|
||||
language/module-code/ambiguous-export-bindings/namespace-unambiguous-if-export-star-as-from.js
|
||||
language/module-code/ambiguous-export-bindings/namespace-unambiguous-if-export-star-as-from-and-import-star-as-and-export.js
|
||||
language/identifiers/start-unicode-17.0.0-escaped.js
|
||||
language/identifiers/part-unicode-17.0.0-class-escaped.js
|
||||
language/identifiers/part-unicode-17.0.0.js
|
||||
language/identifiers/start-unicode-17.0.0-class-escaped.js
|
||||
language/identifiers/part-unicode-17.0.0-class.js
|
||||
language/identifiers/start-unicode-17.0.0.js
|
||||
language/identifiers/start-unicode-17.0.0-class.js
|
||||
language/identifiers/part-unicode-17.0.0-escaped.js
|
||||
built-ins/Promise/allSettledKeyed/not-a-constructor.js
|
||||
built-ins/Promise/allSettledKeyed/proto.js
|
||||
built-ins/Promise/allSettledKeyed/name.js
|
||||
built-ins/Promise/allSettledKeyed/extensible.js
|
||||
built-ins/Promise/allSettledKeyed/prop-desc.js
|
||||
built-ins/Promise/allSettledKeyed/length.js
|
||||
built-ins/Promise/allKeyed/not-a-constructor.js
|
||||
built-ins/Promise/allKeyed/prop-desc.js
|
||||
built-ins/Promise/allKeyed/proto.js
|
||||
built-ins/Promise/allKeyed/extensible.js
|
||||
built-ins/Promise/allKeyed/length.js
|
||||
built-ins/Promise/allKeyed/name.js
|
||||
built-ins/Iterator/zipKeyed/suspended-start-iterator-close-calls-return.js
|
||||
built-ins/Iterator/zipKeyed/iterables-containing-string-objects.js
|
||||
built-ins/Iterator/zipKeyed/iterator-zip-iteration-iterator-step-value-abrupt-completion.js
|
||||
built-ins/Iterator/zipKeyed/results-object-has-default-attributes.js
|
||||
built-ins/Iterator/zipKeyed/iterator-zip-iteration-strict-iterator-close-i-is-not-zero-abrupt-completion.js
|
||||
built-ins/Iterator/zipKeyed/suspended-start-iterator-close-calls-next.js
|
||||
built-ins/Iterator/zipKeyed/padding-iteration.js
|
||||
built-ins/Iterator/zipKeyed/iterator-zip-iteration-iterator-close-abrupt-completion.js
|
||||
built-ins/Iterator/zipKeyed/iterator-zip-iteration-longest-iterator-close-abrupt-completion.js
|
||||
built-ins/Iterator/zipKeyed/proto.js
|
||||
built-ins/Iterator/zipKeyed/iterables-iteration-inherited.js
|
||||
built-ins/Iterator/zipKeyed/iterables-iteration-symbol-key.js
|
||||
built-ins/Iterator/zipKeyed/basic-longest.js
|
||||
built-ins/Iterator/zipKeyed/basic-strict.js
|
||||
built-ins/Iterator/zipKeyed/iterables-iteration-get-iterator-flattenable-abrupt-completion.js
|
||||
built-ins/Iterator/zipKeyed/iterables-iteration-after-reading-options.js
|
||||
built-ins/Iterator/zipKeyed/iterator-zip-iteration-shortest-iterator-close-abrupt-completion.js
|
||||
built-ins/Iterator/zipKeyed/suspended-yield-iterator-close-calls-next.js
|
||||
built-ins/Iterator/zipKeyed/iterator-zip-iteration-strict-iterator-step-abrupt-completion.js
|
||||
built-ins/Iterator/zipKeyed/padding-iteration-get-abrupt-completion.js
|
||||
built-ins/Iterator/zipKeyed/prop-desc.js
|
||||
built-ins/Iterator/zipKeyed/iterables-iteration-get-abrupt-completion.js
|
||||
built-ins/Iterator/zipKeyed/results-object-from-array.js
|
||||
built-ins/Iterator/zipKeyed/iterables-iteration.js
|
||||
built-ins/Iterator/zipKeyed/is-function.js
|
||||
built-ins/Iterator/zipKeyed/iterables-iteration-undefined.js
|
||||
built-ins/Iterator/zipKeyed/iterables-iteration-get-own-property-abrupt-completion.js
|
||||
built-ins/Iterator/zipKeyed/options.js
|
||||
built-ins/Iterator/zipKeyed/iterables-iteration-deleted.js
|
||||
built-ins/Iterator/zipKeyed/suspended-yield-iterator-close-calls-return.js
|
||||
built-ins/Iterator/zipKeyed/basic-shortest.js
|
||||
built-ins/Iterator/zipKeyed/options-padding.js
|
||||
built-ins/Iterator/zipKeyed/iterables-iteration-enumerable.js
|
||||
built-ins/Iterator/zipKeyed/iterator-zip-iteration-strict-iterator-close-i-is-zero-abrupt-completion.js
|
||||
built-ins/Iterator/zipKeyed/length.js
|
||||
built-ins/Iterator/zipKeyed/results-object-has-no-undefined-iterables-properties.js
|
||||
built-ins/Iterator/zipKeyed/options-mode.js
|
||||
built-ins/Iterator/zipKeyed/name.js
|
||||
built-ins/Iterator/zipKeyed/result-is-iterator.js
|
||||
built-ins/Iterator/zipKeyed/iterator-zip-iteration.js
|
||||
built-ins/Iterator/zip/iterables-containing-string-objects.js
|
||||
built-ins/Iterator/zip/iterator-zip-iteration-strict-iterator-close-i-is-not-zero-abrupt-completion.js
|
||||
built-ins/Iterator/zip/padding-iteration-iterator-close-abrupt-completion.js
|
||||
built-ins/Iterator/zip/suspended-start-iterator-close-calls-return.js
|
||||
built-ins/Iterator/zip/suspended-start-iterator-close-calls-next.js
|
||||
built-ins/Iterator/zip/iterator-zip-iteration-iterator-step-value-abrupt-completion.js
|
||||
built-ins/Iterator/zip/padding-iteration.js
|
||||
built-ins/Iterator/zip/padding-iteration-get-iterator-abrupt-completion.js
|
||||
built-ins/Iterator/zip/iterator-zip-iteration-iterator-close-abrupt-completion.js
|
||||
built-ins/Iterator/zip/iterator-zip-iteration-longest-iterator-close-abrupt-completion.js
|
||||
built-ins/Iterator/zip/proto.js
|
||||
built-ins/Iterator/zip/basic-longest.js
|
||||
built-ins/Iterator/zip/basic-strict.js
|
||||
built-ins/Iterator/zip/iterables-iteration-get-iterator-flattenable-abrupt-completion.js
|
||||
built-ins/Iterator/zip/iterables-iteration-after-reading-options.js
|
||||
built-ins/Iterator/zip/iterator-zip-iteration-shortest-iterator-close-abrupt-completion.js
|
||||
built-ins/Iterator/zip/suspended-yield-iterator-close-calls-next.js
|
||||
built-ins/Iterator/zip/iterator-zip-iteration-strict-iterator-step-abrupt-completion.js
|
||||
built-ins/Iterator/zip/prop-desc.js
|
||||
built-ins/Iterator/zip/iterables-iteration.js
|
||||
built-ins/Iterator/zip/is-function.js
|
||||
built-ins/Iterator/zip/padding-iteration-iterator-step-value-abrupt-completion.js
|
||||
built-ins/Iterator/zip/options.js
|
||||
built-ins/Iterator/zip/suspended-yield-iterator-close-calls-return.js
|
||||
built-ins/Iterator/zip/options-padding.js
|
||||
built-ins/Iterator/zip/iterables-iteration-iterator-step-value-abrupt-completion.js
|
||||
built-ins/Iterator/zip/basic-shortest.js
|
||||
built-ins/Iterator/zip/iterator-zip-iteration-strict-iterator-close-i-is-zero-abrupt-completion.js
|
||||
built-ins/Iterator/zip/length.js
|
||||
built-ins/Iterator/zip/name.js
|
||||
built-ins/Iterator/zip/iterator-zip-iteration.js
|
||||
built-ins/Iterator/zip/result-is-iterator.js
|
||||
built-ins/Iterator/zip/options-mode.js
|
||||
built-ins/RegExp/unicodeSets/generated/rgi-emoji-17.0.js
|
||||
@@ -0,0 +1,97 @@
|
||||
##################
|
||||
### Slow Tests ###
|
||||
##################
|
||||
|
||||
# Comments start with `#` or `;`.
|
||||
# Paths are relative to the `test262/test` directory.
|
||||
# Paths may contain globs.
|
||||
|
||||
built-ins/RegExp/property-escapes/generated
|
||||
built-ins/RegExp/CharacterClassEscapes
|
||||
|
||||
# Slow on CI
|
||||
built-ins/Array/prototype/concat/Array.prototype.concat_small-typed-array.js
|
||||
built-ins/Array/prototype/every/15.4.4.16-7-c-ii-2.js
|
||||
built-ins/Array/prototype/indexOf/15.4.4.14-10-1.js
|
||||
built-ins/Array/prototype/lastIndexOf/15.4.4.15-9-1.js
|
||||
built-ins/Array/prototype/map/15.4.4.19-8-c-ii-1.js
|
||||
built-ins/Array/prototype/some/15.4.4.17-7-c-ii-2.js
|
||||
built-ins/decodeURI
|
||||
built-ins/encodeURIComponent
|
||||
language/literals/regexp/S7.8.5_A1.4_T2.js
|
||||
language/literals/regexp/S7.8.5_A2.4_T2.js
|
||||
staging/sm/expressions/object-literal-__proto__.js
|
||||
staging/sm/misc/getter-setter-outerize-this.js
|
||||
staging/sm/Proxy/ownkeys-linear.js
|
||||
staging/sm/RegExp/unicode-class-braced.js
|
||||
staging/sm/String/replace-math.js
|
||||
staging/sm/TypedArray/entries.js
|
||||
staging/sm/TypedArray/fill.js
|
||||
staging/sm/TypedArray/forEach.js
|
||||
staging/sm/TypedArray/sort_small.js
|
||||
staging/sm/TypedArray/sort-negative-nan.js
|
||||
|
||||
# Slow tests appended by --update-slow-tests=10
|
||||
built-ins/Array/fromAsync/asyncitems-arraylike-too-long.js
|
||||
built-ins/Array/prototype/concat/Array.prototype.concat_large-typed-array.js
|
||||
built-ins/decodeURI/S15.1.3.1_A1.10_T1.js
|
||||
built-ins/decodeURI/S15.1.3.1_A1.11_T1.js
|
||||
built-ins/decodeURI/S15.1.3.1_A1.11_T2.js
|
||||
built-ins/decodeURI/S15.1.3.1_A1.12_T1.js
|
||||
built-ins/decodeURI/S15.1.3.1_A1.12_T2.js
|
||||
built-ins/decodeURI/S15.1.3.1_A1.12_T3.js
|
||||
built-ins/decodeURI/S15.1.3.1_A1.2_T1.js
|
||||
built-ins/decodeURI/S15.1.3.1_A1.2_T2.js
|
||||
built-ins/decodeURI/S15.1.3.1_A2.1_T1.js
|
||||
built-ins/decodeURI/S15.1.3.1_A2.4_T1.js
|
||||
built-ins/decodeURI/S15.1.3.1_A2.5_T1.js
|
||||
built-ins/decodeURIComponent/S15.1.3.2_A1.10_T1.js
|
||||
built-ins/decodeURIComponent/S15.1.3.2_A1.11_T1.js
|
||||
built-ins/decodeURIComponent/S15.1.3.2_A1.11_T2.js
|
||||
built-ins/decodeURIComponent/S15.1.3.2_A1.12_T1.js
|
||||
built-ins/decodeURIComponent/S15.1.3.2_A1.12_T2.js
|
||||
built-ins/decodeURIComponent/S15.1.3.2_A1.12_T3.js
|
||||
built-ins/decodeURIComponent/S15.1.3.2_A1.2_T1.js
|
||||
built-ins/decodeURIComponent/S15.1.3.2_A1.2_T2.js
|
||||
built-ins/decodeURIComponent/S15.1.3.2_A2.1_T1.js
|
||||
built-ins/decodeURIComponent/S15.1.3.2_A2.4_T1.js
|
||||
built-ins/decodeURIComponent/S15.1.3.2_A2.5_T1.js
|
||||
built-ins/encodeURI/S15.1.3.3_A2.3_T1.js
|
||||
built-ins/encodeURI/S15.1.3.3_A2.5_T1.js
|
||||
built-ins/encodeURIComponent/S15.1.3.4_A2.3_T1.js
|
||||
built-ins/encodeURIComponent/S15.1.3.4_A2.5_T1.js
|
||||
built-ins/Function/prototype/toString/built-in-function-object.js
|
||||
built-ins/parseFloat/S15.1.2.3_A6.js
|
||||
built-ins/parseInt/S15.1.2.2_A8.js
|
||||
built-ins/RegExp/character-class-escape-non-whitespace.js
|
||||
language/comments/S7.4_A5.js
|
||||
language/comments/S7.4_A6.js
|
||||
language/literals/regexp/S7.8.5_A1.1_T2.js
|
||||
language/literals/regexp/S7.8.5_A2.1_T2.js
|
||||
staging/sm/Array/length-truncate-nonconfigurable-sparse.js
|
||||
staging/sm/Array/toSpliced-dense.js
|
||||
staging/sm/Date/dst-offset-caching-1-of-8.js
|
||||
staging/sm/Date/dst-offset-caching-2-of-8.js
|
||||
staging/sm/Date/dst-offset-caching-3-of-8.js
|
||||
staging/sm/Date/dst-offset-caching-4-of-8.js
|
||||
staging/sm/Date/dst-offset-caching-5-of-8.js
|
||||
staging/sm/Date/dst-offset-caching-6-of-8.js
|
||||
staging/sm/Date/dst-offset-caching-7-of-8.js
|
||||
staging/sm/Date/dst-offset-caching-8-of-8.js
|
||||
staging/sm/Date/two-digit-years.js
|
||||
staging/sm/expressions/short-circuit-compound-assignment.js
|
||||
staging/sm/Function/has-instance-jitted.js
|
||||
staging/sm/JSON/parse-mega-huge-array.js
|
||||
staging/sm/RegExp/unicode-ignoreCase.js
|
||||
staging/sm/regress/regress-1507322-deep-weakmap.js
|
||||
staging/sm/regress/regress-610026.js
|
||||
staging/sm/String/fromCodePoint.js
|
||||
staging/sm/String/string-upper-lower-mapping.js
|
||||
staging/sm/TypedArray/element-setting-converts-using-ToNumber.js
|
||||
staging/sm/TypedArray/every-and-some.js
|
||||
staging/sm/TypedArray/map-and-filter.js
|
||||
staging/sm/TypedArray/set-same-buffer-different-source-target-types.js
|
||||
staging/sm/TypedArray/sort_large_countingsort.js
|
||||
staging/sm/TypedArray/sort_modifications.js
|
||||
staging/sm/TypedArray/sort_snans.js
|
||||
staging/sm/TypedArray/sort_sorted.js
|
||||
@@ -0,0 +1,37 @@
|
||||
##########################
|
||||
### Slow Tests (on CI) ###
|
||||
##########################
|
||||
|
||||
# Comments start with `#` or `;`.
|
||||
# Paths are relative to the `test262/test` directory.
|
||||
# Paths may contain globs.
|
||||
|
||||
built-ins/Array/prototype/concat/Array.prototype.concat_small-typed-array.js
|
||||
built-ins/Array/prototype/every/15.4.4.16-7-c-ii-2.js
|
||||
built-ins/Array/prototype/filter/15.4.4.20-9-c-ii-1.js
|
||||
built-ins/Array/prototype/forEach/15.4.4.18-7-c-ii-1.js
|
||||
built-ins/Array/prototype/indexOf/15.4.4.14-10-1.js
|
||||
built-ins/Array/prototype/lastIndexOf/15.4.4.15-9-1.js
|
||||
built-ins/Array/prototype/map/15.4.4.19-8-c-ii-1.js
|
||||
built-ins/Array/prototype/some/15.4.4.17-7-c-ii-2.js
|
||||
built-ins/encodeURI/S15.1.3.3_A2.4_T1.js
|
||||
built-ins/encodeURI/S15.1.3.3_A2.4_T2.js
|
||||
built-ins/encodeURIComponent/S15.1.3.4_A2.4_T1.js
|
||||
built-ins/encodeURIComponent/S15.1.3.4_A2.4_T2.js
|
||||
built-ins/String/prototype/repeat/empty-string-returns-empty.js
|
||||
built-ins/TypedArray/prototype/copyWithin/coerced-values-end-detached-prototype.js
|
||||
built-ins/TypedArray/prototype/copyWithin/coerced-values-end-detached.js
|
||||
built-ins/TypedArray/prototype/copyWithin/coerced-values-start-detached.js
|
||||
language/literals/regexp/S7.8.5_A1.4_T2.js
|
||||
language/literals/regexp/S7.8.5_A2.4_T2.js
|
||||
staging/sm/expressions/object-literal-__proto__.js
|
||||
staging/sm/misc/getter-setter-outerize-this.js
|
||||
staging/sm/Proxy/ownkeys-linear.js
|
||||
staging/sm/RegExp/unicode-class-braced.js
|
||||
staging/sm/String/replace-math.js
|
||||
staging/sm/TypedArray/entries.js
|
||||
staging/sm/TypedArray/fill.js
|
||||
staging/sm/TypedArray/forEach.js
|
||||
staging/sm/TypedArray/sort_small.js
|
||||
staging/sm/TypedArray/sort_small.js
|
||||
staging/sm/TypedArray/sort-negative-nan.js
|
||||
Submodule
+1
Submodule test/test262/test262 added at 3aa9cb2c71
@@ -0,0 +1,446 @@
|
||||
/* eslint-disable no-console */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { join, resolve, relative } from 'node:path';
|
||||
import { createWriteStream } from 'node:fs';
|
||||
import { opendir, readFile, stat } from 'node:fs/promises';
|
||||
import { stripVTControlCharacters, styleText } from 'node:util';
|
||||
import { fork } from 'node:child_process';
|
||||
import { cpus } from 'node:os';
|
||||
import { glob, isDynamicPattern } from 'tinyglobby';
|
||||
import YAML from 'js-yaml';
|
||||
import { highlight } from 'cli-highlight';
|
||||
import {
|
||||
type WorkerToSupervisor, type SupervisorToWorker, Test,
|
||||
readList,
|
||||
type WorkerToSupervisor_Failed,
|
||||
type Stack,
|
||||
} from '../base.mts';
|
||||
import { annotateFileWithURL, isCI } from '../tui.mts';
|
||||
import {
|
||||
createTestReporter,
|
||||
supportColor,
|
||||
type SkipReason,
|
||||
} from '../tui.mts';
|
||||
import { fatal_exit } from '../base.mts';
|
||||
import { args } from './test262.mts';
|
||||
|
||||
const inputs = {
|
||||
Test262TestsPath: join(process.env.TEST262 || resolve(import.meta.dirname, 'test262'), 'test'),
|
||||
AllTests: async () => {
|
||||
const files: string[] = [];
|
||||
for await (const file of readdir(inputs.Test262TestsPath)) {
|
||||
files.push(file);
|
||||
}
|
||||
inputs.AllTests = async () => files;
|
||||
return files;
|
||||
},
|
||||
AssertToBeFailedList: resolve(import.meta.dirname, 'failed'),
|
||||
SkipList: resolve(import.meta.dirname, 'skip'),
|
||||
SlowList: resolve(import.meta.dirname, 'slow'),
|
||||
SlowListCI: resolve(import.meta.dirname, 'slow-ci'),
|
||||
Features: resolve(import.meta.dirname, 'features'),
|
||||
};
|
||||
|
||||
const outputs = {
|
||||
LastRunFailedList: resolve(import.meta.dirname, 'last-failed-list'),
|
||||
CurrentRunFailureLog: resolve(import.meta.dirname, 'last-failed.log'),
|
||||
};
|
||||
|
||||
if (args.values['failed-only']) {
|
||||
args.positionals = (await readFile(outputs.LastRunFailedList, { encoding: 'utf-8' })).split('\n');
|
||||
}
|
||||
|
||||
const outputStreams = {
|
||||
SlowList: args.values['update-slow'] ? createWriteStream(inputs.SlowList, { encoding: 'utf-8', flags: 'a' }) : undefined,
|
||||
LastRunFailedList: createWriteStream(outputs.LastRunFailedList, { encoding: 'utf-8' }),
|
||||
CurrentRunFailureLog: createWriteStream(outputs.CurrentRunFailureLog, { encoding: 'utf-8' }),
|
||||
AssertToBeFailedList: args.values['update-failed'] ? createWriteStream(inputs.AssertToBeFailedList, { encoding: 'utf-8', flags: 'a' }) : undefined,
|
||||
};
|
||||
|
||||
let allTestsDiscovered = false;
|
||||
|
||||
const disabledFeatures = new Set<string>();
|
||||
readList(inputs.Features).forEach((feature) => {
|
||||
if (feature.startsWith('-')) {
|
||||
disabledFeatures.add(feature.slice(1));
|
||||
}
|
||||
});
|
||||
disabledFeatures.delete(args.values.features!);
|
||||
|
||||
const workersToStart = Math.max(
|
||||
1,
|
||||
process.env.NUM_WORKERS
|
||||
? Number.parseInt(process.env.NUM_WORKERS, 10)
|
||||
: cpus().length - 2,
|
||||
);
|
||||
const workers = Array.from({ length: workersToStart }, (_, index) => createWorker(index));
|
||||
/**
|
||||
* Do not replace this with reporter.workers.
|
||||
* This variable maintains the state in the main thread, but reporter.workers is updated asynchronously based on the feedback from workers.
|
||||
*/
|
||||
const workerHasPendingTask: boolean[] = new Array(workersToStart).fill(false);
|
||||
|
||||
const [
|
||||
slowList,
|
||||
slowListCI,
|
||||
skipList,
|
||||
assertToBeFailedList,
|
||||
] = await Promise.all([
|
||||
readListPaths(inputs.SlowList, false),
|
||||
readListPaths(inputs.SlowListCI, true),
|
||||
readListPaths(inputs.SkipList, false),
|
||||
readListPaths(inputs.AssertToBeFailedList, false),
|
||||
]);
|
||||
|
||||
if (outputStreams.AssertToBeFailedList) {
|
||||
outputStreams.AssertToBeFailedList.write('\n# Failed tests appended by --update-failed-tests\n');
|
||||
}
|
||||
|
||||
/** This is for skipping the report of a test if it's variant (strict version) has already failed. */
|
||||
const currentRunFailedTestFiles = new Set<string>();
|
||||
const pendingTests: Test[] = [];
|
||||
const reporter = createTestReporter();
|
||||
|
||||
if (outputStreams.SlowList) {
|
||||
const second = parseInt(args.values['update-slow']!, 10);
|
||||
if (Number.isNaN(second)) {
|
||||
fatal_exit('--update-slow must be a number');
|
||||
}
|
||||
outputStreams.SlowList.write(`\n# Slow tests appended by --update-slow=${second}\n`);
|
||||
reporter.setSlowTestReporting(second, outputStreams.SlowList);
|
||||
}
|
||||
|
||||
function discoverTest(test: Test) {
|
||||
reporter.addTest(test);
|
||||
const disabledFeature = test.attrs.features?.find((feature: string) => disabledFeatures.has(feature));
|
||||
if (disabledFeature) {
|
||||
return reporter.skipTest(test.id, 'feature-disabled', disabledFeature);
|
||||
}
|
||||
if (skipList.has(test.file)) {
|
||||
return reporter.skipTest(test.id, 'skip-list');
|
||||
}
|
||||
if (slowList.has(test.file) && !args.values['run-slow']) {
|
||||
return reporter.skipTest(test.id, 'slow-list');
|
||||
}
|
||||
if (isCI && slowListCI.has(test.file) && !args.values['run-slow']) {
|
||||
return reporter.skipTest(test.id, 'slow-list');
|
||||
}
|
||||
pendingTests.push(test);
|
||||
distributeTest();
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function distributeTest() {
|
||||
while (true) {
|
||||
const candidate = workerHasPendingTask.findIndex((work) => !work);
|
||||
if (candidate === -1) {
|
||||
return;
|
||||
}
|
||||
if (!pendingTests.length) {
|
||||
if (allTestsDiscovered && workerHasPendingTask.every((work) => !work)) {
|
||||
reporter.exit();
|
||||
}
|
||||
return;
|
||||
}
|
||||
workers[candidate].send(pendingTests.shift()! satisfies SupervisorToWorker);
|
||||
workerHasPendingTask[candidate] = true;
|
||||
}
|
||||
}
|
||||
|
||||
const visited = new Set<string>();
|
||||
reporter.start();
|
||||
reporter.addEventListener('exit', () => {
|
||||
workers.forEach((worker) => worker.kill());
|
||||
});
|
||||
reporter.onExit.promise.then(() => {
|
||||
if (args.values.verbose || args.values.vv) {
|
||||
const skip: Record<SkipReason, Set<string>> = {
|
||||
'feature-disabled': new Set<string>(),
|
||||
'skip-list': new Set<string>(),
|
||||
'slow-list': new Set<string>(),
|
||||
};
|
||||
const passed: Set<string> = new Set<string>();
|
||||
const skipByFeature: Record<string, Set<string>> = {};
|
||||
const failed: Set<string> = new Set<string>();
|
||||
for (const test of reporter.tests.values()) {
|
||||
if (test.status === 'skipped') {
|
||||
if (test.skipFeature) {
|
||||
(skipByFeature[test.skipFeature] ??= new Set()).add(test.file);
|
||||
} else if (test.skipReason) {
|
||||
skip[test.skipReason].add(test.file);
|
||||
}
|
||||
} else if (test.status === 'failed') {
|
||||
failed.add(test.file);
|
||||
} else if (args.values.vv && test.status === 'passed') {
|
||||
passed.add(test.file);
|
||||
}
|
||||
}
|
||||
if (args.values.vv && passed.size > 0) {
|
||||
console.log(styleText('green', 'The following tests passed:'));
|
||||
for (const test of passed) {
|
||||
console.log(`- ./test/test262/test262/test/${test}`);
|
||||
}
|
||||
}
|
||||
let skipPrint = () => {
|
||||
console.log(styleText('yellow', 'The following tests were skipped:'));
|
||||
skipPrint = () => { };
|
||||
};
|
||||
for (const [reason, tests] of Object.entries(skip)) {
|
||||
if (tests.size === 0) {
|
||||
continue;
|
||||
}
|
||||
skipPrint();
|
||||
console.log(`- Reason: ${styleText('yellow', reason)} (${tests.size} tests)`);
|
||||
for (const test of tests) {
|
||||
console.log(` - ./test/test262/test262/test/${test}`);
|
||||
}
|
||||
}
|
||||
for (const [feature, tests] of Object.entries(skipByFeature)) {
|
||||
skipPrint();
|
||||
console.log(`- Reason: ${styleText('yellow', `feature-disabled (${feature})`)} (${tests.size} tests)`);
|
||||
for (const test of tests) {
|
||||
console.log(` - ./test/test262/test262/test/${test}`);
|
||||
}
|
||||
}
|
||||
if (failed.size > 0) {
|
||||
console.log(styleText('red', '\nThe following tests failed:'));
|
||||
for (const test of failed) {
|
||||
console.log(`- ./test/test262/test262/test/${test}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const engineFeatures = [...args.values['engine-features'] || []];
|
||||
|
||||
const promises = [];
|
||||
for await (const file of parsePositionals(args.positionals, true)) {
|
||||
if (visited.has(file) || /_FIXTURE|README\.md|\.py|\.map|\.mts/.test(file)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
visited.add(file);
|
||||
promises.push(readFile(file, 'utf8').then((contents) => {
|
||||
const frontmatterYaml = contents.match(/\/\*---(.*?)---\*\//s)?.[1];
|
||||
const attrs: any = frontmatterYaml ? YAML.load(frontmatterYaml) : {};
|
||||
|
||||
if (args.values.features && (!attrs.features || !attrs.features.includes(args.values.features))) {
|
||||
// feature not match
|
||||
return;
|
||||
}
|
||||
|
||||
attrs.flags = (attrs.flags || []).reduce((acc: any, c: any) => {
|
||||
acc[c] = true;
|
||||
return acc;
|
||||
}, {});
|
||||
attrs.includes = attrs.includes || [];
|
||||
|
||||
const test = new Test(relative(inputs.Test262TestsPath, file), file, engineFeatures, attrs, '', contents);
|
||||
|
||||
if (test.attrs.flags.module) {
|
||||
discoverTest(test.withDifferentTestFlag('module'));
|
||||
} else {
|
||||
if (!test.attrs.flags.onlyStrict && !args.values['strict-only'] && !args.values.fast) {
|
||||
discoverTest(test);
|
||||
}
|
||||
|
||||
if (!test.attrs.flags.noStrict && !test.attrs.flags.raw) {
|
||||
discoverTest(test.withDifferentTestFlag('strict', `'use strict';${test.content}`));
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
if (args.positionals.length && !promises.length) {
|
||||
fatal_exit(`No tests found based on the given globs: ${args.positionals.join(', ')}`);
|
||||
}
|
||||
await Promise.all(promises);
|
||||
|
||||
allTestsDiscovered = true;
|
||||
reporter.allTestsDiscovered();
|
||||
distributeTest();
|
||||
|
||||
async function readListPaths(file: string, defaults: boolean) {
|
||||
const list = readList(file);
|
||||
const files = new Set<string>();
|
||||
for await (const file of parsePositionals(list, defaults)) {
|
||||
files.add(relative(inputs.Test262TestsPath, file));
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
async function* readdir(dir: string): AsyncGenerator<string> {
|
||||
for await (const dirent of await opendir(dir)) {
|
||||
const p = join(dir, dirent.name);
|
||||
if (dirent.isDirectory()) {
|
||||
yield* readdir(p);
|
||||
} else {
|
||||
yield p;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function* parsePositional(pattern: string): AsyncGenerator<string> {
|
||||
if (!isDynamicPattern(pattern)) {
|
||||
const a_path = join(inputs.Test262TestsPath, pattern);
|
||||
const a = await stat(a_path).catch(() => undefined);
|
||||
if (a?.isDirectory()) {
|
||||
return yield* readdir(a_path);
|
||||
} else if (a?.isFile()) {
|
||||
return yield a_path;
|
||||
}
|
||||
|
||||
const b_path = join(process.cwd(), pattern);
|
||||
const b = await stat(b_path).catch(() => undefined);
|
||||
if (b?.isDirectory()) {
|
||||
return yield* readdir(b_path);
|
||||
} else if (b?.isFile()) {
|
||||
return yield b_path;
|
||||
}
|
||||
|
||||
const files = await inputs.AllTests();
|
||||
const matched = files.filter((f) => f.toLowerCase().includes(pattern.toLowerCase()));
|
||||
if (matched.length) {
|
||||
return yield* matched;
|
||||
}
|
||||
}
|
||||
|
||||
const files1 = await glob(pattern, { cwd: inputs.Test262TestsPath, absolute: true, caseSensitiveMatch: false });
|
||||
if (files1.length) {
|
||||
return yield* files1;
|
||||
}
|
||||
|
||||
const files2 = await glob(pattern, { cwd: process.cwd(), absolute: true, caseSensitiveMatch: false });
|
||||
if (files2.length) {
|
||||
return yield* files2;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async function* parsePositionals(pattern: string[], defaults: boolean): AsyncGenerator<string> {
|
||||
if (!pattern.length) {
|
||||
if (defaults) {
|
||||
yield* readdir(inputs.Test262TestsPath);
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (const p of pattern) {
|
||||
if (p) {
|
||||
yield* parsePositional(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createWorker(workerId: number) {
|
||||
const c = fork(resolve(import.meta.dirname, './test262-worker.mts'));
|
||||
c.on('message', (message: WorkerToSupervisor) => {
|
||||
switch (message.status) {
|
||||
case 'RUNNING':
|
||||
return reporter.updateWorker(workerId, message.testId);
|
||||
case 'PASS':
|
||||
workerHasPendingTask[workerId] = false;
|
||||
reporter.updateWorker(workerId, null);
|
||||
distributeTest();
|
||||
if (assertToBeFailedList.has(message.file)) {
|
||||
if (currentRunFailedTestFiles.has(message.file)) {
|
||||
return reporter.testFailed(message.testId);
|
||||
} else {
|
||||
currentRunFailedTestFiles.add(message.file);
|
||||
return fail({
|
||||
file: message.file,
|
||||
description: 'The test is declared to be failed, but passed.',
|
||||
error: '',
|
||||
flags: message.flags,
|
||||
status: 'FAIL',
|
||||
testId: message.testId,
|
||||
stack: [],
|
||||
}, false);
|
||||
}
|
||||
}
|
||||
return reporter.testPassed(message.testId);
|
||||
case 'FAIL': {
|
||||
workerHasPendingTask[workerId] = false;
|
||||
reporter.updateWorker(workerId, null);
|
||||
distributeTest();
|
||||
if (assertToBeFailedList.has(message.file)) {
|
||||
return reporter.assertFailedTestFails(message.testId);
|
||||
}
|
||||
if (currentRunFailedTestFiles.has(message.file)) {
|
||||
return reporter.testFailed(message.testId);
|
||||
}
|
||||
currentRunFailedTestFiles.add(message.file);
|
||||
outputStreams.LastRunFailedList.write(`${message.file}\n`);
|
||||
if (outputStreams.AssertToBeFailedList) {
|
||||
outputStreams.AssertToBeFailedList.write(`${message.file}\n`);
|
||||
}
|
||||
return fail(message, true);
|
||||
}
|
||||
default:
|
||||
console.error(message);
|
||||
throw new RangeError('Unknown message from worker');
|
||||
}
|
||||
});
|
||||
c.on('exit', (code) => {
|
||||
if (code !== 0 && code !== null) {
|
||||
fatal_exit(`Worker ${workerId} exited with code ${code}`);
|
||||
}
|
||||
});
|
||||
return c;
|
||||
}
|
||||
|
||||
|
||||
function fail(message: WorkerToSupervisor_Failed, showSource: boolean) {
|
||||
const { description, testId, file } = message;
|
||||
let error = message.error;
|
||||
error = error.replaceAll(`${process.cwd()}/`, '');
|
||||
process.exitCode = 1;
|
||||
|
||||
const desc = styleText('yellow', description.trim());
|
||||
const descNeedOwnLine = desc.includes('\n') || desc.length > (process.stdout.columns - file.length - 8);
|
||||
// FAILED filename.js
|
||||
const line1 = `${styleText(['bgRed', 'white', 'bold'], ' FAIL ')} ${annotateFileWithURL(file)}${descNeedOwnLine ? '' : ` ${desc}`}\n`;
|
||||
// Test description in the header
|
||||
const line2 = descNeedOwnLine ? `${indent(desc, ' ')}\n` : '';
|
||||
// Source code with error position annotated
|
||||
const line3 = showSource ? annotateSourceWithErrorPosition(error, reporter.tests.get(testId)!.content, message.stack) : '';
|
||||
// Error message
|
||||
const line4 = `${indent(error, ' ')}\n`;
|
||||
const line5 = styleText('red', `${'⎯'.repeat(process.stdout.columns)}\n`);
|
||||
const output = line1 + line2 + line3 + line4;
|
||||
reporter.stdout(output, line5);
|
||||
outputStreams.CurrentRunFailureLog.write(stripVTControlCharacters(output));
|
||||
reporter.testFailed(testId);
|
||||
}
|
||||
|
||||
function indent(string: string, space: string) {
|
||||
return string.split('\n').map((line) => space + line).join('\n');
|
||||
}
|
||||
|
||||
function annotateSourceWithErrorPosition(error: string, sourceCode: string, [stack]: Stack[]) {
|
||||
sourceCode = stack?.source || sourceCode;
|
||||
if (!stack) {
|
||||
return '';
|
||||
}
|
||||
if (sourceCode.endsWith('\n')) {
|
||||
sourceCode = sourceCode.slice(0, -1);
|
||||
}
|
||||
const highLightedLines = (supportColor ? highlight(sourceCode, { language: 'js' }) : sourceCode).split('\n');
|
||||
const linesPad = (highLightedLines.length + 1).toString().length;
|
||||
const decoratedLines = highLightedLines.map((line, index) => ` ${styleText('red', (index + 1).toString().padStart(linesPad))} | ${line}`);
|
||||
const LINES_BEFORE = 3;
|
||||
const LINES_AFTER = 2;
|
||||
const slicedLines = decoratedLines.slice(
|
||||
Math.max(0, Number(stack.line) - LINES_BEFORE),
|
||||
Math.min(decoratedLines.length, Number(stack.line)),
|
||||
);
|
||||
slicedLines.push(''.padStart(linesPad + 5) + styleText('red', `${'-'.repeat(Math.max(Number(stack.column) - 1, 0))}^ ${error.split('\n')[0].trim()}`));
|
||||
slicedLines.push(
|
||||
decoratedLines.slice(
|
||||
Number(stack.line),
|
||||
Math.min(decoratedLines.length, Number(stack.line) + LINES_AFTER),
|
||||
).join('\n'),
|
||||
);
|
||||
|
||||
sourceCode = `${slicedLines.join('\n')}`;
|
||||
sourceCode += '\n';
|
||||
return sourceCode;
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
/* eslint-disable no-console */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable no-multi-assign */
|
||||
import * as path from 'node:path';
|
||||
import * as fs from 'node:fs';
|
||||
import * as util from 'node:util';
|
||||
import {
|
||||
readList, type Stack, type SupervisorToWorker, type Test, type WorkerToSupervisor,
|
||||
} from '../base.mts';
|
||||
import { createRealm, createAgent } from '../base.mts';
|
||||
import {
|
||||
AbruptCompletion, ObjectValue, evalQ,
|
||||
setSurroundingAgent,
|
||||
inspect,
|
||||
Value,
|
||||
IsCallable,
|
||||
IsDataDescriptor,
|
||||
JSStringValue,
|
||||
skipDebugger,
|
||||
boostTest262Harness,
|
||||
ThrowCompletion,
|
||||
getHostDefinedErrorStack,
|
||||
CallSite,
|
||||
} from '#self';
|
||||
|
||||
const TEST262 = process.env.TEST262 || path.resolve(import.meta.dirname, 'test262');
|
||||
const TEST262_TESTS = path.join(TEST262, 'test');
|
||||
|
||||
const featureMap: Record<string, string> = Object.create(null);
|
||||
readList(path.resolve(import.meta.dirname, 'features')).forEach((f) => {
|
||||
if (f.includes('=')) {
|
||||
const [k, v] = f.split('=');
|
||||
featureMap[k.trim()] = v.trim();
|
||||
}
|
||||
});
|
||||
|
||||
const includeCache: Record<string, undefined | { source: string, specifier: string }> = {};
|
||||
|
||||
process.on('message', (test: SupervisorToWorker) => {
|
||||
try {
|
||||
process.send!({ status: 'RUNNING', testId: test.id } satisfies WorkerToSupervisor, handleSendError);
|
||||
const result = run(test);
|
||||
if (result.status === 'PASS') {
|
||||
process.send!({
|
||||
status: 'PASS', file: test.file, flags: test.currentTestFlag, testId: test.id,
|
||||
} satisfies WorkerToSupervisor, handleSendError);
|
||||
} else {
|
||||
process.send!(result satisfies WorkerToSupervisor, handleSendError);
|
||||
}
|
||||
} catch (e) {
|
||||
process.send!(fails(test, util.inspect(e), []), handleSendError);
|
||||
}
|
||||
});
|
||||
|
||||
function run(test: Test): WorkerToSupervisor {
|
||||
const features = [...test.engineFeatures];
|
||||
if (test.attrs.features) {
|
||||
test.attrs.features.forEach((f) => {
|
||||
if (featureMap[f]) {
|
||||
features.push(featureMap[f]);
|
||||
}
|
||||
});
|
||||
}
|
||||
const agent = createAgent({ features });
|
||||
const parsedScripts = new Map<string, string>();
|
||||
setSurroundingAgent(agent);
|
||||
agent.hostDefinedOptions.errorStackAttachNativeStack = true;
|
||||
agent.hostDefinedOptions.onScriptParsed = (script, id) => {
|
||||
parsedScripts.set(id, script.ECMAScriptCode.sourceText);
|
||||
};
|
||||
|
||||
|
||||
function fail(test: Test, error: Value): WorkerToSupervisor {
|
||||
const stacks = getHostDefinedErrorStack(error);
|
||||
const reportStack: Stack[] = [];
|
||||
for (const stack of stacks || []) {
|
||||
if (!(stack instanceof CallSite)) {
|
||||
continue;
|
||||
}
|
||||
const scriptId = stack.getScriptId();
|
||||
if (!scriptId || stack.columnNumber === null || stack.lineNumber === null) {
|
||||
continue;
|
||||
}
|
||||
const source = parsedScripts.get(scriptId);
|
||||
const record = agent.parsedSources.get(scriptId);
|
||||
if (record?.HostDefined.specifier?.includes('harness')) {
|
||||
continue;
|
||||
}
|
||||
reportStack.push({
|
||||
column: stack.columnNumber,
|
||||
line: stack.lineNumber,
|
||||
source: source === test.content ? undefined : source,
|
||||
specifier: stack.getSpecifier(),
|
||||
});
|
||||
}
|
||||
return {
|
||||
status: 'FAIL',
|
||||
file: test.file,
|
||||
flags: test.currentTestFlag,
|
||||
testId: test.id,
|
||||
description: test.attrs.description,
|
||||
error: inspect(error),
|
||||
stack: reportStack,
|
||||
};
|
||||
}
|
||||
|
||||
const { realm, resolverCache, setPrintHandle } = createRealm({ specifier: test.specifier });
|
||||
const r = realm.scope((): WorkerToSupervisor => {
|
||||
test.attrs.includes.unshift('assert.js', 'sta.js');
|
||||
if (test.attrs.flags.async) {
|
||||
test.attrs.includes.unshift('doneprintHandle.js');
|
||||
}
|
||||
|
||||
for (const include of test.attrs.includes) {
|
||||
if (includeCache[include] === undefined) {
|
||||
const p = path.resolve(TEST262, `harness/${include}`);
|
||||
includeCache[include] = {
|
||||
source: fs.readFileSync(p, 'utf8'),
|
||||
specifier: p,
|
||||
};
|
||||
}
|
||||
const entry = includeCache[include];
|
||||
const completion = realm.evaluateScript(entry.source, { specifier: entry.specifier });
|
||||
if (completion instanceof AbruptCompletion) {
|
||||
return fail(test, completion.Value);
|
||||
}
|
||||
}
|
||||
boostTest262Harness(realm);
|
||||
|
||||
{
|
||||
const DONE = `
|
||||
function $DONE(error) {
|
||||
if (error) {
|
||||
if (typeof error === 'object' && error !== null && 'stack' in error) {
|
||||
print('Test262:AsyncTestFailure:' + error.stack, error);
|
||||
} else {
|
||||
print('Test262:AsyncTestFailure:Test262Error: ' + error, error);
|
||||
}
|
||||
} else {
|
||||
print('Test262:AsyncTestComplete');
|
||||
}
|
||||
}`;
|
||||
const completion = realm.evaluateScript(`\
|
||||
var Test262Error = class Test262Error extends Error {};
|
||||
Test262Error.thrower = (...args) => {
|
||||
throw new Test262Error(...args);
|
||||
};
|
||||
${test.attrs.flags.async ? DONE : ''}`);
|
||||
if (completion instanceof AbruptCompletion) {
|
||||
return fail(test, completion.Value);
|
||||
}
|
||||
}
|
||||
|
||||
let asyncResult: WorkerToSupervisor | undefined;
|
||||
if (test.attrs.flags.async) {
|
||||
setPrintHandle((m, value) => {
|
||||
if (m === 'Test262:AsyncTestComplete') {
|
||||
asyncResult = {
|
||||
status: 'PASS', flags: test.currentTestFlag, testId: test.id, file: test.file,
|
||||
};
|
||||
} else {
|
||||
asyncResult = fail(test, value);
|
||||
}
|
||||
setPrintHandle(undefined);
|
||||
});
|
||||
}
|
||||
|
||||
const specifier = path.resolve(TEST262_TESTS, test.file);
|
||||
|
||||
const completion = evalQ((Q) => {
|
||||
if (test.attrs.flags.module) {
|
||||
const module = Q(realm.compileModule(test.content, { specifier }));
|
||||
resolverCache.set(specifier, module);
|
||||
const loadModuleCompletion = module.LoadRequestedModules();
|
||||
if (loadModuleCompletion.PromiseState === 'rejected') {
|
||||
Q(ThrowCompletion(loadModuleCompletion.PromiseResult!));
|
||||
} else if (loadModuleCompletion.PromiseState === 'pending') {
|
||||
throw new Error('Internal error: .LoadRequestedModules() returned a pending promise');
|
||||
}
|
||||
Q(module.Link());
|
||||
const evaluateCompletion = Q(skipDebugger(module.Evaluate()));
|
||||
if (evaluateCompletion.PromiseState === 'rejected') {
|
||||
Q(ThrowCompletion(evaluateCompletion.PromiseResult!));
|
||||
}
|
||||
} else {
|
||||
Q(realm.evaluateScript(test.content, { specifier }));
|
||||
}
|
||||
});
|
||||
|
||||
if (completion.Type === 'throw') {
|
||||
if (test.attrs.negative && isError(test.attrs.negative.type, completion.Value)) {
|
||||
return {
|
||||
status: 'PASS', flags: test.currentTestFlag, testId: test.id, file: test.file,
|
||||
};
|
||||
} else {
|
||||
return fail(test, completion.Value);
|
||||
}
|
||||
}
|
||||
|
||||
if (test.attrs.flags.async) {
|
||||
if (!asyncResult) {
|
||||
throw new Error('missing async result');
|
||||
}
|
||||
return asyncResult;
|
||||
}
|
||||
|
||||
if (test.attrs.negative) {
|
||||
return fails(test, `Expected ${test.attrs.negative.type} during ${test.attrs.negative.phase}`, []);
|
||||
} else {
|
||||
return {
|
||||
status: 'PASS', flags: test.currentTestFlag, testId: test.id, file: test.file,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
function handleSendError(e: any) {
|
||||
if (e) {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function isError(type: string, value: unknown) {
|
||||
if (!(value instanceof ObjectValue)) {
|
||||
return false;
|
||||
}
|
||||
const proto = (value as any).Prototype;
|
||||
if (!proto || !(proto instanceof ObjectValue)) {
|
||||
return false;
|
||||
}
|
||||
const ctorDesc = proto.properties.get(Value('constructor'));
|
||||
if (!ctorDesc || !IsDataDescriptor(ctorDesc)) {
|
||||
return false;
|
||||
}
|
||||
const ctor = ctorDesc.Value;
|
||||
if (!(ctor instanceof ObjectValue) || !IsCallable(ctor)) {
|
||||
return false;
|
||||
}
|
||||
const namePropDesc = ctor.properties.get(Value('name'));
|
||||
if (!namePropDesc || !IsDataDescriptor(namePropDesc)) {
|
||||
return false;
|
||||
}
|
||||
const nameProp = namePropDesc.Value;
|
||||
return nameProp instanceof JSStringValue && nameProp.stringValue() === type;
|
||||
}
|
||||
|
||||
function fails(test: Test, error: string, stack: Stack[]): WorkerToSupervisor {
|
||||
return {
|
||||
status: 'FAIL',
|
||||
file: test.file,
|
||||
flags: test.currentTestFlag,
|
||||
testId: test.id,
|
||||
description: test.attrs.description,
|
||||
error,
|
||||
stack,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/* eslint-disable no-console */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { relative } from 'node:path';
|
||||
import util, { styleText } from 'node:util';
|
||||
import { cpus } from 'node:os';
|
||||
import { link } from '../base.mts';
|
||||
import { isCI } from '../tui.mts';
|
||||
|
||||
export const args = util.parseArgs({
|
||||
args: process.argv.slice(2),
|
||||
allowNegative: true,
|
||||
allowPositionals: true,
|
||||
strict: true,
|
||||
options: {
|
||||
'help': { type: 'boolean', short: 'h' },
|
||||
'features': { type: 'string', short: 'f' },
|
||||
'engine-features': { type: 'string', multiple: true, short: 'e' },
|
||||
'update-slow': { type: 'string' },
|
||||
'update-failed': { type: 'boolean', short: 'u' },
|
||||
|
||||
'run-slow': { type: 'boolean' },
|
||||
'failed-only': { type: 'boolean', short: 'f' },
|
||||
'strict-only': { type: 'boolean' },
|
||||
'fast': { type: 'boolean', short: 'q', default: !!isCI },
|
||||
|
||||
'verbose': { type: 'boolean', short: 'v' },
|
||||
'vv': { type: 'boolean', short: 'V' },
|
||||
},
|
||||
});
|
||||
|
||||
async function main() {
|
||||
if (args.values.help) {
|
||||
const TEST_PATTERN = styleText('gray', '[TEST-PATTERN]');
|
||||
const SLOW_LIST = link('the slow list file', new URL('./slow', import.meta.url));
|
||||
const LAST_FAILED_LIST = link('last-failed-list', new URL('./last-failed-list', import.meta.url));
|
||||
const LOCAL_FILE = styleText('gray', '(Local file)');
|
||||
const usage = `
|
||||
Usage: node ${relative(process.cwd(), import.meta.filename)} ${TEST_PATTERN} ...
|
||||
Run ${link('test262 tests', 'https://github.com/tc39/test262')} against engine262.
|
||||
|
||||
${TEST_PATTERN} supports glob syntax, and is interpreted relative to
|
||||
${link('the test262 "test" subdirectory', new URL('./test262/test262/test/', import.meta.url))} or (if that fails for any pattern)
|
||||
relative to the working directory. If no patterns are specified,
|
||||
all tests are run.
|
||||
|
||||
${styleText('magentaBright', 'Environment variables:')}
|
||||
${styleText('magenta', 'TEST262')} ${styleText('gray', process.env.TEST262 ? `(set to '${process.env.TEST262}')` : '(unset)')}
|
||||
The test262 directory, which contains the "test" subdirectory.
|
||||
If empty, it defaults to the "test262" sibling of this file.
|
||||
${styleText('magenta', 'NUM_WORKERS')} ${styleText('gray', process.env.NUM_WORKERS ? `(set to '${process.env.NUM_WORKERS}')` : '(unset)')}
|
||||
The count of child processes that should be created to run tests.
|
||||
If empty, it defaults to ${cpus().length}.
|
||||
|
||||
${styleText('greenBright', 'Options:')}
|
||||
${styleText('green', '--features / -f')} ${styleText('gray', '[feature]')}
|
||||
Only run tests that has the specified feature.
|
||||
${styleText('green', '--engine-features / -e')} ${styleText('gray', '[feature]')}
|
||||
Enable specified engine features during test execution.
|
||||
${styleText('green', '--update-slow')} ${styleText('gray', '[seconds]')}
|
||||
Append tests that take longer than the given time to ${SLOW_LIST}.
|
||||
${styleText('green', '--update-failed / -u')}
|
||||
Append failed tests to ${link('the failed list', new URL('./failed', import.meta.url))}.
|
||||
If test in this list passes, it will be an error.
|
||||
${styleText('green', '--run-slow')}
|
||||
Run slow tests that are listed in ${SLOW_LIST}.
|
||||
${styleText('green', '--failed-only / -f')}
|
||||
Run only the tests that failed in the previous run.
|
||||
Listed in ${LAST_FAILED_LIST}.
|
||||
${styleText('green', '--strict-only / --fast / --q')}
|
||||
Only run strict mode tests.
|
||||
${styleText('green', '--verbose / -v')}
|
||||
Print why tests are skipped or failed.
|
||||
${styleText('green', '--vv / -V')}
|
||||
Print why tests are skipped or failed, and also print passed tests.
|
||||
|
||||
${styleText('yellowBright', 'Files:')}
|
||||
${link(styleText('yellow', 'features'), new URL('./features', import.meta.url))}
|
||||
Specifies handling of test262 features, notably which ones to skip.
|
||||
${link(styleText('yellow', 'skip'), new URL('./skip', import.meta.url))}
|
||||
Includes patterns of test files to skip.
|
||||
${link(styleText('yellow', 'slow'), new URL('./slow', import.meta.url))}
|
||||
Includes patterns of test files to skip in the absence of ${styleText('green', '--run-slow-tests')}.
|
||||
${link(styleText('yellow', 'failed'), new URL('./failed', import.meta.url))}
|
||||
Includes patterns of test files that are expected to fail.
|
||||
${styleText('yellow', LAST_FAILED_LIST)} ${LOCAL_FILE}
|
||||
The list of test files that failed in the last run.
|
||||
${link(styleText('yellow', 'last-failed.log'), new URL('./last-failed.log', import.meta.url))} ${LOCAL_FILE}
|
||||
The detailed log of test files that failed in the last run.
|
||||
`.slice(1);
|
||||
const indent = usage.match(/^\s*/)![0];
|
||||
process.stdout.write(
|
||||
`${usage
|
||||
.trimEnd()
|
||||
.split('\n')
|
||||
.map((line) => line.replace(indent, ''))
|
||||
.join('\n')}\n`,
|
||||
);
|
||||
process.exit(64);
|
||||
}
|
||||
|
||||
await import('./test262-runner.mts');
|
||||
}
|
||||
main();
|
||||
Executable
+11
@@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -x
|
||||
|
||||
E=0
|
||||
|
||||
npm run test:supplemental || E=$?
|
||||
npm run test:json || E=$?
|
||||
npm run test:test262 || E=$?
|
||||
|
||||
exit $E
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"references": [{ "path": "../src/" }, { "path": "../lib-src/inspector/" }],
|
||||
"extends": "../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"rootDir": "../",
|
||||
"allowImportingTsExtensions": true
|
||||
},
|
||||
"include": [
|
||||
"./base.mts",
|
||||
"./tui.mts",
|
||||
"./test262/test262.mts",
|
||||
"./test262/test262-runner.mts",
|
||||
"./test262/test262-worker.mts",
|
||||
"./json/json.mts",
|
||||
"./inspector/*",
|
||||
"./engine262/*"
|
||||
]
|
||||
}
|
||||
+405
@@ -0,0 +1,405 @@
|
||||
/* eslint-disable no-console */
|
||||
import { styleText } from 'util';
|
||||
import { type WriteStream } from 'fs';
|
||||
import * as React from 'react';
|
||||
import {
|
||||
render, Text, useApp, useInput, useStdout,
|
||||
} from 'ink';
|
||||
import { TaskList, Task } from 'ink-task-list';
|
||||
import { BarChart } from '@pppp606/ink-chart';
|
||||
import { link, type Test } from './base.mts';
|
||||
|
||||
const { createElement: h } = React;
|
||||
export const isCI = process.env.CI || process.env.CONTINUOUS_INTEGRATION;
|
||||
export const supportColor = !isCI && styleText('red', 'test') !== 'test';
|
||||
|
||||
function Fragment(...children: (React.JSX.Element | null)[]) {
|
||||
return h(React.Fragment, null, ...children);
|
||||
}
|
||||
|
||||
// from https://github.com/sindresorhus/cli-spinners
|
||||
const spinner = {
|
||||
interval: 80,
|
||||
frames: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'],
|
||||
};
|
||||
|
||||
const quitMessage = [styleText('gray', 'Press '), styleText('yellow', 'q'), styleText('gray', ' to exit.')].join('');
|
||||
function TerminalUI({ runner }: { runner: TerminalUIReporter }) {
|
||||
useFlushStdout(runner);
|
||||
const exiting = useExit(runner);
|
||||
|
||||
const tasks = useWorkers(runner);
|
||||
const runtime = JSON.parse(useRuntimeUpdate(runner)) as number[];
|
||||
const needPad = runtime.some((time) => time >= runner.slowThreshold);
|
||||
const runtimePadLength = needPad ? String(Math.max(...runtime)).length : 0;
|
||||
const previous = React.useRef<number[]>([]);
|
||||
|
||||
return Fragment(
|
||||
h(ProgressBar, { runner, running: !exiting, exiting }),
|
||||
h(
|
||||
TaskList,
|
||||
null,
|
||||
...(exiting ? [] : tasks).map((test, index) => {
|
||||
previous.current.length = tasks.length;
|
||||
previous.current[index] = test?.id ?? previous.current[index];
|
||||
|
||||
let label: string;
|
||||
let padding = '';
|
||||
let status = '';
|
||||
let state: 'loading' | 'success' = 'loading';
|
||||
if (test) {
|
||||
label = test.file;
|
||||
status = test.currentTestFlag;
|
||||
if (runtime[index] >= runner.slowThreshold) {
|
||||
padding = styleText('red', `[${String(runtime[index]).padStart(runtimePadLength)}s] `);
|
||||
}
|
||||
} else {
|
||||
let test = runner.tests.get(previous.current[index]);
|
||||
if (test && (Date.now() - test.endTime! > 200)) {
|
||||
test = undefined;
|
||||
}
|
||||
label = styleText('dim', test?.file ?? 'Idle');
|
||||
status = test?.currentTestFlag ?? '';
|
||||
state = test ? 'loading' : 'success';
|
||||
if (!test && needPad) {
|
||||
padding = ' '.repeat(runtimePadLength + 3);
|
||||
}
|
||||
}
|
||||
if (!padding && needPad) {
|
||||
padding = ' '.repeat(runtimePadLength + 4);
|
||||
}
|
||||
label = `${padding}${label}`;
|
||||
|
||||
return h(Task, {
|
||||
key: index, label, state, spinner, status,
|
||||
});
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function ProgressBar({ runner, running, exiting }: { runner: TestReporter, running: boolean, exiting: boolean }) {
|
||||
const stats = useStats(runner);
|
||||
if (!stats) {
|
||||
return null;
|
||||
}
|
||||
const {
|
||||
failed, passed, pending, skipped, total, ready,
|
||||
} = stats;
|
||||
if (!ready && running) {
|
||||
return h(
|
||||
Text,
|
||||
null,
|
||||
`Discovering tests... ${total} found so far. ${passed} passed, ${skipped} skipped, ${failed} failed, ${pending} pending. `,
|
||||
exiting ? '' : quitMessage,
|
||||
);
|
||||
}
|
||||
return Fragment(
|
||||
h(Text, null, `${total} tests in total. `, exiting ? '' : quitMessage),
|
||||
h(
|
||||
BarChart,
|
||||
{
|
||||
data: [
|
||||
{ label: `${passed} passed`, value: passed, color: 'green' },
|
||||
{ label: `${skipped} skipped`, value: skipped, color: 'yellow' },
|
||||
{ label: `${failed} failed`, value: failed, color: 'red' },
|
||||
running || pending ? { label: `${pending} pending`, value: pending, color: 'cyan' } : null!,
|
||||
].filter(Boolean),
|
||||
width: 'full',
|
||||
max: total,
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function useStats(runner: TestReporter) {
|
||||
return React.useSyncExternalStore(
|
||||
(onUpdate) => {
|
||||
runner.addEventListener('stats', onUpdate);
|
||||
return () => runner.removeEventListener('stats', onUpdate);
|
||||
},
|
||||
() => runner.getStats(),
|
||||
);
|
||||
}
|
||||
|
||||
function useWorkers(runner: TestReporter) {
|
||||
return React.useSyncExternalStore(
|
||||
(onUpdate) => {
|
||||
runner.addEventListener('update', onUpdate);
|
||||
return () => runner.removeEventListener('update', onUpdate);
|
||||
},
|
||||
() => runner.workers,
|
||||
);
|
||||
}
|
||||
|
||||
function useRuntimeUpdate(runner: TestReporter) {
|
||||
return React.useSyncExternalStore(
|
||||
(onUpdate) => {
|
||||
runner.addEventListener('update', onUpdate);
|
||||
return () => runner.removeEventListener('update', onUpdate);
|
||||
},
|
||||
() => JSON.stringify(
|
||||
runner.workers.map((test) => test?.getRuntimeSeconds()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function useExit(runner: TestReporter) {
|
||||
const exitRequested = React.useSyncExternalStore(
|
||||
(onExit) => {
|
||||
runner.addEventListener('exit', onExit);
|
||||
return () => runner.removeEventListener('exit', onExit);
|
||||
},
|
||||
() => runner.exited,
|
||||
);
|
||||
const { exit } = useApp();
|
||||
const [previousExitState, setExitState] = React.useState<boolean>(false);
|
||||
if (exitRequested && !previousExitState) {
|
||||
setExitState(true);
|
||||
setTimeout(exit, 10);
|
||||
}
|
||||
useInput((input, key) => {
|
||||
if (input === 'q' || (key.ctrl && input === 'c')) {
|
||||
setExitState(true);
|
||||
runner.exit();
|
||||
}
|
||||
});
|
||||
return previousExitState;
|
||||
}
|
||||
|
||||
function useFlushStdout(runner: TerminalUIReporter) {
|
||||
const stdout = useStdout();
|
||||
React.useEffect(() => {
|
||||
function flush() {
|
||||
stdout.write(runner.pending_stdout.join(''));
|
||||
runner.pending_stdout.length = 0;
|
||||
}
|
||||
runner.addEventListener('flush', flush);
|
||||
return () => runner.removeEventListener('flush', flush);
|
||||
});
|
||||
}
|
||||
|
||||
export type SkipReason = 'feature-disabled' | 'skip-list' | 'slow-list';
|
||||
|
||||
export abstract class TestReporter extends EventTarget {
|
||||
tests: Map<number, Test> = new Map();
|
||||
|
||||
workers: (Test | undefined)[] = [];
|
||||
|
||||
protected skipped = 0;
|
||||
|
||||
protected passed = 0;
|
||||
|
||||
protected failed = 0;
|
||||
|
||||
protected ready = false;
|
||||
|
||||
setSlowTestReporting(threshold: number, stream: WriteStream) {
|
||||
this.slowThreshold = threshold;
|
||||
this.slowStream = stream;
|
||||
}
|
||||
|
||||
slowThreshold = 2;
|
||||
|
||||
protected slowStream: WriteStream | null = null;
|
||||
|
||||
protected stats: { total: number; pending: number; passed: number; failed: number; skipped: number; ready: boolean } = this.getStats();
|
||||
|
||||
protected statsStale = false;
|
||||
|
||||
getStats() {
|
||||
if (!this.statsStale) {
|
||||
return this.stats;
|
||||
}
|
||||
this.statsStale = false;
|
||||
this.stats = {
|
||||
total: this.tests.size,
|
||||
pending: this.tests.size - this.passed - this.failed - this.skipped,
|
||||
passed: this.passed,
|
||||
failed: this.failed,
|
||||
skipped: this.skipped,
|
||||
ready: this.ready,
|
||||
};
|
||||
return this.stats;
|
||||
}
|
||||
|
||||
exited = false;
|
||||
|
||||
abstract stdout(...message: string[]): void
|
||||
|
||||
abstract start(): void
|
||||
|
||||
protected slowTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
protected startSlowTimer() {
|
||||
const seenSlow = new Set<string>();
|
||||
this.slowTimer = setInterval(() => {
|
||||
let hasSlow = false;
|
||||
for (const test of this.tests.values()) {
|
||||
if (test.getRuntimeSeconds() >= this.slowThreshold) {
|
||||
if (this.slowStream && !seenSlow.has(test.file)) {
|
||||
this.slowStream?.write(`${test.file}\n`);
|
||||
seenSlow.add(test.file);
|
||||
}
|
||||
hasSlow = true;
|
||||
}
|
||||
}
|
||||
if (hasSlow) {
|
||||
this.dispatchEvent(new Event('update'));
|
||||
}
|
||||
}, 400);
|
||||
}
|
||||
|
||||
allTestsDiscovered() {
|
||||
this.ready = true;
|
||||
this.statsStale = true;
|
||||
this.dispatchEvent(new Event('stats'));
|
||||
}
|
||||
|
||||
onExit = Promise.withResolvers<void>();
|
||||
|
||||
exit() {
|
||||
this.exited = true;
|
||||
if (this.slowTimer) {
|
||||
clearInterval(this.slowTimer);
|
||||
this.slowTimer = null;
|
||||
}
|
||||
this.dispatchEvent(new Event('exit'));
|
||||
}
|
||||
|
||||
addTest(test: Test) {
|
||||
this.tests.set(test.id, test);
|
||||
this.statsStale = true;
|
||||
}
|
||||
|
||||
updateWorker(workerId: number, taskId: number | null) {
|
||||
if (this.workers.length <= workerId) {
|
||||
const next = [...this.workers];
|
||||
next.length = workerId + 1;
|
||||
this.workers = next;
|
||||
}
|
||||
if (taskId === null) {
|
||||
this.workers = this.workers.with(workerId, undefined);
|
||||
} else {
|
||||
const test = this.tests.get(taskId)!;
|
||||
test.status = 'running';
|
||||
test.startTime = Date.now();
|
||||
this.workers = this.workers.with(workerId, test!);
|
||||
}
|
||||
this.statsStale = true;
|
||||
this.dispatchEvent(new Event('update'));
|
||||
}
|
||||
|
||||
skipTest(testId: number, reason: SkipReason, feature?: string) {
|
||||
const test = this.tests.get(testId)!;
|
||||
test.status = 'skipped';
|
||||
test.skipReason = reason;
|
||||
test.skipFeature = feature ?? null;
|
||||
test.content = '';
|
||||
this.skipped += 1;
|
||||
this.statsStale = true;
|
||||
test.endTime = Date.now();
|
||||
this.dispatchEvent(new Event('stats'));
|
||||
}
|
||||
|
||||
testFailed(testId: number) {
|
||||
const test = this.tests.get(testId)!;
|
||||
test.status = 'failed';
|
||||
test.endTime = Date.now();
|
||||
test.content = '';
|
||||
this.failed += 1;
|
||||
this.statsStale = true;
|
||||
this.dispatchEvent(new Event('stats'));
|
||||
}
|
||||
|
||||
assertFailedTestFails(testId: number) {
|
||||
this.testPassed(testId);
|
||||
}
|
||||
|
||||
testPassed(testId: number) {
|
||||
const test = this.tests.get(testId)!;
|
||||
test.status = 'passed';
|
||||
test.endTime = Date.now();
|
||||
test.content = '';
|
||||
this.passed += 1;
|
||||
this.statsStale = true;
|
||||
this.dispatchEvent(new Event('stats'));
|
||||
}
|
||||
}
|
||||
|
||||
class BasicReporter extends TestReporter {
|
||||
stdout(...message: string[]): void {
|
||||
process.stdout.write(message.join(''));
|
||||
}
|
||||
|
||||
start(): void {
|
||||
this.timer = setInterval(() => {
|
||||
const {
|
||||
failed, passed, pending, ready, skipped, total,
|
||||
} = this.getStats();
|
||||
if (ready) {
|
||||
console.log(`Total ${total}, ${passed} passed, ${skipped} skipped, ${failed} failed, ${pending} pending`);
|
||||
} else {
|
||||
console.log(`Total ${total}, ${passed} passed, ${skipped} skipped, ${failed} failed`);
|
||||
}
|
||||
const seen = new Set();
|
||||
this.workers.forEach((task) => {
|
||||
if (!task || seen.has(task.file)) {
|
||||
return;
|
||||
}
|
||||
seen.add(task.file);
|
||||
const time = task?.getRuntimeSeconds() || 0;
|
||||
if (time >= this.slowThreshold) {
|
||||
console.log(`${task?.file} is slow. Taking ${time}s.`);
|
||||
}
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
private timer: NodeJS.Timeout | undefined;
|
||||
|
||||
override exit(): void {
|
||||
super.exit();
|
||||
clearInterval(this.timer);
|
||||
const {
|
||||
failed, passed, pending, skipped, total,
|
||||
} = this.getStats();
|
||||
console.log(`Total ${total}, ${passed} passed, ${skipped} skipped, ${failed} failed, ${pending} pending`);
|
||||
this.onExit.resolve();
|
||||
}
|
||||
}
|
||||
class TerminalUIReporter extends TestReporter {
|
||||
pending_stdout: string[] = [];
|
||||
|
||||
stdout(...message: string[]) {
|
||||
this.pending_stdout.push(...message);
|
||||
this.statsStale = true;
|
||||
this.dispatchEvent(new Event('flush'));
|
||||
}
|
||||
|
||||
start() {
|
||||
render(h(TerminalUI, { runner: this }), { incrementalRendering: true, exitOnCtrlC: false }).waitUntilExit().then(this.onExit.resolve);
|
||||
this.startSlowTimer();
|
||||
}
|
||||
|
||||
override exit() {
|
||||
super.exit();
|
||||
process.stdin.setRawMode(false);
|
||||
}
|
||||
}
|
||||
|
||||
export function createTestReporter(): TestReporter {
|
||||
if (isCI) {
|
||||
return new BasicReporter();
|
||||
} else {
|
||||
return new TerminalUIReporter();
|
||||
}
|
||||
}
|
||||
|
||||
export function annotateFileWithURL(filePath: string) {
|
||||
if (supportColor) {
|
||||
const fileLink = link(filePath, new URL(`./test262/test262/test/${filePath}`, import.meta.url));
|
||||
return `${fileLink} ${link('[GitHub]', `https://github.com/tc39/test262/blob/main/test/${filePath}`)}`;
|
||||
}
|
||||
return filePath;
|
||||
}
|
||||
Reference in New Issue
Block a user