blob: ec6581e692873fe37e1c97ee0416c636f69843a8 [file]
// Copyright 2023 The Chromium Authors
// 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 sinon from 'sinon';
import {WorkerPlugin} from '../src/DevToolsPluginHost.js';
import {createPlugin} from '../src/DWARFSymbols.js';
import {ResourceLoader} from '../src/MEMFSResourceLoader.js';
import {DEFAULT_MODULE_CONFIGURATIONS} from '../src/ModuleConfiguration.js';
import type {WasmValue} from '../src/WasmTypes.js';
import {type AsyncHostInterface, WorkerRPC} from '../src/WorkerRPC.js';
import type {TestWorkerInterface} from './DevToolsPluginTestWorker.js';
import {makeURL, TestHostInterface} from './TestUtils.js';
describe('DevToolsPlugin', () => {
describe('addRawModule', () => {
const expectedSources = [
makeURL('/build/tests/inputs/hello.c'),
makeURL('/build/tests/inputs/printf.h'),
];
it('does not race with removeRawModule', async () => {
const plugin = await createPlugin(
new TestHostInterface(),
new ResourceLoader(),
);
const sources1Promise = plugin.addRawModule('0', '', {
url: makeURL('/build/tests/inputs/hello.s.wasm'),
});
const sources1 = await sources1Promise;
assert.deepEqual(sources1, expectedSources);
const remove1Promise = plugin.removeRawModule('0');
const sources2Promise = plugin.addRawModule('0', '', {
url: makeURL('/build/tests/inputs/hello.s.wasm'),
});
const [, sources2] = await Promise.all([remove1Promise, sources2Promise]);
assert.deepEqual(sources2, expectedSources);
});
it('does not try to create module file names that contain /', async () => {
const plugin = await createPlugin(
new TestHostInterface(),
new ResourceLoader(),
);
const sources = await plugin.addRawModule('?ΓΌ', '', {
url: makeURL('/build/tests/inputs/hello.s.wasm'),
});
assert.deepEqual(sources, expectedSources);
});
it('reports missing module', async () => {
const hostInterface = new TestHostInterface();
const spy = sinon.spy(hostInterface, 'reportResourceLoad');
const url = makeURL('/build/tests/inputs/notExistent.s.wasm');
const plugin = await createPlugin(hostInterface, new ResourceLoader());
try {
await plugin.addRawModule('0', '', {url});
} catch {
}
sinon.assert.calledOnceWithExactly(spy, url, {
success: false,
errorMessage: `NotFoundError: Unable to load debug symbols from \'${url}\' (Not Found)`,
});
});
it('reports loaded module and potentially missing dwp', async () => {
const hostInterface = new TestHostInterface();
const spy = sinon.spy(hostInterface, 'reportResourceLoad');
const url = makeURL('/build/tests/inputs/hello.s.wasm');
const plugin = await createPlugin(hostInterface, new ResourceLoader());
await plugin.addRawModule('0', '', {url});
const dwpUrl = makeURL('/build/tests/inputs/hello.s.wasm.dwp');
sinon.assert.calledTwice(spy);
sinon.assert.calledWith(spy, url, {success: true, size: 401});
sinon.assert.calledWith(spy, dwpUrl, {
success: false,
errorMessage: 'Failed to fetch dwp file: Not Found',
});
});
it('reports loaded dwos', async () => {
const url = makeURL('/build/tests/inputs/hello-split.wasm');
const defaultConfig = {
moduleConfigurations: DEFAULT_MODULE_CONFIGURATIONS,
logPluginApiCalls: false,
};
const plugin = await WorkerPlugin.create(
defaultConfig.moduleConfigurations,
defaultConfig.logPluginApiCalls,
);
const spy = sinon.spy(plugin, 'reportResourceLoad');
const rawModuleId = 'hello-split.wasm@123456';
const helloFileURL = makeURL('/build/tests/inputs/hello-split.c');
const helperFileURL = makeURL('/build/tests/inputs/helper.c');
const sources = await plugin.addRawModule(rawModuleId, '', {url});
assert.deepEqual(sources, [helloFileURL, helperFileURL]);
// Request raw location to trigger access to DWO files.
await plugin.rawLocationToSourceLocation({
rawModuleId,
codeOffset: 0x9,
inlineFrameIndex: 0,
});
await plugin.rawLocationToSourceLocation({
rawModuleId,
codeOffset: 0x5,
inlineFrameIndex: 0,
});
const helloDwoURL = makeURL('/build/tests/inputs/hello-split.dwo');
const helperDwoURL = makeURL('/build/tests/inputs/helper.dwo');
// Loaded .wasm, missing .dwp, 2x missing .dwo.
assert.lengthOf(spy.args, 4);
sinon.assert.calledWith(spy, helloDwoURL, {success: true, size: 217});
sinon.assert.calledWith(spy, helperDwoURL, {success: true, size: 207});
});
it('reports missing dwos', async () => {
const url = makeURL('/build/tests/inputs/hello-split-missing-dwo.wasm');
const defaultConfig = {
moduleConfigurations: DEFAULT_MODULE_CONFIGURATIONS,
logPluginApiCalls: false,
};
const plugin = await WorkerPlugin.create(
defaultConfig.moduleConfigurations,
defaultConfig.logPluginApiCalls,
);
const spy = sinon.spy(plugin, 'reportResourceLoad');
const rawModuleId = 'hello-split-missing-dwo.wasm@123456';
const helloFileURL = makeURL(
'/build/tests/inputs/hello-split-missing-dwo.c',
);
const sources = await plugin.addRawModule(rawModuleId, '', {url});
assert.deepEqual(sources, [helloFileURL]);
// Request raw location to trigger access to DWO files.
await plugin.rawLocationToSourceLocation({
rawModuleId,
codeOffset: 0x5,
inlineFrameIndex: 0,
});
const helloDwoURL = makeURL(
'/build/tests/inputs/hello-split-missing-dwo.dwo',
);
// Loaded .wasm, missing .dwp, missing .dwo is reported twice, since we try to load
// the .dwo twice.
assert.lengthOf(spy.args, 4);
sinon.assert.calledWith(spy, helloDwoURL, {
success: false,
errorMessage: `Couldn't load ${helloDwoURL}. Status: 404`,
});
});
});
describe('rawLocationToSourceLocation', () => {
it('maps bytecode addresses correctly', async () => {
const plugin = await createPlugin(
new TestHostInterface(),
new ResourceLoader(),
);
const rawModuleId = 'hello.wasm@123456';
const sourceFileURL = makeURL('/build/tests/inputs/hello.c');
const header = makeURL('/build/tests/inputs/printf.h');
const sources = await plugin.addRawModule(rawModuleId, '', {
url: makeURL('/build/tests/inputs/hello.s.wasm'),
});
assert.deepEqual(sources, [sourceFileURL, header]);
assert.deepEqual(
await plugin.rawLocationToSourceLocation({
rawModuleId,
codeOffset: 0x02,
inlineFrameIndex: 0,
}),
[{sourceFileURL, rawModuleId, lineNumber: 2, columnNumber: -1}],
);
assert.deepEqual(
await plugin.rawLocationToSourceLocation({
rawModuleId,
codeOffset: 0x5,
inlineFrameIndex: 0,
}),
[
{
sourceFileURL: header,
rawModuleId,
lineNumber: 0,
columnNumber: -1,
},
],
);
});
});
describe('sourceLocationToRawLocation', () => {
it('maps source locations to ranges correctly in the presence of inlining', async () => {
const plugin = await createPlugin(
new TestHostInterface(),
new ResourceLoader(),
);
const rawModuleId = 'inline.s.wasm@123456';
const sourceFileURL = makeURL('/build/tests/inputs/inline.c');
const sources = await plugin.addRawModule(rawModuleId, '', {
url: makeURL('/build/tests/inputs/inline.s.wasm'),
});
assert.deepEqual(sources, [sourceFileURL]);
assert.deepEqual(
await plugin.sourceLocationToRawLocation({
rawModuleId,
sourceFileURL,
lineNumber: 0,
columnNumber: -1,
}),
[{rawModuleId, startOffset: 0x2, endOffset: 0x5}],
);
assert.deepEqual(
await plugin.sourceLocationToRawLocation({
rawModuleId,
sourceFileURL,
lineNumber: 9,
columnNumber: -1,
}),
[{rawModuleId, startOffset: 0x5, endOffset: 0x6}],
);
});
it('returns only raw locations for the same line', async () => {
const plugin = await createPlugin(
new TestHostInterface(),
new ResourceLoader(),
);
const rawModuleId = 'hello.wasm@123456';
const sourceFileURL = makeURL('/build/tests/inputs/hello.c');
const header = makeURL('/build/tests/inputs/printf.h');
const sources = await plugin.addRawModule(rawModuleId, '', {
url: makeURL('/build/tests/inputs/hello.s.wasm'),
});
assert.deepEqual(sources, [sourceFileURL, header]);
assert.deepEqual(
await plugin.sourceLocationToRawLocation({
rawModuleId,
sourceFileURL,
lineNumber: 5,
columnNumber: -1,
}),
[],
);
assert.deepEqual(
await plugin.sourceLocationToRawLocation({
rawModuleId,
sourceFileURL,
lineNumber: 10,
columnNumber: -1,
}),
[],
);
assert.deepEqual(
await plugin.sourceLocationToRawLocation({
rawModuleId,
sourceFileURL,
lineNumber: 16,
columnNumber: -1,
}),
[],
);
});
});
describe('getScopeInfo', () => {
it('handles globals, locals, and parameters', async () => {
const plugin = await createPlugin(
new TestHostInterface(),
new ResourceLoader(),
);
assert.include(await plugin.getScopeInfo('GLOBAL'), {
type: 'GLOBAL',
typeName: 'Global',
});
assert.include(await plugin.getScopeInfo('LOCAL'), {
type: 'LOCAL',
typeName: 'Local',
});
assert.include(await plugin.getScopeInfo('PARAMETER'), {
type: 'PARAMETER',
typeName: 'Parameter',
});
});
});
describe('getInlinedCalleeRanges', () => {
it('gets inlined callee PC ranges correctly', async () => {
const plugin = await createPlugin(
new TestHostInterface(),
new ResourceLoader(),
);
const sources = await plugin.addRawModule('0', '', {
url: makeURL('/build/tests/inputs/inline.s.wasm'),
});
assert.deepEqual(sources, [makeURL('/build/tests/inputs/inline.c')]);
const ranges = await plugin.getInlinedCalleesRanges({
rawModuleId: '0',
codeOffset: 0x2,
inlineFrameIndex: 0,
});
assert.deepEqual(ranges, [
{rawModuleId: '0', startOffset: 0x5, endOffset: 0x6},
]);
});
});
describe('getInlinedFunctionRanges', () => {
it('gets inlined function PC ranges correctly', async () => {
const plugin = await createPlugin(
new TestHostInterface(),
new ResourceLoader(),
);
const sources = await plugin.addRawModule('0', '', {
url: makeURL('/build/tests/inputs/inline.s.wasm'),
});
assert.deepEqual(sources, [makeURL('/build/tests/inputs/inline.c')]);
const ranges = await plugin.getInlinedFunctionRanges({
rawModuleId: '0',
codeOffset: 0x5,
inlineFrameIndex: 0,
});
assert.deepEqual(ranges, [
{rawModuleId: '0', startOffset: 0x5, endOffset: 0x6},
]);
});
});
describe('getFunctionInfo', () => {
it('gets inlined function infos correctly', async () => {
const plugin = await createPlugin(
new TestHostInterface(),
new ResourceLoader(),
);
const sources = await plugin.addRawModule('0', '', {
url: makeURL('/build/tests/inputs/inline.s.wasm'),
});
assert.deepEqual(sources, [makeURL('/build/tests/inputs/inline.c')]);
const functions = await plugin.getFunctionInfo({
rawModuleId: '0',
codeOffset: 0x5,
inlineFrameIndex: 0,
});
assert.deepEqual(functions, {
frames: [{name: 'callee'}, {name: 'caller'}],
missingSymbolFiles: [],
});
});
});
describe('listVariablesInScope', () => {
it('lists parameter variables correctly', async () => {
const plugin = await createPlugin(
new TestHostInterface(),
new ResourceLoader(),
);
await plugin.addRawModule('0', '', {
url: makeURL('/build/tests/inputs/shadowing.s.wasm'),
});
{
const variables = await plugin.listVariablesInScope({
rawModuleId: '0',
codeOffset: 0x2,
inlineFrameIndex: 0,
});
assert.deepEqual(
variables.map(v => v.scope),
['PARAMETER'],
);
}
{
const variables = await plugin.listVariablesInScope({
rawModuleId: '0',
codeOffset: 0x3,
inlineFrameIndex: 0,
});
assert.deepEqual(
variables.map(v => v.scope),
['LOCAL'],
);
}
});
it('lists global variables correctly', async () => {
const plugin = await createPlugin(
new TestHostInterface(),
new ResourceLoader(),
);
await plugin.addRawModule('0', '', {
url: makeURL('/build/tests/inputs/globals.s.wasm'),
});
const variables = await plugin.listVariablesInScope({
rawModuleId: '0',
codeOffset: 0x6,
inlineFrameIndex: 0,
});
assert.deepEqual(
variables.map(v => v.name),
['::var_separate_cu'],
);
});
});
describe('getMappedLines', () => {
it('computes mapped lines correctly.', async () => {
const plugin = await createPlugin(
new TestHostInterface(),
new ResourceLoader(),
);
await plugin.addRawModule('hello', '', {
url: makeURL('/build/tests/inputs/hello.s.wasm'),
});
const mappedLines = await plugin.getMappedLines(
'hello',
makeURL('/build/tests/inputs/hello.c'),
);
assert.deepEqual(mappedLines, [2]);
});
});
describe('HostInterface', () => {
it('provides access to wasm state', async () => {
class TestAsyncHostInterface implements AsyncHostInterface {
readonly memory = {
offset: 1026,
length: 5,
stopId: 9,
result: new Uint8Array([5, 6, 7, 8, 9]).buffer,
};
readonly local = {
local: 9,
stopId: 10,
result: {type: 'i32', value: 5} as const,
};
readonly global = {
global: 10,
stopId: 11,
result: {type: 'i32', value: 6} as const,
};
readonly op = {
op: 11,
stopId: 12,
result: {type: 'i32', value: 7} as const,
};
async getWasmLinearMemory(
offset: number,
length: number,
stopId: unknown,
): Promise<ArrayBuffer> {
if (offset === this.memory.offset && length === this.memory.length && stopId === this.memory.stopId) {
return this.memory.result;
}
throw new Error('Unexpected arguments to call');
}
async getWasmLocal(local: number, stopId: unknown): Promise<WasmValue> {
if (local === this.local.local && stopId === this.local.stopId) {
return this.local.result;
}
throw new Error('Unexpected arguments to call');
}
async getWasmGlobal(
global: number,
stopId: unknown,
): Promise<WasmValue> {
if (global === this.global.global && stopId === this.global.stopId) {
return this.global.result;
}
throw new Error('Unexpected arguments to call');
}
async getWasmOp(op: number, stopId: unknown): Promise<WasmValue> {
if (op === this.op.op && stopId === this.op.stopId) {
return this.op.result;
}
throw new Error('Unexpected arguments to call');
}
reportResourceLoad(
_resourceUrl: string,
_status: {success: boolean, errorMessage?: string, size?: number},
): Promise<void> {
throw new Error('Method not implemented.');
}
}
// To be able to test the synchronous API calls we need a worker. In order to test the wasm state APIs explicitely
// we wrap real RPCInterface that's running on the plugin worker in a test-specific interface that just
// round-trips the wasm state access calls to here:
const hostInterface = new TestAsyncHostInterface();
const worker = new Worker('/build/tests/DevToolsPluginTestWorker.js', {
type: 'module',
});
const rpc = new WorkerRPC<AsyncHostInterface, TestWorkerInterface>(
worker,
hostInterface,
);
{
const {offset, length, stopId, result} = hostInterface.memory;
const callResult = await rpc.sendMessage(
'getWasmMemoryForTest',
offset,
length,
stopId,
);
assert.deepEqual(callResult, result);
}
{
const {local, stopId, result} = hostInterface.local;
const callResult = await rpc.sendMessage(
'getWasmLocalForTest',
local,
stopId,
);
assert.deepEqual(callResult, result);
}
{
const {global, stopId, result} = hostInterface.global;
const callResult = await rpc.sendMessage(
'getWasmGlobalForTest',
global,
stopId,
);
assert.deepEqual(callResult, result);
}
{
const {op, stopId, result} = hostInterface.op;
const callResult = await rpc.sendMessage(
'getWasmOpForTest',
op,
stopId,
);
assert.deepEqual(callResult, result);
}
});
});
it('provides a method to report resource loads', async () => {
class TestAsyncHostInterface implements AsyncHostInterface {
async getWasmLinearMemory(
_offset: number,
_length: number,
_stopId: unknown,
): Promise<ArrayBuffer> {
throw new Error('Method not implemented.');
}
async getWasmLocal(_local: number, _stopId: unknown): Promise<WasmValue> {
throw new Error('Method not implemented.');
}
async getWasmGlobal(
_global: number,
_stopId: unknown,
): Promise<WasmValue> {
throw new Error('Method not implemented.');
}
async getWasmOp(_op: number, _stopId: unknown): Promise<WasmValue> {
throw new Error('Method not implemented.');
}
reportResourceLoad(
_resourceUrl: string,
_status: {success: boolean, errorMessage?: string, size?: number},
): Promise<void> {
return Promise.resolve();
}
}
const hostInterface = new TestAsyncHostInterface();
const worker = new Worker('/build/tests/DevToolsPluginTestWorker.js', {
type: 'module',
});
const rpc = new WorkerRPC<AsyncHostInterface, TestWorkerInterface>(
worker,
hostInterface,
);
const resourceUrl = 'test.dwo';
const status = {success: true};
const spy = sinon.spy(hostInterface, 'reportResourceLoad');
await rpc.sendMessage('reportResourceLoadForTest', resourceUrl, status);
sinon.assert.calledOnceWithExactly(spy, resourceUrl, status);
});
});