wamr-ide: Add vscode extension tests (#2292)

This PR adds tests for #2219 by changing the `compilation_on_android_ubuntu.yml` workflow.
The first run will take about two hours, since LLDB is built from scratch. Later, the build is
cached and the whole job should not take more than three minutes.

Core of the PR is an integration test that boots up vscode and lets it debug a test WASM file.
This commit is contained in:
Ben Riegel 2023-06-20 08:33:01 +01:00 committed by GitHub
parent 85981b77b8
commit 72fc872afe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 511 additions and 22 deletions

View File

@ -21,6 +21,7 @@ on:
- "tests/wamr-test-suites/**"
- "wamr-compiler/**"
- "wamr-sdk/**"
- "test-tools/wamr-ide/**"
# will be triggered on push events
push:
branches:
@ -38,6 +39,7 @@ on:
- "tests/wamr-test-suites/**"
- "wamr-compiler/**"
- "wamr-sdk/**"
- "test-tools/wamr-ide/**"
# allow to be triggered manually
workflow_dispatch:
@ -545,3 +547,117 @@ jobs:
if: env.TEST_ON_X86_32 == 'true'
run: ./test_wamr.sh ${{ env.X86_32_TARGET_TEST_OPTIONS }} ${{ matrix.test_option }} -t ${{ matrix.running_mode }}
working-directory: ./tests/wamr-test-suites
test-wamr-ide:
needs:
[
build_iwasm
]
runs-on: ubuntu-22.04
env:
PYTHON_VERSION: '3.10'
PYTHON_UBUNTU_STANDALONE_BUILD: https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.10.11+20230507-x86_64-unknown-linux-gnu-install_only.tar.gz
steps:
- name: checkout
uses: actions/checkout@v3
- name: install dependencies
run: |
rustup target add wasm32-wasi
sudo apt update && sudo apt-get install -y lld ninja-build
npm install
working-directory: test-tools/wamr-ide/VSCode-Extension
- name: build iwasm with source debugging feature
run: |
mkdir build
cd build
cmake .. -DWAMR_BUILD_DEBUG_INTERP=1
make
working-directory: product-mini/platforms/linux
- name: Cache LLDB
id: cache-lldb
uses: actions/cache@v3
env:
cache-name: cache-lldb-vscode
with:
path: test-tools/wamr-ide/VSCode-Extension/resource/debug/linux
key: ${{ env.cache-name }}-${{ hashFiles('build-scripts/lldb-wasm.patch') }}-${{ env.PYTHON_UBUNTU_STANDALONE_BUILD }}
- if: ${{ steps.cache-lldb.outputs.cache-hit != 'true' }}
name: get stand-alone python ubuntu
run: |
wget ${{ env.PYTHON_UBUNTU_STANDALONE_BUILD }} -O python.tar.gz
tar -xvf python.tar.gz
working-directory: core/deps
- if: ${{ steps.cache-lldb.outputs.cache-hit != 'true' }}
name: download llvm
run: |
wget https://github.com/llvm/llvm-project/archive/1f27fe6128769f00197925c3b8f6abb9d0e5cd2e.zip
unzip -q 1f27fe6128769f00197925c3b8f6abb9d0e5cd2e.zip
mv llvm-project-1f27fe6128769f00197925c3b8f6abb9d0e5cd2e llvm-project
working-directory: core/deps
- if: ${{ steps.cache-lldb.outputs.cache-hit != 'true' }}
name: apply wamr patch
run: |
git init
git config user.email "action@github.com"
git config user.name "github action"
git apply ../../../build-scripts/lldb-wasm.patch
working-directory: core/deps/llvm-project
- if: ${{ steps.cache-lldb.outputs.cache-hit != 'true' }}
name: build lldb ubuntu
run: |
echo "start to build lldb..."
mkdir -p wamr-lldb
cmake -S ./llvm -B build \
-G Ninja \
-DCMAKE_INSTALL_PREFIX=../wamr-lldb \
-DCMAKE_BUILD_TYPE:STRING="Release" \
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
-DLLVM_ENABLE_PROJECTS="clang;lldb" \
-DLLVM_TARGETS_TO_BUILD:STRING="X86;WebAssembly" \
-DLLVM_BUILD_BENCHMARKS:BOOL=OFF \
-DLLVM_BUILD_DOCS:BOOL=OFF \
-DLLVM_BUILD_EXAMPLES:BOOL=OFF \
-DLLVM_BUILD_LLVM_DYLIB:BOOL=OFF \
-DLLVM_BUILD_TESTS:BOOL=OFF \
-DLLVM_INCLUDE_BENCHMARKS:BOOL=OFF \
-DLLVM_INCLUDE_DOCS:BOOL=OFF \
-DLLVM_INCLUDE_EXAMPLES:BOOL=OFF \
-DLLVM_INCLUDE_TESTS:BOOL=OFF \
-DLLVM_ENABLE_BINDINGS:BOOL=OFF \
-DLLVM_ENABLE_LIBXML2:BOOL=ON \
-DLLVM_ENABLE_LLD:BOOL=ON \
-DLLDB_ENABLE_PYTHON:BOOL=ON \
-DLLDB_EMBED_PYTHON_HOME=ON \
-DLLDB_PYTHON_HOME=.. \
-DLLDB_PYTHON_RELATIVE_PATH=lib/lldb-python \
-DPython3_EXECUTABLE="$(pwd)/../python/bin/python${{ env.PYTHON_VERSION }}"
cmake --build build --target lldb install --parallel $(nproc)
working-directory: core/deps/llvm-project
- if: ${{ steps.cache-lldb.outputs.cache-hit != 'true' }}
name: copy lldb to extension folder
run: |
mkdir -p bin
mkdir -p lib
cp ../../../../../../core/deps/llvm-project/lldb/tools/lldb-vscode/package.json ./
cp -r ../../../../../../core/deps/llvm-project/lldb/tools/lldb-vscode/syntaxes/ ./
cp ../../../../../../core/deps/llvm-project/build/bin/lldb* bin
cp ../../../../../../core/deps/llvm-project/build/lib/liblldb*.so lib
cp ../../../../../../core/deps/llvm-project/build/lib/liblldb*.so.* lib
cp -R ../../../../../../core/deps/llvm-project/build/lib/lldb-python lib
cp -R ../../../../../../core/deps/python/lib/python* lib
cp ../../../../../../core/deps/python/lib/libpython${{ env.PYTHON_VERSION }}.so.1.0 lib
working-directory: test-tools/wamr-ide/VSCode-Extension/resource/debug/linux
- name: run tests
timeout-minutes: 5
run: xvfb-run npm run test
working-directory: test-tools/wamr-ide/VSCode-Extension

View File

@ -4,4 +4,9 @@ node_modules
.vscode-test/
*.vsix
package-lock.json
src/test
.vscode
resource/debug/**
!resource/debug/darwin/.placeholder
!resource/debug/linux/.placeholder
!resource/debug/windows/.placeholder
resource/test/test.wasm

View File

@ -0,0 +1 @@
engine-strict=true

View File

@ -10,6 +10,17 @@
"args": ["--extensionDevelopmentPath=${workspaceFolder}"],
"outFiles": ["${workspaceFolder}/out/**/*.js"],
"preLaunchTask": "${defaultBuildTask}"
},
{
"name": "Launch Extension Tests",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}",
"--extensionTestsPath=${workspaceFolder}/out/test/suite/index"
],
"outFiles": ["${workspaceFolder}/out/test/**/*.js"]
}
]
}

View File

@ -9,3 +9,11 @@ out/test/**
**/.eslintrc.json
**/*.map
**/*.ts
src
resource/test
resource/debug/**
!resource/debug/darwin/.placeholder
!resource/debug/linux/.placeholder
!resource/debug/windows/.placeholder

View File

@ -8,8 +8,10 @@
"description": "An Integrated Development Environment for WASM",
"version": "1.2.2",
"engines": {
"vscode": "^1.59.0"
"vscode": "^1.59.0",
"node": ">=16.0.0"
},
"engineStrict": true,
"categories": [
"Other"
],
@ -235,6 +237,7 @@
"prettier-format-apply": "prettier --config .prettierrc.json 'src/**/*.ts' --write"
},
"devDependencies": {
"@types/chai": "^4.3.5",
"@types/glob": "^7.1.3",
"@types/mocha": "^8.2.2",
"@types/node": "14.x",
@ -243,12 +246,14 @@
"@types/yauzl": "^2.10.0",
"@typescript-eslint/eslint-plugin": "^4.26.0",
"@typescript-eslint/parser": "^4.26.0",
"@vscode/debugprotocol": "^1.61.0",
"@vscode/test-electron": "^2.3.3",
"chai": "^4.3.7",
"eslint": "^7.32.0",
"glob": "^7.1.7",
"mocha": "^8.4.0",
"mocha": "^10.2.0",
"prettier": "2.5.1",
"typescript": "^4.3.2",
"vscode-test": "^1.5.2"
"typescript": "^4.3.2"
},
"dependencies": {
"@vscode/webview-ui-toolkit": "^0.8.4",

View File

@ -0,0 +1,2 @@
# compile with debug symbols and no optimization
rustc --target wasm32-wasi ./test.rs -g -C opt-level=0

View File

@ -0,0 +1,35 @@
/*
* Copyright (C) 2019 Intel Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
*/
use std::collections::HashMap;
use std::collections::VecDeque;
use std::cell::RefCell;
fn main() {
let mut vector = Vec::from([1, 2, 3, 4]);
vector.push(12);
let mut map: HashMap<&str, f64> = HashMap::from([
("Mercury", 0.4),
("Venus", 0.7),
("Earth", 1.0),
("Mars", 1.5),
]);
map.insert("Venus", 2.5);
map.insert("Sun", 312.2);
let string = "this is a string";
let tmp = String::from("hello world");
let slice = &tmp[1..5];
let mut deque = VecDeque::from([1, 2, 3]);
deque.push_back(4);
deque.push_back(5);
let ref_cell = RefCell::new(5);
println!("Hello, world!"); // BP_MARKER_1
}

View File

@ -40,7 +40,7 @@ let isWasmProject = false;
export async function activate(context: vscode.ExtensionContext) {
const extensionPath = context.extensionPath;
const osPlatform = os.platform();
const wamrVersion = getWAMRExtensionVersion(context);
const wamrVersion = getWAMRExtensionVersion(context.extensionPath);
const typeMap = new Map<string, string>();
const scriptMap = new Map<string, string>();
/* set relative path of build.bat|sh script */
@ -409,13 +409,13 @@ export async function activate(context: vscode.ExtensionContext) {
/* we should check again whether the user installed lldb, as this can be skipped during activation */
try {
if (!isLLDBInstalled(context)) {
if (!isLLDBInstalled(context.extensionPath)) {
/**NOTE - if users select to skip install,
* we should return rather than continue
* the execution
*/
if (
(await promptInstallLLDB(context)) ===
(await promptInstallLLDB(context.extensionPath)) ===
SelectionOfPrompt.skip
) {
return;
@ -772,8 +772,8 @@ export async function activate(context: vscode.ExtensionContext) {
);
try {
if (!isLLDBInstalled(context)) {
await promptInstallLLDB(context);
if (!isLLDBInstalled(context.extensionPath)) {
await promptInstallLLDB(context.extensionPath);
}
if (

View File

@ -0,0 +1,33 @@
/*
* Copyright (C) 2019 Intel Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
*/
import * as path from 'path';
import * as os from 'os';
import { runTests } from '@vscode/test-electron';
async function main() {
try {
// The folder containing the Extension Manifest package.json
// Passed to `--extensionDevelopmentPath`
const extensionDevelopmentPath = path.resolve(__dirname, '../../');
// The path to the extension test script
// Passed to --extensionTestsPath
const extensionTestsPath = path.resolve(__dirname, './suite/index');
// Download VS Code, unzip it and run the integration test
await runTests({
extensionDevelopmentPath,
extensionTestsPath,
launchArgs: ['--user-data-dir', `${os.tmpdir()}`]
});
} catch (err) {
console.error('Failed to run tests');
process.exit(1);
}
}
main();

View File

@ -0,0 +1,183 @@
/*
* Copyright (C) 2019 Intel Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
*/
import {DebugProtocol} from '@vscode/debugprotocol';
import {after, before, test, suite} from 'mocha';
import {assert} from 'chai';
import * as vscode from 'vscode';
import * as cp from 'child_process';
import * as path from "path";
import * as os from 'os';
import {WasmDebugConfig, WasmDebugConfigurationProvider} from "../../debugConfigurationProvider";
import {EXTENSION_PATH, clearAllBp, setBpAtMarker, compileRustToWasm} from "./utils";
import {downloadLldb, isLLDBInstalled} from '../../utilities/lldbUtilities';
suite('Unit Tests', function () {
test('DebugConfigurationProvider init commands', function () {
const testExtensionPath = "/test/path/";
const provider = new WasmDebugConfigurationProvider(testExtensionPath);
assert.includeMembers(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
provider.getDebugConfig().initCommands!,
[`command script import ${testExtensionPath}/formatters/rust.py`],
"Debugger init commands did not contain "
);
});
test('DebugConfigurationProvider resolve configuration', function () {
const testExtensionPath = "/test/path/";
const provider = new WasmDebugConfigurationProvider(testExtensionPath);
const actual = provider.resolveDebugConfiguration(undefined, {
type: "wamr-debug",
name: "Attach",
request: "attach",
initCommands: [],
attachCommands: [
'process connect -p wasm connect://123.456.789.1:1237',
]
});
assert.deepEqual(
actual,
{
type: "wamr-debug",
name: "Attach",
request: "attach",
stopOnEntry: true,
initCommands: [],
attachCommands: [
'process connect -p wasm connect://123.456.789.1:1237',
]
},
"Configuration did not match the expected configuration after calling resolveDebugConfiguration()"
);
});
});
suite('Inegration Tests', function () {
let debuggerProcess: cp.ChildProcessWithoutNullStreams;
const port = 1239;
const downloadTimeout = 60 * 1000;
before(async function () {
// timeout of 20 seconds
this.timeout(20 * 1000);
// Download LLDB if necessary. Should be available in the CI. Only for local execution.
if (!isLLDBInstalled(EXTENSION_PATH)) {
this.timeout(downloadTimeout);
console.log("Downloading LLDB. This might take a moment...");
await downloadLldb(EXTENSION_PATH);
assert.isTrue(isLLDBInstalled(EXTENSION_PATH), "LLDB was not installed correctly");
}
compileRustToWasm();
const platform = os.platform();
assert.isTrue(platform === "darwin" || platform === "linux", `Tests do not support your platform: ${platform}`);
const iWasmPath = path.resolve(`${EXTENSION_PATH}/../../../product-mini/platforms/${platform}/build/iwasm`);
const testWasmFilePath = `${EXTENSION_PATH}/resource/test/test.wasm`;
debuggerProcess = cp.spawn(
iWasmPath,
[`-g=127.0.0.1:${port}`, testWasmFilePath],
{}
);
debuggerProcess.stderr.on('data', (data) => {
console.log(`Error from debugger process: ${data}`);
});
});
after(async function () {
await vscode.debug.stopDebugging();
debuggerProcess.kill();
});
test('Rust formatters', async function () {
// timeout of 1 minutes
this.timeout(60 * 1000);
clearAllBp();
setBpAtMarker(`${EXTENSION_PATH}/resource/test/test.rs`, "BP_MARKER_1");
const getVariables = new Promise<DebugProtocol.Variable[]>((resolve, reject) => {
vscode.debug.registerDebugAdapterTrackerFactory("wamr-debug", {
createDebugAdapterTracker: function () {
return {
// The debug adapter has sent a Debug Adapter Protocol message to the editor.
onDidSendMessage: (message: DebugProtocol.ProtocolMessage) => {
if (message.type === "response") {
const m = message as DebugProtocol.Response;
if (m.command === "variables") {
const res = m as DebugProtocol.VariablesResponse;
resolve(res.body.variables);
}
}
},
onError: (error: Error) => {
reject("An error occurred before vscode reached the breakpoint: " + error);
},
onExit: (code: number | undefined) => {
reject(`Debugger exited before vscode reached the breakpoint with code: ${code}`);
},
};
}
});
});
const config: WasmDebugConfig = {
type: "wamr-debug",
request: "attach",
name: "Attach Debugger",
stopOnEntry: false,
initCommands: [
`command script import ${EXTENSION_PATH}/formatters/rust.py`
],
attachCommands: [
`process connect -p wasm connect://127.0.0.1:${port}`
]
};
if (os.platform() === 'win32' || os.platform() === 'darwin') {
config.initCommands?.push('platform select remote-linux');
}
try {
await vscode.debug.startDebugging(undefined, config);
} catch (e) {
assert.fail("Could not connect to debug adapter");
}
// wait until vs code has reached breakpoint and has requested the variables.
const variables = await getVariables;
const namesToVariables = variables.reduce((acc: { [name: string]: DebugProtocol.Variable }, c) => {
if (c.evaluateName) {
acc[c.evaluateName] = c;
}
return acc;
}, {});
assert.includeMembers(Object.keys(namesToVariables), ["vector", "map", "string", "slice", "deque", "ref_cell"], "The Debugger did not return all expected debugger variables.");
// Vector
assert.equal(namesToVariables["vector"].value, " (5) vec![1, 2, 3, 4, 12]", "The Vector summary string looks different than expected");
// Map
assert.equal(namesToVariables["map"].value, " size=5, capacity=8", "The Map summary string looks different than expected");
// String
assert.equal(namesToVariables["string"].value, " \"this is a string\"", "The String summary string looks different than expected");
// Slice
assert.equal(namesToVariables["slice"].value, " \"ello\"", "The Slice summary string looks different than expected");
// Deque
assert.equal(namesToVariables["deque"].value, " (5) VecDeque[1, 2, 3, 4, 5]", "The Deque summary string looks different than expected");
// RefCell
assert.equal(namesToVariables["ref_cell"].value, " 5", "The RefCell summary string looks different than expected");
});
});

View File

@ -0,0 +1,42 @@
/*
* Copyright (C) 2019 Intel Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
*/
import * as path from 'path';
import * as Mocha from 'mocha';
import * as glob from 'glob';
export function run(): Promise<void> {
// Create the mocha test
const mocha = new Mocha({
ui: 'tdd'
});
const testsRoot = path.resolve(__dirname, '..');
return new Promise((c, e) => {
glob('**/**.test.js', { cwd: testsRoot }, (err, files) => {
if (err) {
return e(err);
}
// Add files to the test suite
files.forEach(f => mocha.addFile(path.resolve(testsRoot, f)));
try {
// Run the mocha test
mocha.run(failures => {
if (failures > 0) {
e(new Error(`${failures} tests failed.`));
} else {
c();
}
});
} catch (err) {
console.error(err);
e(err);
}
});
});
}

View File

@ -0,0 +1,43 @@
/*
* Copyright (C) 2019 Intel Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
*/
import {assert} from 'chai';
import * as vscode from 'vscode';
import {Range, SourceBreakpoint} from "vscode";
import * as fs from "fs";
import path = require('path');
import * as cp from 'child_process';
export const EXTENSION_PATH = path.resolve(`${__dirname}/../../..`);
// clears all set breakpoints
export function clearAllBp(): void {
vscode.debug.removeBreakpoints(vscode.debug.breakpoints);
}
// Inserts a breakpoint in a file at the first occurrence of bpMarker
export function setBpAtMarker(file: string, bpMarker: string): void {
const uri = vscode.Uri.file(file);
const data = fs.readFileSync(uri.path, "utf8");
const line = data.split("\n").findIndex(line => line.includes(bpMarker));
assert.notStrictEqual(line, -1, "Could not find breakpoint marker in source file");
const position = new vscode.Position(line, 0);
const bp = new SourceBreakpoint(new vscode.Location(uri, new Range(position, position)), true);
vscode.debug.addBreakpoints([bp]);
}
// compiles resources/test/test.rs to test.wasm
export function compileRustToWasm(): void {
const testResourceFolder = `${EXTENSION_PATH}/resource/test`;
// compile with debug symbols and no optimization
const cmd = `rustc --target wasm32-wasi ${testResourceFolder}/test.rs -g -C opt-level=0 -o ${testResourceFolder}/test.wasm`;
try {
cp.execSync(cmd, {stdio: [null, null, process.stderr]});
} catch (e) {
assert.fail(`Compilation of example rust file failed with error: ${e}`);
}
assert.isTrue(fs.existsSync(`${testResourceFolder}/test.wasm`), "Could not find wasm file WASM file to run debugger on.");
}

View File

@ -102,7 +102,7 @@ export async function checkIfDockerImagesExist(
): Promise<boolean> {
try {
/* the tag of images is equal to extension's version */
const imageTag = getWAMRExtensionVersion(context);
const imageTag = getWAMRExtensionVersion(context.extensionPath);
await execShell(
`docker image inspect wasm-debug-server:${imageTag} wasm-toolchain:${imageTag}`
);
@ -115,7 +115,7 @@ export async function checkIfDockerImagesExist(
function getDockerImagesDownloadUrl(
context: vscode.ExtensionContext
): string[] {
const wamrVersion = getWAMRExtensionVersion(context);
const wamrVersion = getWAMRExtensionVersion(context.extensionPath);
const wamrReleaseUrl = `https://github.com/bytecodealliance/wasm-micro-runtime/releases/download/WAMR`;
return [

View File

@ -36,14 +36,14 @@ function getLLDBUnzipFilePath(destinationFolder: string, filename: string) {
}
export function getWAMRExtensionVersion(
context: vscode.ExtensionContext
extensionPath: string
): string {
// eslint-disable-next-line @typescript-eslint/no-var-requires
return require(path.join(context.extensionPath, 'package.json')).version;
return require(path.join(extensionPath, 'package.json')).version;
}
function getLLDBDownloadUrl(context: vscode.ExtensionContext): string {
const wamrVersion = getWAMRExtensionVersion(context);
function getLLDBDownloadUrl(extensionPath: string): string {
const wamrVersion = getWAMRExtensionVersion(extensionPath);
const lldbOsUrlSuffix = LLDB_OS_DOWNLOAD_URL_SUFFIX_MAP[os.platform()];
if (!lldbOsUrlSuffix) {
@ -53,8 +53,7 @@ function getLLDBDownloadUrl(context: vscode.ExtensionContext): string {
return `https://github.com/bytecodealliance/wasm-micro-runtime/releases/download/WAMR-${wamrVersion}/wamr-lldb-${wamrVersion}-${lldbOsUrlSuffix}.zip`;
}
export function isLLDBInstalled(context: vscode.ExtensionContext): boolean {
const extensionPath = context.extensionPath;
export function isLLDBInstalled(extensionPath: string): boolean {
const lldbOSDir = os.platform();
const lldbBinaryPath = path.join(
extensionPath,
@ -67,9 +66,8 @@ export function isLLDBInstalled(context: vscode.ExtensionContext): boolean {
}
export async function promptInstallLLDB(
context: vscode.ExtensionContext
extensionPath: string
): Promise<SelectionOfPrompt> {
const extensionPath = context.extensionPath;
const response = await vscode.window.showWarningMessage(
'No LLDB instance found. Setup now?',
@ -81,7 +79,15 @@ export async function promptInstallLLDB(
return response;
}
const downloadUrl = getLLDBDownloadUrl(context);
await downloadLldb(extensionPath);
return SelectionOfPrompt.setUp;
}
export async function downloadLldb(
extensionPath: string
): Promise<void> {
const downloadUrl = getLLDBDownloadUrl(extensionPath);
const destinationDir = os.platform();
if (!downloadUrl) {
@ -115,5 +121,4 @@ export async function promptInstallLLDB(
// Remove the bundle.zip
fs.unlinkSync(lldbZipPath);
return SelectionOfPrompt.setUp;
}