Squashed 'engine262/' content from commit ae71998

git-subtree-dir: engine262
git-subtree-split: ae71998cc5a8315700555135b1ac202a0d6d0b31
This commit is contained in:
2026-03-27 10:07:29 +05:30
commit 6e92d2345e
433 changed files with 90478 additions and 0 deletions
+177
View File
@@ -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;
}
}
+77
View File
@@ -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);
});
});
+45
View File
@@ -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);
});
});
+72
View File
@@ -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"
`);
});
+119
View File
@@ -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'");
});
+93
View File
@@ -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);
});
+9
View File
@@ -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
+278
View File
@@ -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,
}
`);
});
+489
View File
@@ -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;
});
+224
View File
@@ -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",
},
},
},
]
`);
});
+90
View File
@@ -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();
});
+341
View File
@@ -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);
}
});
+86
View File
@@ -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,
});
}
}
+100
View File
@@ -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);
}
+106
View File
@@ -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
+27
View File
@@ -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
+124
View File
@@ -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
+97
View File
@@ -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
+37
View File
@@ -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
+446
View File
@@ -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;
}
+260
View File
@@ -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,
};
}
+103
View File
@@ -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();
+11
View File
@@ -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
+20
View File
@@ -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
View File
@@ -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;
}