blob: dd48b3aa6fab8806f18e2376d14f704356b2c32f [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';
import {expectError} from '../../conductor/events.js';
import {
$,
$$,
assertNotNullOrUndefined,
click,
getAllTextContents,
getBrowserAndPages,
getDevToolsFrontendHostname,
getResourcesPath,
getTestServerPort,
getTextContent,
goToResource,
installEventListener,
pasteText,
pressKey,
typeText,
waitFor,
waitForFunction,
waitForMany,
waitForNone,
} from '../../shared/helper.js';
import {
CONSOLE_TAB_SELECTOR,
focusConsolePrompt,
getCurrentConsoleMessages,
getStructuredConsoleMessages,
} from '../helpers/console-helpers.js';
import {checkIfTabExistsInDrawer} from '../helpers/cross-tool-helper.js';
import {getResourcesPathWithDevToolsHostname, loadExtension} from '../helpers/extension-helpers.js';
import {
captureAddedSourceFiles,
DEBUGGER_PAUSED_EVENT,
getCallFrameLocations,
getCallFrameNames,
getNonBreakableLines,
getValuesForScope,
type LabelMapping,
openFileInEditor,
openSourcesPanel,
PAUSE_ON_UNCAUGHT_EXCEPTION_SELECTOR,
RESUME_BUTTON,
retrieveTopCallFrameWithoutResuming,
stepOver,
switchToCallFrame,
WasmLocationLabels,
} from '../helpers/sources-helpers.js';
const DEVELOPER_RESOURCES_TAB_SELECTOR = '#tab-developer-resources';
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;
function goToWasmResource(
moduleName: string, options: {autoLoadModule?: boolean, runFunctionAfterLoad?: string} = {}): Promise<void> {
const queryParams = [`module=${moduleName}`];
if (!options.autoLoadModule) {
queryParams.push('defer=1');
}
if (options.runFunctionAfterLoad) {
queryParams.push(`autorun=${options.runFunctionAfterLoad}`);
}
return goToResource(`extensions/wasm_module.html?${queryParams.join('&')}`);
}
// We need a dummy external DWARF file such that DevTools uses the mock extensions
// for debugging the WebAssembly.
async function addDummyExternalDWARFInfo(wasmFile: string) {
await openFileInEditor(wasmFile);
await click('aria/Code editor', {clickOptions: {button: 'right'}});
await click('aria/Add DWARF debug info…');
await waitFor('.add-source-map');
await typeText('dummy-external-file');
await pressKey('Enter');
}
// 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', () => {
// 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`,
/* allowFileAccess*/ true);
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: ['ExternalDWARF']});
});
await goToWasmResource('/test/e2e/resources/extensions/global_variable.wasm');
await openSourcesPanel();
const capturedFileNames = captureAddedSourceFiles(2, async () => {
await target.evaluate('loadModule();');
});
await addDummyExternalDWARFInfo('global_variable.wasm');
assert.deepEqual(await 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`,
/* allowFileAccess */ true);
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: ['ExternalDWARF']});
}, locationLabels.getMappingsForPlugin());
await openSourcesPanel();
await click(PAUSE_ON_UNCAUGHT_EXCEPTION_SELECTOR);
await goToWasmResource('unreachable.wasm', {runFunctionAfterLoad: 'Main', autoLoadModule: true});
await addDummyExternalDWARFInfo('unreachable.wasm');
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`,
/* allowFileAccess */ true);
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: ['ExternalDWARF']});
}, locationLabels.getMappingsForPlugin());
await goToWasmResource('/test/e2e/resources/extensions/global_variable.wasm', {autoLoadModule: true});
await openSourcesPanel();
await addDummyExternalDWARFInfo('global_variable.wasm');
await openFileInEditor('global_variable.wat');
const toolbarLink = await waitFor('devtools-toolbar .devtools-link');
const toolbarLinkText = await toolbarLink.evaluate(({textContent}) => textContent);
assert.strictEqual(toolbarLinkText, 'global_variable.wasm');
assert.isNotEmpty(await getNonBreakableLines());
await locationLabels.setBreakpointInSourceAndRun('BREAK(return)', 'Module.instance.exports.Main();');
});
it('shows top-level and nested variables', async () => {
const extension = await loadExtension(
'TestExtension', `${getResourcesPathWithDevToolsHostname()}/extensions/language_extensions.html`,
/* allowFileAccess */ true);
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: ['ExternalDWARF']});
});
await openSourcesPanel();
await click(PAUSE_ON_UNCAUGHT_EXCEPTION_SELECTOR);
await goToResource('sources/wasm/unreachable.html');
await addDummyExternalDWARFInfo('unreachable.wasm');
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`,
/* allowFileAccess */ true);
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: ['ExternalDWARF']});
});
await openSourcesPanel();
await click(PAUSE_ON_UNCAUGHT_EXCEPTION_SELECTOR);
await goToResource('sources/wasm/unreachable.html');
await waitFor(RESUME_BUTTON);
await addDummyExternalDWARFInfo('unreachable.wasm');
// Call stack shows inline function names and source locations.
let funcNames: string[] = [];
await waitForFunction(async () => {
funcNames = await getCallFrameNames();
return funcNames.length === 6;
});
assert.deepEqual(funcNames, ['inner_inline_func', 'outer_inline_func', 'Main', 'go', 'await in go', '(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`,
/* allowFileAccess */ true);
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: ['ExternalDWARF']});
});
await openSourcesPanel();
await click(PAUSE_ON_UNCAUGHT_EXCEPTION_SELECTOR);
await goToResource('sources/wasm/unreachable.html');
await addDummyExternalDWARFInfo('unreachable.wasm');
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', '(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`,
/* allowFileAccess */ true);
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: ['ExternalDWARF']});
});
await openSourcesPanel();
await click(PAUSE_ON_UNCAUGHT_EXCEPTION_SELECTOR);
await goToResource('sources/wasm/unreachable.html');
await waitFor(RESUME_BUTTON);
await addDummyExternalDWARFInfo('unreachable.wasm');
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`,
/* allowFileAccess */ true);
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: ['ExternalDWARF']});
});
await openSourcesPanel();
await click(PAUSE_ON_UNCAUGHT_EXCEPTION_SELECTOR);
await goToResource('sources/wasm/unreachable.html');
await addDummyExternalDWARFInfo('unreachable.wasm');
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('connects warnings to the developer resource panel', async () => {
const extension = await loadExtension(
'TestExtension', `${getResourcesPathWithDevToolsHostname()}/extensions/language_extensions.html`,
/* allowFileAccess */ true);
await extension.evaluate(() => {
class MissingInfoPlugin {
async addRawModule() {
await chrome.devtools.languageServices.reportResourceLoad(
'http://test.com/test.dwo', {success: false, errorMessage: '404'});
return [];
}
async getFunctionInfo() {
return {missingSymbolFiles: ['http://test.com/test.dwo']};
}
}
RegisterExtension(
new MissingInfoPlugin(), 'MissingInfo', {language: 'WebAssembly', symbol_types: ['ExternalDWARF']});
});
await openSourcesPanel();
await click(PAUSE_ON_UNCAUGHT_EXCEPTION_SELECTOR);
await goToResource('sources/wasm/unreachable.html');
await addDummyExternalDWARFInfo('unreachable.wasm');
await waitFor(RESUME_BUTTON);
const incompleteMessage = 'The debug information for function $Main is incomplete';
const infoBar = await waitFor(`.infobar-error[aria-label="${incompleteMessage}"`);
assert.deepEqual(await getTextContent('devtools-button', infoBar), 'Show more');
await click('devtools-button', {root: infoBar});
const detailsRowMessage = await waitFor('.infobar-row-message');
assert.deepEqual(await getTextContent('devtools-button', detailsRowMessage), 'Show request');
await click('devtools-button', {root: detailsRowMessage});
await checkIfTabExistsInDrawer(DEVELOPER_RESOURCES_TAB_SELECTOR);
const resourcesGrid = await waitFor('.developer-resource-view-results');
const selectedReportedResource = await waitFor('.data-grid-data-grid-node.selected', resourcesGrid);
const selectedDetails = await getAllTextContents('td', selectedReportedResource);
const initiatorUrl = `https://${getDevToolsFrontendHostname()}:${getTestServerPort()}`;
const dwoUrl = 'http://test.com/test.dwo';
assert.deepEqual(selectedDetails, [
'failure',
dwoUrl,
initiatorUrl,
'',
'404',
'',
]);
});
it('shows variable values with the evaluate API', async () => {
const extension = await loadExtension(
'TestExtension', `${getResourcesPathWithDevToolsHostname()}/extensions/language_extensions.html`,
/* allowFileAccess */ true);
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 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: ['ExternalDWARF']});
});
await openSourcesPanel();
await click(PAUSE_ON_UNCAUGHT_EXCEPTION_SELECTOR);
await goToResource('sources/wasm/unreachable.html');
await addDummyExternalDWARFInfo('unreachable.wasm');
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`,
/* allowFileAccess */ true);
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'}];
}
evaluate(expression: string, _context: Chrome.DevTools.RawLocation, _stopId: unknown):
Promise<Chrome.DevTools.RemoteObject|null> {
if (expression === 'unreachable') {
return Promise.resolve({type: 'number', value: 23, description: '23', hasChildren: false});
}
return Promise.resolve(null);
}
}
RegisterExtension(
new VariableListingPlugin(), 'Location Mapping', {language: 'WebAssembly', symbol_types: ['ExternalDWARF']});
});
await openSourcesPanel();
await click(PAUSE_ON_UNCAUGHT_EXCEPTION_SELECTOR);
await goToResource('sources/wasm/unreachable.html');
await addDummyExternalDWARFInfo('unreachable.wasm');
await waitFor(RESUME_BUTTON);
await openFileInEditor('unreachable.ll');
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`,
/* allowFileAccess */ true);
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) {
console.error('foobar');
return [];
}
return [{scope: 'LOCAL', name: 'unreachable', type: 'int'}];
}
async evaluate(expression: string, _context: Chrome.DevTools.RawLocation, _stopId: unknown):
Promise<Chrome.DevTools.RemoteObject|null> {
if (expression === 'foo') {
return {type: 'number', value: 23, description: '23', hasChildren: false};
}
throw new Error(`No typeinfo for ${expression}`);
}
}
RegisterExtension(
new FormattingErrorsPlugin(), 'Formatter Errors', {language: 'WebAssembly', symbol_types: ['ExternalDWARF']});
});
await openSourcesPanel();
await click(PAUSE_ON_UNCAUGHT_EXCEPTION_SELECTOR);
await goToResource('sources/wasm/unreachable.html');
await addDummyExternalDWARFInfo('unreachable.wasm');
await waitFor(RESUME_BUTTON);
const locals = await getValuesForScope('LOCAL', 0, 1);
assert.deepEqual(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 waitForFunction(async () => {
const texts = await Promise.all(watchResults.map(async watch => await watch.evaluate(e => e.textContent)));
return texts.every(t => t?.length) ? texts : null;
});
assert.deepEqual(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.deepEqual(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`,
/* allowFileAccess */ true);
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: ['ExternalDWARF']});
});
await goToWasmResource('can_access_wasm_data.wasm', {autoLoadModule: true});
await openSourcesPanel();
await addDummyExternalDWARFInfo('can_access_wasm_data.wasm');
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 () => {
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);
// TODO(crbug.com/1472241): Find a way to stop the flake by determining the stopid
// 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`,
/* allowFileAccess */ true);
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 goToWasmResource('/test/e2e/resources/extensions/global_variable.wasm');
await openSourcesPanel();
{
const capturedFileNames = await captureAddedSourceFiles(1, async () => {
await target.evaluate('loadModule();');
});
assert.deepEqual(capturedFileNames, ['/test/e2e/resources/extensions/global_variable.wasm']);
}
{
const capturedFileNames = await captureAddedSourceFiles(1, async () => {
await openFileInEditor('global_variable.wasm');
await click('aria/Code editor', {clickOptions: {button: 'right'}});
await click('aria/Add DWARF debug info…');
await waitFor('.add-source-map');
await typeText('foobar81');
await pressKey('Enter');
});
assert.deepEqual(capturedFileNames, ['/source_file.c']);
}
});
it('does not auto-step for modules without a plugin', async () => {
const {frontend} = getBrowserAndPages();
const locationLabels = WasmLocationLabels.load('extensions/stepping.wat', 'extensions/stepping.wasm');
await goToWasmResource('stepping.wasm', {autoLoadModule: true});
await openSourcesPanel();
installEventListener(frontend, DEBUGGER_PAUSED_EVENT);
await locationLabels.setBreakpointInWasmAndRun('FIRST_PAUSE', 'window.Module.instance.exports.Main(16)');
await waitFor('.paused-status');
await locationLabels.checkLocationForLabel('FIRST_PAUSE');
const beforeStepCallFrame = (await retrieveTopCallFrameWithoutResuming())!.split(':');
const beforeStepFunctionNames = await getCallFrameNames();
await stepOver();
const afterStepCallFrame = await waitForFunction(async () => {
const callFrame = (await retrieveTopCallFrameWithoutResuming())?.split(':');
if (callFrame && (callFrame[0] !== beforeStepCallFrame[0] || callFrame[1] !== beforeStepCallFrame[1])) {
return callFrame;
}
return undefined;
});
const afterStepFunctionNames = await getCallFrameNames();
// still in the same function:
assert.deepEqual(beforeStepFunctionNames, afterStepFunctionNames);
// still in the same module:
assert.deepEqual(beforeStepCallFrame[0], afterStepCallFrame[0]);
// moved one instruction:
assert.deepEqual(parseInt(beforeStepCallFrame[1], 16) + 2, parseInt(afterStepCallFrame[1], 16));
});
it('auto-steps over unmapped code correctly', async () => {
const {frontend} = getBrowserAndPages();
const extension = await loadExtension(
'TestExtension', `${getResourcesPathWithDevToolsHostname()}/extensions/language_extensions.html`,
/* allowFileAccess */ true);
const locationLabels = WasmLocationLabels.load('extensions/stepping.wat', 'extensions/stepping.wasm');
await goToWasmResource('stepping.wasm', {autoLoadModule: true});
await openSourcesPanel();
await addDummyExternalDWARFInfo('stepping.wasm');
// Do this after setting the breakpoint, otherwise the helper gets confused
await locationLabels.setBreakpointInWasmAndRun('FIRST_PAUSE', 'window.Module.instance.exports.Main(16)');
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('stepping.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 && m.label !== 'THIRD_PAUSE');
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: ['ExternalDWARF']});
}, locationLabels.getMappingsForPlugin());
await waitFor('.paused-status');
await locationLabels.checkLocationForLabel('FIRST_PAUSE');
installEventListener(frontend, DEBUGGER_PAUSED_EVENT);
await stepOver();
await locationLabels.checkLocationForLabel('SECOND_PAUSE');
await stepOver();
const pausedLocation = await locationLabels.checkLocationForLabel('THIRD_PAUSE');
// We're paused at the right location, but let's also check that we're paused in wasm, not the source code:
const pausedFrame = await retrieveTopCallFrameWithoutResuming();
assert.deepEqual(pausedFrame, `stepping.wasm:0x${pausedLocation.moduleOffset.toString(16)}`);
});
});