blob: d1670449baf363db29cf4a3f511192a7b9e7fe11 [file]
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {assert} from 'chai';
import {$, click, enableExperiment, getBrowserAndPages, getResourcesPath, goToResource, pasteText, waitFor, waitForFunction, waitForMany, waitForNone} from '../../shared/helper.js';
import {describe, it} from '../../shared/mocha-extensions.js';
import {CONSOLE_TAB_SELECTOR, focusConsolePrompt, getCurrentConsoleMessages} from '../helpers/console-helpers.js';
import {addBreakpointForLine, checkBreakpointIsNotActive, getCallFrameLocations, getCallFrameNames, getValuesForScope, listenForSourceFilesAdded, openFileInEditor, openFileInSourcesPanel, openSourcesPanel, PAUSE_ON_EXCEPTION_BUTTON, RESUME_BUTTON, retrieveSourceFilesAdded, retrieveTopCallFrameScriptLocation, switchToCallFrame, waitForAdditionalSourceFiles} from '../helpers/sources-helpers.js';
// TODO: Remove once Chromium updates its version of Node.js to 12+.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const globalThis: any = global;
declare global {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface Window {
__sourceFilesAddedEvents: string[];
}
}
type RawModule = {
url: string,
code?: ArrayBuffer,
};
type RawLocationRange = {
rawModuleId: string,
startOffset: number,
endOffset: number,
};
type RawLocation = {
rawModuleId: string,
codeOffset: number,
inlineFrameIndex?: number,
};
type SourceLocation = {
rawModuleId: string,
sourceFileURL: string,
lineNumber: number,
columnNumber: number,
};
type Variable = {
scope: string,
name: string,
type: string,
nestedName?: Array<string>,
};
type VariableValue = {
value: string|Array<VariableValue>,
js_type: string,
type: string,
name: string,
};
type EvaluatorModule = {
code?: ArrayBuffer,
constantValue?: VariableValue,
};
type ScopeInfo = {
type: string,
typeName: string,
icon?: string,
};
interface EvalBase {
rootType: TypeInfo;
payload: unknown;
}
interface FieldInfo {
name?: string;
offset: number;
typeId: unknown;
}
interface TypeInfo {
typeNames: string[];
typeId: unknown;
members: FieldInfo[];
alignment: number;
arraySize: number;
size: number;
canExpand: boolean;
hasValue: boolean;
}
type FunctionInfo = {
name?: string,
};
interface TestPluginImpl {
addRawModule?(rawModuleId: string, symbolsURL: string, rawModule: {url: string}): Promise<Array<string>>;
removeRawModule?(rawModuleId: string): Promise<void>;
sourceLocationToRawLocation?(sourceLocation: SourceLocation): Promise<Array<RawLocationRange>>;
rawLocationToSourceLocation?(rawLocation: RawLocation): Promise<Array<SourceLocation>>;
getScopeInfo?(type: string): Promise<ScopeInfo>;
listVariablesInScope?(rawLocation: RawLocation): Promise<Array<Variable>>;
evaluateVariable?(name: string, location: RawLocation): Promise<EvaluatorModule|null>;
getTypeInfo?(expression: string, context: RawLocation): Promise<{typeInfos: TypeInfo[], base: EvalBase}|null>;
getFormatter?(expressionOrField: string|{base: EvalBase, field: FieldInfo[]}, context: RawLocation):
Promise<{js: string}|null>;
getFunctionInfo?(rawLocation: RawLocation): Promise<{frames: Array<FunctionInfo>}|null>;
getMappedLines?(rawModuleId: string, sourceFileURL: string): Promise<number[]|undefined>;
dispose?(): void;
}
declare function RegisterExtension(
extensionAPI: unknown, pluginImpl: TestPluginImpl, name: string,
supportedScriptTypes: {language: string, symbol_types: string[]}): void;
// This testcase reaches into DevTools internals to install the extension plugin. At this point, there is no sensible
// alternative, because loading a real extension is not supported in our test setup.
describe('The Debugger Language Plugins', async () => {
beforeEach(async () => {
await enableExperiment('wasmDWARFDebugging');
const {frontend} = getBrowserAndPages();
await frontend.evaluate(resourcePath => {
globalThis.installExtensionPlugin = function(
registerPluginCallback: (extensionServerClient: unknown, extensionAPI: unknown) => void) {
const extensionServer = globalThis.Extensions.ExtensionServer.instance();
/** @type {!{startPage: string, name: string}} */
const extensionInfo = {
startPage: `${resourcePath}/sources/language_extensions.html`,
name: 'TestExtension',
};
extensionServer._addExtension(extensionInfo);
const extensionIFrames = document.body.querySelectorAll(`[data-devtools-extension="${extensionInfo.name}"]`);
const injectedAPI = globalThis.buildExtensionAPIInjectedScript(
extensionInfo, undefined, 'default', globalThis.UI.shortcutRegistry.globalShortcutKeys(),
registerPluginCallback);
function injectAPICallback(completionCallback: () => void) {
return (ev: Event) => {
if (ev.target) {
const iframeWin = (ev.target as HTMLIFrameElement).contentWindow;
if (iframeWin) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(iframeWin as any).eval(`${injectedAPI}()`);
}
}
completionCallback();
};
}
return Promise.all(Array.from(extensionIFrames, element => new Promise<void>(resolve => {
(element as HTMLIFrameElement).onload =
injectAPICallback(resolve);
})));
};
}, getResourcesPath());
});
// Load a simple wasm file and verify that the source file shows up in the file tree.
it('can show C filenames after loading the module', async () => {
const {target, frontend} = getBrowserAndPages();
await frontend.evaluate(
() => globalThis.installExtensionPlugin((extensionServerClient: unknown, extensionAPI: unknown) => {
// A simple plugin that resolves to a single source file
class SingleFilePlugin {
async addRawModule(rawModuleId: string, symbols: string, rawModule: RawModule) {
const fileUrl = new URL('/source_file.c', rawModule.url || symbols);
return [fileUrl.href];
}
}
RegisterExtension(
extensionAPI, new SingleFilePlugin(), 'Single File', {language: 'WebAssembly', symbol_types: ['None']});
}));
await openFileInSourcesPanel('wasm/global_variable.html');
await listenForSourceFilesAdded(frontend);
const additionalFilesPromise = waitForAdditionalSourceFiles(frontend);
await target.evaluate('go();');
await additionalFilesPromise;
const capturedFileNames = await retrieveSourceFilesAdded(frontend);
assert.deepEqual(capturedFileNames, ['/test/e2e/resources/sources/wasm/global_variable.wasm', '/source_file.c']);
});
// Resolve a single code offset to a source line to test the correctness of offset computations.
it('use correct code offsets to interpret raw locations', async () => {
const {frontend} = getBrowserAndPages();
await frontend.evaluate(
() => globalThis.installExtensionPlugin((extensionServerClient: unknown, extensionAPI: unknown) => {
class LocationMappingPlugin {
private modules: Map<string, {rawLocationRange?: RawLocationRange, sourceLocation?: SourceLocation}>;
constructor() {
this.modules = new Map();
}
async addRawModule(rawModuleId: string, symbols: string, rawModule: RawModule) {
const sourceFileURL = new URL('unreachable.ll', rawModule.url || symbols).href;
this.modules.set(rawModuleId, {
rawLocationRange: {rawModuleId, startOffset: 6, endOffset: 7},
sourceLocation: {rawModuleId, sourceFileURL, lineNumber: 5, columnNumber: 2},
});
return [sourceFileURL];
}
async rawLocationToSourceLocation(rawLocation: RawLocation) {
const {rawLocationRange, sourceLocation} = this.modules.get(rawLocation.rawModuleId) || {};
if (rawLocationRange && sourceLocation && rawLocationRange.startOffset <= rawLocation.codeOffset &&
rawLocation.codeOffset < rawLocationRange.endOffset) {
return [sourceLocation];
}
return [];
}
}
RegisterExtension(
extensionAPI, new LocationMappingPlugin(), 'Location Mapping',
{language: 'WebAssembly', symbol_types: ['None']});
}));
await openSourcesPanel();
await click(PAUSE_ON_EXCEPTION_BUTTON);
await goToResource('sources/wasm/unreachable.html');
await waitFor('.paused-status');
const callFrameLoc = await waitFor('.call-frame-location');
const scriptLocation = await callFrameLoc.evaluate(location => location.textContent);
assert.deepEqual(scriptLocation, 'unreachable.ll:6');
});
// Resolve the location for a breakpoint.
it('resolve locations for breakpoints correctly', async () => {
const {target, frontend} = getBrowserAndPages();
await frontend.evaluate(
() => globalThis.installExtensionPlugin((extensionServerClient: unknown, extensionAPI: unknown) => {
// This plugin will emulate a source mapping with a single file and a single corresponding source line and byte
// code offset pair.
class LocationMappingPlugin {
private modules: Map<string, {rawLocationRange?: RawLocationRange, sourceLocation?: SourceLocation}>;
constructor() {
this.modules = new Map();
}
async addRawModule(rawModuleId: string, symbols: string, rawModule: RawModule) {
const sourceFileURL = new URL('global_variable.ll', rawModule.url || symbols).href;
this.modules.set(rawModuleId, {
rawLocationRange: {rawModuleId, startOffset: 25, endOffset: 26},
sourceLocation: {rawModuleId, sourceFileURL, lineNumber: 8, columnNumber: -1},
});
return [sourceFileURL];
}
async rawLocationToSourceLocation(rawLocation: RawLocation) {
const {rawLocationRange, sourceLocation} = this.modules.get(rawLocation.rawModuleId) || {};
if (rawLocationRange && sourceLocation && rawLocationRange.startOffset <= rawLocation.codeOffset &&
rawLocation.codeOffset < rawLocationRange.endOffset) {
return [sourceLocation];
}
return [];
}
async sourceLocationToRawLocation(sourceLocationArg: SourceLocation) {
const {rawLocationRange, sourceLocation} = this.modules.get(sourceLocationArg.rawModuleId) || {};
if (rawLocationRange && sourceLocation &&
JSON.stringify(sourceLocation) === JSON.stringify(sourceLocationArg)) {
return [rawLocationRange];
}
return [];
}
async getMappedLines(rawModuleId: string, sourceFileURL: string) {
const {sourceLocation} = this.modules.get(rawModuleId) || {};
if (sourceLocation && sourceLocation.sourceFileURL === sourceFileURL) {
return [5, 6, 7, 8, 9];
}
return undefined;
}
}
RegisterExtension(
extensionAPI, new LocationMappingPlugin(), 'Location Mapping',
{language: 'WebAssembly', symbol_types: ['None']});
}));
await openFileInSourcesPanel('wasm/global_variable.html');
await target.evaluate('go();');
await openFileInEditor('global_variable.ll');
// Line 4 is non-breakable.
await addBreakpointForLine(frontend, 4, true);
await addBreakpointForLine(frontend, 9);
const scriptLocation = await retrieveTopCallFrameScriptLocation('main();', target);
assert.deepEqual(scriptLocation, 'global_variable.ll:9');
await checkBreakpointIsNotActive(4);
});
it('shows top-level and nested variables', async () => {
const {frontend} = getBrowserAndPages();
await frontend.evaluateHandle(
() => globalThis.installExtensionPlugin((extensionServerClient: unknown, extensionAPI: unknown) => {
class VariableListingPlugin {
private modules: Map<string, {rawLocationRange?: RawLocationRange, sourceLocation?: SourceLocation}>;
constructor() {
this.modules = new Map();
}
async addRawModule(rawModuleId: string, symbols: string, rawModule: RawModule) {
const sourceFileURL = new URL('unreachable.ll', rawModule.url || symbols).href;
this.modules.set(rawModuleId, {
rawLocationRange: {rawModuleId, startOffset: 6, endOffset: 7},
sourceLocation: {rawModuleId, sourceFileURL, lineNumber: 5, columnNumber: 2},
});
return [sourceFileURL];
}
async rawLocationToSourceLocation(rawLocation: RawLocation) {
const {rawLocationRange, sourceLocation} = this.modules.get(rawLocation.rawModuleId) || {};
if (rawLocationRange && sourceLocation && rawLocationRange.startOffset <= rawLocation.codeOffset &&
rawLocation.codeOffset < rawLocationRange.endOffset) {
return [sourceLocation];
}
return [];
}
async getScopeInfo(type: string) {
return {type, typeName: type};
}
async listVariablesInScope(rawLocation: RawLocation) {
const {rawLocationRange} = this.modules.get(rawLocation.rawModuleId) || {};
if (rawLocationRange && rawLocationRange.startOffset <= rawLocation.codeOffset &&
rawLocation.codeOffset < rawLocationRange.endOffset) {
return [
{scope: 'LOCAL', name: 'localX', type: 'int'},
{scope: 'GLOBAL', name: 'n1::n2::globalY', nestedName: ['n1', 'n2', 'globalY'], type: 'float'},
];
}
return [];
}
}
RegisterExtension(
extensionAPI, new VariableListingPlugin(), 'Location Mapping',
{language: 'WebAssembly', symbol_types: ['None']});
}));
await openSourcesPanel();
await click(PAUSE_ON_EXCEPTION_BUTTON);
await goToResource('sources/wasm/unreachable.html');
await waitFor(RESUME_BUTTON);
const locals = await getValuesForScope('LOCAL', 0, 1);
assert.deepEqual(locals, ['localX: undefined']);
const globals = await getValuesForScope('GLOBAL', 2, 3);
assert.deepEqual(globals, ['n1: namespace', 'n2: namespace', 'globalY: undefined']);
});
it('shows inline frames', async () => {
const {frontend} = getBrowserAndPages();
await frontend.evaluateHandle(
() => globalThis.installExtensionPlugin((extensionServerClient: unknown, extensionAPI: unknown) => {
class InliningPlugin {
private modules: Map<string, {rawLocationRange?: RawLocationRange, sourceLocations?: SourceLocation[]}>;
constructor() {
this.modules = new Map();
}
async addRawModule(rawModuleId: string, symbols: string, rawModule: RawModule) {
const sourceFileURL = new URL('unreachable.ll', rawModule.url || symbols).href;
this.modules.set(rawModuleId, {
rawLocationRange: {rawModuleId, startOffset: 6, endOffset: 7},
sourceLocations: [
{rawModuleId, sourceFileURL, lineNumber: 5, columnNumber: 2},
{rawModuleId, sourceFileURL, lineNumber: 10, columnNumber: 2},
{rawModuleId, sourceFileURL, lineNumber: 15, columnNumber: 2},
],
});
return [sourceFileURL];
}
async rawLocationToSourceLocation(rawLocation: RawLocation) {
const {rawLocationRange, sourceLocations} = this.modules.get(rawLocation.rawModuleId) || {};
if (rawLocationRange && sourceLocations && rawLocationRange.startOffset <= rawLocation.codeOffset &&
rawLocation.codeOffset < rawLocationRange.endOffset) {
return [sourceLocations[rawLocation.inlineFrameIndex || 0]];
}
return [];
}
async getFunctionInfo(rawLocation: RawLocation) {
const {rawLocationRange} = this.modules.get(rawLocation.rawModuleId) || {};
if (rawLocationRange && rawLocationRange.startOffset <= rawLocation.codeOffset &&
rawLocation.codeOffset < rawLocationRange.endOffset) {
return {frames: [{name: 'inner_inline_func'}, {name: 'outer_inline_func'}, {name: 'Main'}]};
}
return null;
}
async getScopeInfo(type: string) {
return {type, typeName: type};
}
async listVariablesInScope(rawLocation: RawLocation) {
const {rawLocationRange} = this.modules.get(rawLocation.rawModuleId) || {};
const frame = rawLocation.inlineFrameIndex || 0;
if (rawLocationRange && rawLocationRange.startOffset <= rawLocation.codeOffset &&
rawLocation.codeOffset < rawLocationRange.endOffset) {
return [
{scope: 'LOCAL', name: `localX${frame}`, type: 'int'},
];
}
return [];
}
}
RegisterExtension(
extensionAPI, new InliningPlugin(), 'Inlining', {language: 'WebAssembly', symbol_types: ['None']});
}));
await openSourcesPanel();
await click(PAUSE_ON_EXCEPTION_BUTTON);
await goToResource('sources/wasm/unreachable.html');
await waitFor(RESUME_BUTTON);
// Call stack shows inline function names and source locations.
const funcNames = await getCallFrameNames();
assert.deepEqual(
funcNames, ['inner_inline_func', 'outer_inline_func', 'Main', 'go', 'await in go (async)', '(anonymous)']);
const sourceLocations = await getCallFrameLocations();
assert.deepEqual(
sourceLocations,
['unreachable.ll:6', 'unreachable.ll:11', 'unreachable.ll:16', 'unreachable.html:27', 'unreachable.html:30']);
// We see variables for innermost frame.
assert.deepEqual(await getValuesForScope('LOCAL', 0, 1), ['localX0: undefined']);
// Switching frames affects what variables we see.
await switchToCallFrame(2);
assert.deepEqual(await getValuesForScope('LOCAL', 0, 1), ['localX1: undefined']);
await switchToCallFrame(3);
assert.deepEqual(await getValuesForScope('LOCAL', 0, 1), ['localX2: undefined']);
});
it('falls back to wasm function names when inline info not present', async () => {
const {frontend} = getBrowserAndPages();
await frontend.evaluateHandle(
() => globalThis.installExtensionPlugin((extensionServerClient: unknown, extensionAPI: unknown) => {
class InliningPlugin {
private modules: Map<string, {rawLocationRange?: RawLocationRange, sourceLocations?: SourceLocation[]}>;
constructor() {
this.modules = new Map();
}
async addRawModule(rawModuleId: string, symbols: string, rawModule: RawModule) {
const sourceFileURL = new URL('unreachable.ll', rawModule.url || symbols).href;
this.modules.set(rawModuleId, {
rawLocationRange: {rawModuleId, startOffset: 6, endOffset: 7},
sourceLocations: [
{rawModuleId, sourceFileURL, lineNumber: 5, columnNumber: 2},
],
});
return [sourceFileURL];
}
async rawLocationToSourceLocation(rawLocation: RawLocation) {
const {rawLocationRange, sourceLocations} = this.modules.get(rawLocation.rawModuleId) || {};
if (rawLocationRange && sourceLocations && rawLocationRange.startOffset <= rawLocation.codeOffset &&
rawLocation.codeOffset < rawLocationRange.endOffset) {
return [sourceLocations[rawLocation.inlineFrameIndex || 0]];
}
return [];
}
async getFunctionInfo(rawLocation: RawLocation) {
const {rawLocationRange} = this.modules.get(rawLocation.rawModuleId) || {};
if (rawLocationRange && rawLocationRange.startOffset <= rawLocation.codeOffset &&
rawLocation.codeOffset < rawLocationRange.endOffset) {
return {frames: []};
}
return null;
}
async getScopeInfo(type: string) {
return {type, typeName: type};
}
async listVariablesInScope(_rawLocation: RawLocation) {
return [];
}
}
RegisterExtension(
extensionAPI, new InliningPlugin(), 'Inlining', {language: 'WebAssembly', symbol_types: ['None']});
}));
await openSourcesPanel();
await click(PAUSE_ON_EXCEPTION_BUTTON);
await goToResource('sources/wasm/unreachable.html');
await waitFor(RESUME_BUTTON);
// Call stack shows inline function names and source locations.
const funcNames = await getCallFrameNames();
assert.deepEqual(funcNames, ['Main', 'go', 'await in go (async)', '(anonymous)']);
const sourceLocations = await getCallFrameLocations();
assert.deepEqual(sourceLocations, ['unreachable.ll:6', 'unreachable.html:27', 'unreachable.html:30']);
});
it('shows variable values with JS formatters', async () => {
const {frontend} = getBrowserAndPages();
await frontend.evaluateHandle(
() => globalThis.installExtensionPlugin((extensionServerClient: unknown, extensionAPI: unknown) => {
class VariableListingPlugin {
private modules: Map<string, {rawLocationRange?: RawLocationRange, sourceLocation?: SourceLocation}>;
constructor() {
this.modules = new Map();
}
async addRawModule(rawModuleId: string, symbols: string, rawModule: RawModule) {
const sourceFileURL = new URL('unreachable.ll', rawModule.url || symbols).href;
this.modules.set(rawModuleId, {
rawLocationRange: {rawModuleId, startOffset: 6, endOffset: 7},
sourceLocation: {rawModuleId, sourceFileURL, lineNumber: 5, columnNumber: 2},
});
return [sourceFileURL];
}
async rawLocationToSourceLocation(rawLocation: RawLocation) {
const {rawLocationRange, sourceLocation} = this.modules.get(rawLocation.rawModuleId) || {};
if (rawLocationRange && sourceLocation && rawLocationRange.startOffset <= rawLocation.codeOffset &&
rawLocation.codeOffset < rawLocationRange.endOffset) {
return [sourceLocation];
}
return [];
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async listVariablesInScope(rawLocation: RawLocation) {
return [{scope: 'LOCAL', name: 'local', type: 'TestType'}];
}
async getScopeInfo(type: string) {
return {type, typeName: type};
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async getTypeInfo(expression: string, context: RawLocation):
Promise<{typeInfos: TypeInfo[], base: EvalBase}|null> {
if (expression === 'local') {
const typeInfos = [
{
typeNames: ['TestType'],
typeId: 'TestType',
members: [{name: 'member', offset: 1, typeId: 'TestTypeMember'}],
alignment: 0,
arraySize: 0,
size: 4,
canExpand: true,
hasValue: false,
},
{
typeNames: ['TestTypeMember'],
typeId: 'TestTypeMember',
members: [{name: 'member2', offset: 1, typeId: 'TestTypeMember2'}],
alignment: 0,
arraySize: 0,
size: 3,
canExpand: true,
hasValue: false,
},
{
typeNames: ['TestTypeMember2'],
typeId: 'TestTypeMember2',
members: [],
alignment: 0,
arraySize: 0,
size: 2,
canExpand: false,
hasValue: true,
},
{
typeNames: ['int'],
typeId: 'int',
members: [],
alignment: 0,
arraySize: 0,
size: 4,
canExpand: false,
hasValue: true,
},
];
const base = {rootType: typeInfos[0], payload: 28};
return {typeInfos, base};
}
return null;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async getFormatter(expressionOrField: string|{base: EvalBase, field: FieldInfo[]}, context: RawLocation):
Promise<{js: string}|null> {
function format() {
const sym = Symbol('sym');
class $tag {
[sym]: EvalBase;
constructor() {
const rootType = {
typeNames: ['int'],
typeId: 'int',
members: [],
alignment: 0,
arraySize: 0,
size: 4,
canExpand: false,
hasValue: true,
};
this[sym] = {payload: {value: 19}, rootType};
}
}
const value = {value: 26, recurse: new $tag()};
return {tag: {className: '$tag', symbol: sym}, value};
}
if (typeof expressionOrField === 'string') {
return null;
}
const {base, field} = expressionOrField;
if (base.payload === 28 && field.length === 2 && field[0].name === 'member' && field[0].offset === 1 &&
field[0].typeId === 'TestTypeMember' && field[1].name === 'member2' && field[1].offset === 1 &&
field[1].typeId === 'TestTypeMember2') {
return {js: `${format.toString()} format()`};
}
if ((base.payload as {value: number}).value === 19 && field.length === 0) {
return {js: '27'};
}
return null;
}
}
RegisterExtension(
extensionAPI, new VariableListingPlugin(), 'Location Mapping',
{language: 'WebAssembly', symbol_types: ['None']});
}));
await openSourcesPanel();
await click(PAUSE_ON_EXCEPTION_BUTTON);
await goToResource('sources/wasm/unreachable.html');
await waitFor(RESUME_BUTTON);
const locals = await getValuesForScope('LOCAL', 3, 5);
assert.deepEqual(locals, [
'local: TestType',
'member: TestTypeMember',
'member2: TestTypeMember2',
'recurse: 27',
'value: 26',
'__proto__: Object',
]);
});
// Failing on the bots with a timeout
it.skip('[crbug.com/1169688]: shows variable value in popover', async () => {
const {frontend} = getBrowserAndPages();
await frontend.evaluateHandle(
() => globalThis.installExtensionPlugin((extensionServerClient: unknown, extensionAPI: unknown) => {
class VariableListingPlugin {
private modules: Map<string, {rawLocationRange?: RawLocationRange, sourceLocation?: SourceLocation}>;
constructor() {
this.modules = new Map();
}
async addRawModule(rawModuleId: string, symbols: string, rawModule: RawModule) {
const sourceFileURL = new URL('unreachable.ll', rawModule.url || symbols).href;
this.modules.set(rawModuleId, {
rawLocationRange: {rawModuleId, startOffset: 6, endOffset: 7},
sourceLocation: {rawModuleId, sourceFileURL, lineNumber: 5, columnNumber: 2},
});
return [sourceFileURL];
}
async rawLocationToSourceLocation(rawLocation: RawLocation) {
const {rawLocationRange, sourceLocation} = this.modules.get(rawLocation.rawModuleId) || {};
if (rawLocationRange && sourceLocation && rawLocationRange.startOffset <= rawLocation.codeOffset &&
rawLocation.codeOffset < rawLocationRange.endOffset) {
return [sourceLocation];
}
return [];
}
async getScopeInfo(type: string) {
return {type, typeName: type};
}
async listVariablesInScope(rawLocation: RawLocation) {
const {rawLocationRange} = this.modules.get(rawLocation.rawModuleId) || {};
const {codeOffset} = rawLocation;
if (!rawLocationRange || rawLocationRange.startOffset > codeOffset ||
rawLocationRange.endOffset <= codeOffset) {
return [];
}
// The source code is LLVM IR so there are no meaningful variable names. Most tokens are however
// identified as js-variable tokens by codemirror, so we can pretend they're variables. The unreachable
// instruction is where we pause at, so it's really easy to find in the page and is a great mock variable
// candidate.
return [{scope: 'LOCAL', name: 'unreachable', type: 'int'}];
}
async evaluateVariable(
name: string, location: RawLocation) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (name === 'unreachable') {
return {constantValue: {value: '23', js_type: 'number', type: 'int', name: 'unreachable'}};
}
return null;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async getTypeInfo(expression: string, context: RawLocation):
Promise<{typeInfos: TypeInfo[], base: EvalBase}|null> {
if (expression === 'unreachable') {
const typeInfos = [{
typeNames: ['int'],
typeId: 'int',
members: [],
alignment: 0,
arraySize: 0,
size: 4,
canExpand: false,
hasValue: true,
}];
const base = {rootType: typeInfos[0], payload: 28};
return {typeInfos, base};
}
return null;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async getFormatter(expressionOrField: string|{base: EvalBase, field: FieldInfo[]}, context: RawLocation):
Promise<{js: string}|null> {
return {js: '23'};
}
}
RegisterExtension(
extensionAPI, new VariableListingPlugin(), 'Location Mapping',
{language: 'WebAssembly', symbol_types: ['None']});
}));
await openSourcesPanel();
await click(PAUSE_ON_EXCEPTION_BUTTON);
await goToResource('sources/wasm/unreachable.html');
await waitFor(RESUME_BUTTON);
const pausedPosition = await waitForFunction(async () => {
const element = await $('.cm-execution-line-tail');
if (element && await element.evaluate(e => e.isConnected)) {
return element;
}
return undefined;
});
await pausedPosition.hover();
const popover = await waitFor('[data-stable-name-for-test="object-popover-content"]');
const value = await waitFor('.object-value-number', popover).then(e => e.evaluate(node => node.textContent));
assert.strictEqual(value, '23');
});
it('shows sensible error messages.', async () => {
const {frontend} = getBrowserAndPages();
await frontend.evaluateHandle(
() => globalThis.installExtensionPlugin((extensionServerClient: unknown, extensionAPI: unknown) => {
class FormattingErrorsPlugin {
private modules: Map<string, {rawLocationRange?: RawLocationRange, sourceLocation?: SourceLocation}>;
constructor() {
this.modules = new Map();
}
async addRawModule(rawModuleId: string, symbols: string, rawModule: RawModule) {
const sourceFileURL = new URL('unreachable.ll', rawModule.url || symbols).href;
this.modules.set(rawModuleId, {
rawLocationRange: {rawModuleId, startOffset: 6, endOffset: 7},
sourceLocation: {rawModuleId, sourceFileURL, lineNumber: 5, columnNumber: 2},
});
return [sourceFileURL];
}
async rawLocationToSourceLocation(rawLocation: RawLocation) {
const {rawLocationRange, sourceLocation} = this.modules.get(rawLocation.rawModuleId) || {};
if (rawLocationRange && sourceLocation && rawLocationRange.startOffset <= rawLocation.codeOffset &&
rawLocation.codeOffset < rawLocationRange.endOffset) {
return [sourceLocation];
}
return [];
}
async getScopeInfo(type: string) {
return {type, typeName: type};
}
async listVariablesInScope(rawLocation: RawLocation) {
const {rawLocationRange} = this.modules.get(rawLocation.rawModuleId) || {};
const {codeOffset} = rawLocation;
if (!rawLocationRange || rawLocationRange.startOffset > codeOffset ||
rawLocationRange.endOffset <= codeOffset) {
return [];
}
return [{scope: 'LOCAL', name: 'unreachable', type: 'int'}];
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async getTypeInfo(expression: string, context: RawLocation):
Promise<{typeInfos: TypeInfo[], base: EvalBase}|null> {
if (expression === 'foo') {
const typeInfos = [{
typeNames: ['int'],
typeId: 'int',
members: [],
alignment: 0,
arraySize: 0,
size: 4,
canExpand: false,
hasValue: true,
}];
const base = {rootType: typeInfos[0], payload: 28};
return {typeInfos, base};
}
throw new Error(`No typeinfo for ${expression}`);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async getFormatter(expressionOrField: string|{base: EvalBase, field: FieldInfo[]}, context: RawLocation):
Promise<{js: string}|null> {
if (typeof expressionOrField !== 'string' && expressionOrField.base.payload as number === 28 &&
expressionOrField.field.length === 0) {
return {js: '23'};
}
throw new Error(`cannot format ${expressionOrField}`);
}
}
RegisterExtension(
extensionAPI, new FormattingErrorsPlugin(), 'Formatter Errors',
{language: 'WebAssembly', symbol_types: ['None']});
}));
await openSourcesPanel();
await click(PAUSE_ON_EXCEPTION_BUTTON);
await goToResource('sources/wasm/unreachable.html');
await waitFor(RESUME_BUTTON);
const locals = await getValuesForScope('LOCAL', 0, 1);
assert.deepStrictEqual(locals, ['unreachable: undefined']);
const watchPane = await waitFor('[aria-label="Watch"]');
const isExpanded = await watchPane.evaluate(element => {
return element.getAttribute('aria-expanded') === 'true';
});
if (!isExpanded) {
await click('.title-expand-icon', {root: watchPane});
}
await click('[aria-label="Add watch expression"]');
await waitFor('.watch-expression-editing');
await pasteText('foo');
await frontend.keyboard.press('Enter');
await waitForNone('.watch-expression-editing');
await click('[aria-label="Add watch expression"]');
await waitFor('.watch-expression-editing');
await pasteText('bar');
await frontend.keyboard.press('Enter');
await waitForNone('.watch-expression-editing');
const watchResults = await waitForMany('.watch-expression', 2);
const watchTexts = await Promise.all(watchResults.map(async watch => await watch.evaluate(e => e.textContent)));
assert.deepStrictEqual(watchTexts, ['foo: 23', 'bar: <not available>']);
await watchResults[1].hover();
const tooltip = await waitFor('.tooltip');
const tooltipText = await tooltip.evaluate(e => e.textContent);
assert.strictEqual(tooltipText, 'No typeinfo for bar');
await click(CONSOLE_TAB_SELECTOR);
await focusConsolePrompt();
await pasteText('bar');
await frontend.keyboard.press('Enter');
// Wait for the console to be usable again.
await frontend.waitForFunction(() => {
return document.querySelectorAll('.console-user-command-result').length === 1;
});
const messages = await getCurrentConsoleMessages();
assert.deepStrictEqual(messages.filter(m => !m.startsWith('[Formatter Errors]')), ['Uncaught No typeinfo for bar']);
});
});