blob: 6a2f6573f89dd51cbbbf092207790f59d3a68cc8 [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 type {Chrome} from '../../../extension-api/ExtensionAPI.js';
import {
$,
click,
enableExperiment,
getBrowserAndPages,
goToResource,
pasteText,
waitFor,
waitForFunction,
waitForMany,
waitForNone,
} from '../../shared/helper.js';
import {describe, it} from '../../shared/mocha-extensions.js';
import {getResourcesPathWithDevToolsHostname, loadExtension} from '../helpers/extension-helpers.js';
import {
CONSOLE_TAB_SELECTOR,
focusConsolePrompt,
getCurrentConsoleMessages,
getStructuredConsoleMessages,
} from '../helpers/console-helpers.js';
import {
addBreakpointForLine,
getCallFrameLocations,
getCallFrameNames,
getNonBreakableLines,
getValuesForScope,
isBreakpointSet,
listenForSourceFilesAdded,
openFileInEditor,
openFileInSourcesPanel,
openSourceCodeEditorForFile,
openSourcesPanel,
PAUSE_ON_EXCEPTION_BUTTON,
RESUME_BUTTON,
retrieveSourceFilesAdded,
retrieveTopCallFrameScriptLocation,
switchToCallFrame,
waitForAdditionalSourceFiles,
} from '../helpers/sources-helpers.js';
// eslint-disable-next-line @typescript-eslint/naming-convention
declare function RegisterExtension(
pluginImpl: Partial<Chrome.DevTools.LanguageExtensionPlugin>, name: string,
// eslint-disable-next-line @typescript-eslint/naming-convention
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');
});
// 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();
const extension = await loadExtension(
'TestExtension', `${getResourcesPathWithDevToolsHostname()}/extensions/language_extensions.html`);
await extension.evaluate(() => {
// A simple plugin that resolves to a single source file
class SingleFilePlugin {
async addRawModule(rawModuleId: string, symbols: string, rawModule: Chrome.DevTools.RawModule) {
const fileUrl = new URL('/source_file.c', rawModule.url || symbols);
return [fileUrl.href];
}
}
RegisterExtension(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 extension = await loadExtension(
'TestExtension', `${getResourcesPathWithDevToolsHostname()}/extensions/language_extensions.html`);
await extension.evaluate(() => {
class LocationMappingPlugin {
private modules:
Map<string,
{rawLocationRange?: Chrome.DevTools.RawLocationRange, sourceLocation?: Chrome.DevTools.SourceLocation}>;
constructor() {
this.modules = new Map();
}
async addRawModule(rawModuleId: string, symbols: string, rawModule: Chrome.DevTools.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: Chrome.DevTools.RawLocation) {
const {rawLocationRange, sourceLocation} = this.modules.get(rawLocation.rawModuleId) || {};
if (rawLocationRange && sourceLocation && rawLocationRange.startOffset <= rawLocation.codeOffset &&
rawLocation.codeOffset < rawLocationRange.endOffset) {
return [sourceLocation];
}
return [];
}
}
RegisterExtension(
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');
await click(RESUME_BUTTON);
const error = await waitForFunction(async () => {
const messages = await getStructuredConsoleMessages();
return messages.find(message => message.message.startsWith('Uncaught (in promise) RuntimeError: unreachable'));
});
const callframes = error.message.split('\n').slice(1);
assert.deepEqual(callframes, [' at Main (unreachable.ll:6:3)', ' at go (unreachable.html:27:29)']);
});
// Resolve the location for a breakpoint.
it('resolve locations for breakpoints correctly', async () => {
const {target, frontend} = getBrowserAndPages();
const extension = await loadExtension(
'TestExtension', `${getResourcesPathWithDevToolsHostname()}/extensions/language_extensions.html`);
await extension.evaluate(() => {
// 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?: Chrome.DevTools.RawLocationRange, sourceLocation?: Chrome.DevTools.SourceLocation}>;
constructor() {
this.modules = new Map();
}
async addRawModule(rawModuleId: string, symbols: string, rawModule: Chrome.DevTools.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: Chrome.DevTools.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: Chrome.DevTools.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(
new LocationMappingPlugin(), 'Location Mapping', {language: 'WebAssembly', symbol_types: ['None']});
});
await openFileInSourcesPanel('wasm/global_variable.html');
await target.evaluate('go();');
await openFileInEditor('global_variable.ll');
const toolbar = await waitFor('.sources-toolbar');
const itemElements = await waitForMany('.toolbar-item', 2, toolbar);
const items = await Promise.all(itemElements.map(e => e.evaluate(e => e.textContent)));
assert.isAtLeast(
items.indexOf('(provided via debug info by global_variable.wasm)'), 0, 'Toolbar debug info hint not found');
// Line 4 is non-breakable.
assert.include(await getNonBreakableLines(), 4);
await addBreakpointForLine(frontend, 9);
const scriptLocation = await retrieveTopCallFrameScriptLocation('main();', target);
assert.deepEqual(scriptLocation, 'global_variable.ll:9');
await waitForFunction(async () => !(await isBreakpointSet(4)));
});
it('shows top-level and nested variables', async () => {
const extension = await loadExtension(
'TestExtension', `${getResourcesPathWithDevToolsHostname()}/extensions/language_extensions.html`);
await extension.evaluateHandle(() => {
class VariableListingPlugin {
private modules:
Map<string,
{rawLocationRange?: Chrome.DevTools.RawLocationRange, sourceLocation?: Chrome.DevTools.SourceLocation}>;
constructor() {
this.modules = new Map();
}
async addRawModule(rawModuleId: string, symbols: string, rawModule: Chrome.DevTools.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: Chrome.DevTools.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: Chrome.DevTools.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(
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 extension = await loadExtension(
'TestExtension', `${getResourcesPathWithDevToolsHostname()}/extensions/language_extensions.html`);
await extension.evaluate(() => {
class InliningPlugin {
private modules: Map<string, {
rawLocationRange?: Chrome.DevTools.RawLocationRange,
sourceLocations?: Chrome.DevTools.SourceLocation[],
}>;
constructor() {
this.modules = new Map();
}
async addRawModule(rawModuleId: string, symbols: string, rawModule: Chrome.DevTools.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: Chrome.DevTools.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: Chrome.DevTools.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 {frames: []};
}
async getScopeInfo(type: string) {
return {type, typeName: type};
}
async listVariablesInScope(rawLocation: Chrome.DevTools.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(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']);
await click(RESUME_BUTTON);
await waitForFunction(async () => {
const messages = await getStructuredConsoleMessages();
if (!messages.length) {
return false;
}
const message = messages[messages.length - 1];
return message.message === `Uncaught (in promise) RuntimeError: unreachable
at inner_inline_func (unreachable.ll:6)
at outer_inline_func (unreachable.ll:11)
at Main (unreachable.ll:16)
at go (unreachable.html:27:29)`;
});
});
it('falls back to wasm function names when inline info not present', async () => {
const extension = await loadExtension(
'TestExtension', `${getResourcesPathWithDevToolsHostname()}/extensions/language_extensions.html`);
await extension.evaluate(() => {
class InliningPlugin {
private modules: Map<string, {
rawLocationRange?: Chrome.DevTools.RawLocationRange,
sourceLocations?: Chrome.DevTools.SourceLocation[],
}>;
constructor() {
this.modules = new Map();
}
async addRawModule(rawModuleId: string, symbols: string, rawModule: Chrome.DevTools.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: Chrome.DevTools.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: Chrome.DevTools.RawLocation) {
const {rawLocationRange} = this.modules.get(rawLocation.rawModuleId) || {};
if (rawLocationRange && rawLocationRange.startOffset <= rawLocation.codeOffset &&
rawLocation.codeOffset < rawLocationRange.endOffset) {
return {frames: []};
}
return {frames: []};
}
async getScopeInfo(type: string) {
return {type, typeName: type};
}
async listVariablesInScope(_rawLocation: Chrome.DevTools.RawLocation) {
return [];
}
}
RegisterExtension(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 extension = await loadExtension(
'TestExtension', `${getResourcesPathWithDevToolsHostname()}/extensions/language_extensions.html`);
await extension.evaluate(() => {
class VariableListingPlugin {
private modules:
Map<string,
{rawLocationRange?: Chrome.DevTools.RawLocationRange, sourceLocation?: Chrome.DevTools.SourceLocation}>;
constructor() {
this.modules = new Map();
}
async addRawModule(rawModuleId: string, symbols: string, rawModule: Chrome.DevTools.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: Chrome.DevTools.RawLocation) {
const {rawLocationRange, sourceLocation} = this.modules.get(rawLocation.rawModuleId) || {};
if (rawLocationRange && sourceLocation && rawLocationRange.startOffset <= rawLocation.codeOffset &&
rawLocation.codeOffset < rawLocationRange.endOffset) {
return [sourceLocation];
}
return [];
}
async listVariablesInScope(_rawLocation: Chrome.DevTools.RawLocation) {
return [{scope: 'LOCAL', name: 'local', type: 'TestType'}];
}
async getScopeInfo(type: string) {
return {type, typeName: type};
}
async getTypeInfo(expression: string, _context: Chrome.DevTools.RawLocation):
Promise<{typeInfos: Chrome.DevTools.TypeInfo[], base: Chrome.DevTools.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;
}
async getFormatter(
expressionOrField: string|{base: Chrome.DevTools.EvalBase, field: Chrome.DevTools.FieldInfo[]},
_context: Chrome.DevTools.RawLocation): Promise<{js: string}|null> {
function formatWithDescription(description: string) {
const sym = Symbol('sym');
const tag = {className: '$tag', symbol: sym};
return {tag, value: 27, description};
}
function format(description?: string) {
const sym = Symbol('sym');
const tag = {className: '$tag', symbol: sym};
class $tag {
[sym]: Chrome.DevTools.EvalBase;
constructor(value: number) {
const rootType = {
typeNames: ['int'],
typeId: 'int',
members: [],
alignment: 0,
arraySize: 0,
size: 4,
canExpand: false,
hasValue: true,
};
this[sym] = {payload: {value}, rootType};
}
}
const value = {value: 26, recurse: new $tag(19), describe: new $tag(20)};
Object.setPrototypeOf(value, null);
return {tag, value, description};
}
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})()`};
}
if ((base.payload as {value: number}).value === 19 && field.length === 0) {
return {js: '27'};
}
if ((base.payload as {value: number}).value === 20 && field.length === 0) {
return {js: `(${formatWithDescription})('CustomLabel')`};
}
return null;
}
}
RegisterExtension(
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, 6);
assert.deepEqual(locals, [
'local: TestType',
'member: TestTypeMember',
'member2: TestTypeMember2',
'describe: CustomLabel',
'recurse: 27',
'value: 26',
]);
});
it('shows variable value in popover', async () => {
const extension = await loadExtension(
'TestExtension', `${getResourcesPathWithDevToolsHostname()}/extensions/language_extensions.html`);
await extension.evaluate(() => {
class VariableListingPlugin {
private modules:
Map<string,
{rawLocationRange?: Chrome.DevTools.RawLocationRange, sourceLocation?: Chrome.DevTools.SourceLocation}>;
constructor() {
this.modules = new Map();
}
async addRawModule(rawModuleId: string, symbols: string, rawModule: Chrome.DevTools.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: Chrome.DevTools.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: Chrome.DevTools.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 getTypeInfo(expression: string, _context: Chrome.DevTools.RawLocation):
Promise<{typeInfos: Chrome.DevTools.TypeInfo[], base: Chrome.DevTools.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;
}
async getFormatter(
_expressionOrField: string|{base: Chrome.DevTools.EvalBase, field: Chrome.DevTools.FieldInfo[]},
_context: Chrome.DevTools.RawLocation): Promise<{js: string}|null> {
return {js: '23'};
}
}
RegisterExtension(
new VariableListingPlugin(), 'Location Mapping', {language: 'WebAssembly', symbol_types: ['None']});
});
await openSourcesPanel();
await click(PAUSE_ON_EXCEPTION_BUTTON);
await openSourceCodeEditorForFile('unreachable.ll', 'wasm/unreachable.html');
await waitFor(RESUME_BUTTON);
const pausedPosition = await waitForFunction(async () => {
const element = await $('.cm-executionToken');
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();
const extension = await loadExtension(
'TestExtension', `${getResourcesPathWithDevToolsHostname()}/extensions/language_extensions.html`);
await extension.evaluate(() => {
class FormattingErrorsPlugin {
private modules:
Map<string,
{rawLocationRange?: Chrome.DevTools.RawLocationRange, sourceLocation?: Chrome.DevTools.SourceLocation}>;
constructor() {
this.modules = new Map();
}
async addRawModule(rawModuleId: string, symbols: string, rawModule: Chrome.DevTools.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: Chrome.DevTools.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: Chrome.DevTools.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'}];
}
async getTypeInfo(expression: string, _context: Chrome.DevTools.RawLocation):
Promise<{typeInfos: Chrome.DevTools.TypeInfo[], base: Chrome.DevTools.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}`);
}
async getFormatter(
expressionOrField: string|{base: Chrome.DevTools.EvalBase, field: Chrome.DevTools.FieldInfo[]},
_context: Chrome.DevTools.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(
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>']);
const tooltipText = await watchResults[1].evaluate(e => {
const errorElement = e.querySelector('.watch-expression-error');
if (!errorElement) {
return 'NO ERROR COULD BE FOUND';
}
return errorElement.getAttribute('title');
});
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']);
});
});