[go: nahoru, domu]

blob: 6eeee006af16b394f417aff5b205add3ce54ab4c [file] [log] [blame]
// 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'; // eslint-disable-line rulesdir/es_modules_import
import {
$,
$$,
assertNotNullOrUndefined,
click,
enableExperiment,
getBrowserAndPages,
getResourcesPath,
goToResource,
pasteText,
pressKey,
typeText,
waitFor,
waitForAria,
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 {
getCallFrameLocations,
getCallFrameNames,
getNonBreakableLines,
getValuesForScope,
openFileInEditor,
openSourceCodeEditorForFile,
openSourcesPanel,
RESUME_BUTTON,
switchToCallFrame,
WasmLocationLabels,
type LabelMapping,
captureAddedSourceFiles,
PAUSE_ON_UNCAUGHT_EXCEPTION_SELECTOR,
} from '../helpers/sources-helpers.js';
import {expectError} from '../../conductor/events.js';
declare global {
let chrome: Chrome.DevTools.Chrome;
interface Window {
// eslint-disable-next-line @typescript-eslint/naming-convention
Module: {instance: WebAssembly.Instance};
}
}
// 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} = 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 goToResource(
'extensions/wasm_module.html?module=/test/e2e/resources/extensions/global_variable.wasm&defer=1');
await openSourcesPanel();
const capturedFileNames = await captureAddedSourceFiles(2, () => target.evaluate('loadModule();'));
assert.deepEqual(capturedFileNames, ['/test/e2e/resources/extensions/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`);
const locationLabels = WasmLocationLabels.load('extensions/unreachable.wat', 'extensions/unreachable.wasm');
await extension.evaluate((mappings: LabelMapping[]) => {
class LocationMappingPlugin {
private module: undefined|{rawModuleId: string, sourceFileURL: string} = undefined;
async addRawModule(rawModuleId: string, symbols: string, rawModule: Chrome.DevTools.RawModule) {
if (this.module) {
throw new Error('Expected only one module');
}
const sourceFileURL = new URL('unreachable.wat', rawModule.url || symbols).href;
this.module = {rawModuleId, sourceFileURL};
return [sourceFileURL];
}
async rawLocationToSourceLocation(rawLocation: Chrome.DevTools.RawLocation) {
if (this.module) {
const {rawModuleId, sourceFileURL} = this.module;
if (rawModuleId === rawLocation.rawModuleId) {
const mapping = mappings.find(m => rawLocation.codeOffset === m.bytecode);
if (mapping) {
return [{rawModuleId, sourceFileURL, lineNumber: mapping.sourceLine - 1, columnNumber: -1}];
}
}
}
return [];
}
}
RegisterExtension(
new LocationMappingPlugin(), 'Location Mapping', {language: 'WebAssembly', symbol_types: ['None']});
}, locationLabels.getMappingsForPlugin());
await openSourcesPanel();
await click(PAUSE_ON_UNCAUGHT_EXCEPTION_SELECTOR);
await goToResource('extensions/wasm_module.html?module=unreachable.wasm&autorun=Main');
await waitFor('.paused-status');
const pauseLocation = await locationLabels.checkLocationForLabel('PAUSED(unreachable)');
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.wat:${pauseLocation.sourceLine})`,
' at window.loadModule (wasm_module.html?mod…&autorun=Main:24:46)',
]);
});
// Resolve the location for a breakpoint.
it('resolve locations for breakpoints correctly', async () => {
const locationLabels = WasmLocationLabels.load('extensions/global_variable.wat', 'extensions/global_variable.wasm');
const extension = await loadExtension(
'TestExtension', `${getResourcesPathWithDevToolsHostname()}/extensions/language_extensions.html`);
await extension.evaluate((mappings: LabelMapping[]) => {
// 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 module: undefined|{rawModuleId: string, sourceFileURL: string} = undefined;
async addRawModule(rawModuleId: string, symbols: string, rawModule: Chrome.DevTools.RawModule) {
if (this.module) {
throw new Error('Expected only one module');
}
const sourceFileURL = new URL('global_variable.wat', rawModule.url || symbols).href;
this.module = {rawModuleId, sourceFileURL};
return [sourceFileURL];
}
async rawLocationToSourceLocation(rawLocation: Chrome.DevTools.RawLocation) {
if (this.module) {
const {rawModuleId, sourceFileURL} = this.module;
if (rawModuleId === rawLocation.rawModuleId) {
const mapping = mappings.find(m => rawLocation.codeOffset === m.bytecode);
if (mapping) {
return [{rawModuleId, sourceFileURL, lineNumber: mapping.sourceLine - 1, columnNumber: -1}];
}
}
}
return [];
}
async sourceLocationToRawLocation(sourceLocation: Chrome.DevTools.SourceLocation):
Promise<Chrome.DevTools.RawLocationRange[]> {
if (this.module) {
const {rawModuleId, sourceFileURL} = this.module;
if (rawModuleId === sourceLocation.rawModuleId && sourceFileURL === sourceLocation.sourceFileURL) {
const mapping = mappings.find(m => sourceLocation.lineNumber === m.sourceLine - 1);
if (mapping) {
return [{rawModuleId, startOffset: mapping.bytecode, endOffset: mapping.bytecode + 1}];
}
}
}
return [];
}
async getMappedLines(rawModuleIdArg: string, sourceFileURLArg: string) {
if (this.module) {
const {rawModuleId, sourceFileURL} = this.module;
if (rawModuleId === rawModuleIdArg && sourceFileURL === sourceFileURLArg) {
return Array.from(new Set(mappings.map(m => m.sourceLine - 1)).values()).sort();
}
}
return undefined;
}
}
RegisterExtension(
new LocationMappingPlugin(), 'Location Mapping', {language: 'WebAssembly', symbol_types: ['None']});
}, locationLabels.getMappingsForPlugin());
await goToResource('extensions/wasm_module.html?module=/test/e2e/resources/extensions/global_variable.wasm');
await openSourcesPanel();
await openFileInEditor('global_variable.wat');
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');
assert.isNotEmpty(await getNonBreakableLines());
await locationLabels.setBreakpointInSourceAndRun('BREAK(return)', 'Module.instance.exports.Main();');
// FIXME(pfaffe) what was the point of this check?
// 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_UNCAUGHT_EXCEPTION_SELECTOR);
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_UNCAUGHT_EXCEPTION_SELECTOR);
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_UNCAUGHT_EXCEPTION_SELECTOR);
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 a warning when no debug info is present', async () => {
const extension = await loadExtension(
'TestExtension', `${getResourcesPathWithDevToolsHostname()}/extensions/language_extensions.html`);
await extension.evaluate(() => {
class MissingInfoPlugin {
private modules: Map<string, {
rawLocationRange?: Chrome.DevTools.RawLocationRange,
sourceLocations?: Chrome.DevTools.SourceLocation[],
}>;
constructor() {
this.modules = new Map();
}
async addRawModule() {
return {missingSymbolFiles: ['test.wasm']};
}
}
RegisterExtension(new MissingInfoPlugin(), 'MissingInfo', {language: 'WebAssembly', symbol_types: ['None']});
});
await openSourcesPanel();
await click(PAUSE_ON_UNCAUGHT_EXCEPTION_SELECTOR);
await goToResource('sources/wasm/unreachable.html');
await waitFor(RESUME_BUTTON);
const incompleteMessage = `Failed to load any debug info for ${getResourcesPath()}/sources/wasm/unreachable.wasm.`;
const infoBar = await waitFor(`.infobar-error[aria-label="${incompleteMessage}"`);
const details = await waitFor('.infobar-details-rows', infoBar);
const text = await details.evaluate(e => e.textContent);
assert.deepEqual(text, 'Failed to load debug file "test.wasm".');
const banners = await $$('.call-frame-warnings-message');
const bannerTexts = await Promise.all(banners.map(e => e.evaluate(e => e.textContent)));
assert.include(bannerTexts, 'Some call frames have warnings');
const selectedCallFrame = await waitFor('.call-frame-item[aria-selected="true"]');
const warning = await waitFor('.call-frame-warning-icon', selectedCallFrame);
const title = await warning.evaluate(e => e.getAttribute('title'));
assert.deepEqual(title, 'No debug information for function "$Main"');
});
it('shows warnings when function info not present', async () => {
const extension = await loadExtension(
'TestExtension', `${getResourcesPathWithDevToolsHostname()}/extensions/language_extensions.html`);
await extension.evaluate(() => {
class MissingInfoPlugin {
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() {
return {missingSymbolFiles: ['test.dwo']};
}
async getScopeInfo(type: string) {
return {type, typeName: type};
}
async listVariablesInScope(_rawLocation: Chrome.DevTools.RawLocation) {
return [];
}
}
RegisterExtension(new MissingInfoPlugin(), 'MissingInfo', {language: 'WebAssembly', symbol_types: ['None']});
});
await openSourcesPanel();
await click(PAUSE_ON_UNCAUGHT_EXCEPTION_SELECTOR);
await goToResource('sources/wasm/unreachable.html');
await waitFor(RESUME_BUTTON);
const incompleteMessage = 'The debug information for function $Main is incomplete';
const infoBar = await waitFor(`.infobar-error[aria-label="${incompleteMessage}"`);
const details = await waitFor('.infobar-details-rows', infoBar);
const text = await details.evaluate(e => e.textContent);
assert.deepEqual(text, 'Failed to load debug file "test.dwo".');
const banners = await $$('.call-frame-warnings-message');
const bannerTexts = await Promise.all(banners.map(e => e.evaluate(e => e.textContent)));
assert.include(bannerTexts, 'Some call frames have warnings');
const selectedCallFrame = await waitFor('.call-frame-item[aria-selected="true"]');
const warning = await waitFor('.call-frame-warning-icon', selectedCallFrame);
const title = await warning.evaluate(e => e.getAttribute('title'));
assert.deepEqual(title, `${incompleteMessage}\n${text}`);
});
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_UNCAUGHT_EXCEPTION_SELECTOR);
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 values with the evaluate API', async () => {
const extension = await loadExtension(
'TestExtension', `${getResourcesPathWithDevToolsHostname()}/extensions/language_extensions.html`);
await extension.evaluate(() => {
class EvalPlugin {
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: undefined};
return {typeInfos, base};
}
return null;
}
async evaluate(expression: string, _context: Chrome.DevTools.RawLocation, _stopId: unknown):
Promise<Chrome.DevTools.RemoteObject|null> {
if (expression !== 'local') {
return null;
}
return {
type: 'object',
description: 'TestType',
objectId: 'TestType',
hasChildren: true,
};
}
async getProperties(objectId: string): Promise<Chrome.DevTools.PropertyDescriptor[]> {
if (objectId === 'TestType') {
return [{
name: 'member',
value: {
type: 'object',
description: 'TestTypeMember',
objectId: 'TestTypeMember',
hasChildren: true,
},
}];
}
if (objectId === 'TestTypeMember') {
return [{
name: 'member2',
value: {
type: 'object',
description: 'TestTypeMember2',
objectId: 'TestTypeMember2',
hasChildren: true,
},
}];
}
if (objectId === 'TestTypeMember2') {
return [
{
name: 'recurse',
value: {
type: 'number',
description: '27',
value: 27,
hasChildren: false,
},
},
{
name: 'value',
value: {
type: 'number',
description: '26',
value: 26,
hasChildren: false,
},
},
];
}
return [];
}
async releaseObject(objectId: string): Promise<void> {
if (objectId !== 'TestType' && objectId !== 'TestTypeMember' && objectId !== 'TestTypeMember2') {
throw new Error(`Invalid object id ${objectId}`);
}
}
}
RegisterExtension(new EvalPlugin(), 'Evaluation', {language: 'WebAssembly', symbol_types: ['None']});
});
await openSourcesPanel();
await click(PAUSE_ON_UNCAUGHT_EXCEPTION_SELECTOR);
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',
]);
});
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_UNCAUGHT_EXCEPTION_SELECTOR);
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_UNCAUGHT_EXCEPTION_SELECTOR);
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']);
});
it('can access wasm data directly', async () => {
const {target} = getBrowserAndPages();
const extension = await loadExtension(
'TestExtension', `${getResourcesPathWithDevToolsHostname()}/extensions/language_extensions.html`);
await extension.evaluate(() => {
class WasmDataExtension {
constructor() {
}
async addRawModule(rawModuleId: string, symbols: string, rawModule: Chrome.DevTools.RawModule) {
const sourceFileURL = new URL('can_access_wasm_data.wat', rawModule.url || symbols).href;
return [sourceFileURL];
}
}
RegisterExtension(new WasmDataExtension(), 'Wasm Data', {language: 'WebAssembly', symbol_types: ['None']});
});
await goToResource('extensions/wasm_module.html?module=can_access_wasm_data.wasm');
await openSourcesPanel();
await target.evaluate(
() => new Uint8Array((window.Module.instance.exports.memory as WebAssembly.Memory).buffer)
.set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 0));
const locationLabels =
WasmLocationLabels.load('extensions/can_access_wasm_data.wat', 'extensions/can_access_wasm_data.wasm');
await locationLabels.setBreakpointInWasmAndRun(
'BREAK(can_access_wasm_data)', 'window.Module.instance.exports.exported_func(4)');
const mem = await extension.evaluate(async(): Promise<number[]> => {
const buffer = await chrome.devtools.languageServices.getWasmLinearMemory(0, 10, 0n);
if (buffer instanceof ArrayBuffer) {
return Array.from(new Uint8Array(buffer));
}
throw new Error('Expected an ArrayBuffer');
});
assert.deepEqual(mem, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
const global = await extension.evaluate(() => chrome.devtools.languageServices.getWasmGlobal(0, 0n));
assert.deepEqual(global, {type: 'i32', value: 0xdad});
const local = await extension.evaluate(() => chrome.devtools.languageServices.getWasmLocal(0, 0n));
assert.deepEqual(local, {type: 'i32', value: 4});
const local2 = await extension.evaluate(() => chrome.devtools.languageServices.getWasmLocal(1, 0n));
assert.deepEqual(local2, {type: 'i32', value: 0});
await locationLabels.continueAndCheckForLabel('BREAK(can_access_wasm_data)');
const expectedError = expectError('Extension server error: Invalid argument stopId: Unknown stop id');
// The stop id is invalid now:
const fail = await extension.evaluate(() => chrome.devtools.languageServices.getWasmLocal(1, 0n));
// FIXME is this the error reporting experience we want?
assert.deepEqual(fail as unknown, {
code: 'E_BADARG',
description: 'Invalid argument %s: %s',
details: [
'stopId',
'Unknown stop id',
],
isError: true,
});
assertNotNullOrUndefined(expectedError.caught);
const local2Set = await extension.evaluate(() => chrome.devtools.languageServices.getWasmLocal(1, 1n));
assert.deepEqual(local2Set, {type: 'i32', value: 4});
});
it('lets users manually attach debug info', async () => {
const {target} = 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 DWARFSymbolsWithSingleFilePlugin {
async addRawModule(rawModuleId: string, symbols: string, rawModule: Chrome.DevTools.RawModule) {
if (symbols !== 'foobar81') {
return [];
}
const fileUrl = new URL('/source_file.c', rawModule.url || symbols);
return [fileUrl.href];
}
}
RegisterExtension(
new DWARFSymbolsWithSingleFilePlugin(), 'Single File',
{language: 'WebAssembly', symbol_types: ['ExternalDWARF']});
});
await goToResource(
'extensions/wasm_module.html?module=/test/e2e/resources/extensions/global_variable.wasm&defer=1');
await openSourcesPanel();
{
const capturedFileNames = await captureAddedSourceFiles(1, () => target.evaluate('loadModule();'));
assert.deepEqual(capturedFileNames, ['/test/e2e/resources/extensions/global_variable.wasm']);
}
{
const capturedFileNames = await captureAddedSourceFiles(1, async () => {
await openFileInEditor('global_variable.wasm');
const editor = await waitForAria('Code editor');
await click(editor, {clickOptions: {button: 'right'}});
const menuItem = await waitForAria('Add DWARF debug info…');
await click(menuItem);
await waitFor('.add-source-map');
await typeText('foobar81');
await pressKey('Enter');
});
assert.deepEqual(capturedFileNames, ['/source_file.c']);
}
});
});