Skip to content

Commit 5fcd957

Browse files
authored
feat: log stack trace when AS crash due to assert (#54)
1 parent 725a24c commit 5fcd957

21 files changed

+530
-138
lines changed

instrumentation/CoverageInstru.cpp

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
#include "CoverageInstru.hpp"
2+
#include <binaryen-c.h>
3+
#include <fstream>
4+
#include <ostream>
25
namespace wasmInstrumentation {
36

47
void CoverageInstru::innerAnalysis(BasicBlockAnalysis &basicBlockAnalysis) const noexcept {
@@ -30,16 +33,14 @@ void CoverageInstru::innerAnalysis(BasicBlockAnalysis &basicBlockAnalysis) const
3033

3134
InstrumentationResponse CoverageInstru::instrument() const noexcept {
3235
if (config->fileName.empty() || config->reportFunction.empty() || config->sourceMap.empty() ||
33-
config->targetName.empty() || config->expectInfoOutputFilePath.empty()
34-
) {
36+
config->targetName.empty() || config->expectInfoOutputFilePath.empty()) {
3537
std::cout << *config << std::endl;
3638
return InstrumentationResponse::CONFIG_ERROR; // config error
3739
}
38-
std::filesystem::path filePath(config->fileName);
39-
std::filesystem::path targetFilePath(config->targetName);
40-
std::filesystem::path sourceMapPath(config->sourceMap);
41-
if ((!std::filesystem::exists(filePath)) ||
42-
(!std::filesystem::exists(sourceMapPath)) ||
40+
const std::filesystem::path filePath(config->fileName);
41+
const std::filesystem::path targetFilePath(config->targetName);
42+
const std::filesystem::path sourceMapPath(config->sourceMap);
43+
if ((!std::filesystem::exists(filePath)) || (!std::filesystem::exists(sourceMapPath)) ||
4344
(!std::filesystem::exists(targetFilePath.parent_path()))) {
4445
std::cout << *config << std::endl;
4546
return InstrumentationResponse::CONFIG_FILEPATH_ERROR; // config file path error
@@ -127,12 +128,15 @@ InstrumentationResponse CoverageInstru::instrument() const noexcept {
127128
MockInstrumentationWalker mockWalker(&module);
128129
mockWalker.mockWalk();
129130

131+
const std::string targetSourceMapPath = std::string{this->config->targetName} + ".map";
132+
BinaryenSetDebugInfo(true);
130133
const BinaryenModuleAllocateAndWriteResult result =
131-
BinaryenModuleAllocateAndWrite(&module, nullptr);
134+
BinaryenModuleAllocateAndWrite(&module, targetSourceMapPath.c_str());
132135
std::ofstream wasmFileStream(this->config->targetName.data(), std::ios::trunc | std::ios::binary);
133136
wasmFileStream.write(static_cast<char *>(result.binary),
134137
static_cast<std::streamsize>(result.binaryBytes));
135-
wasmFileStream.close();
138+
std::ofstream sourceMapFileStream(targetSourceMapPath, std::ios::trunc | std::ios::binary);
139+
sourceMapFileStream << result.sourceMap << std::flush;
136140
free(result.binary);
137141
free(result.sourceMap);
138142
if (wasmFileStream.fail() || wasmFileStream.bad()) {

package-lock.json

Lines changed: 25 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"glob": "^11.0.0",
3737
"ignore": "^7.0.3",
3838
"semver": "^7.5.3",
39+
"source-map": "^0.7.4",
3940
"wasmparser": "5.11.1"
4041
},
4142
"peerDependencies": {

src/core/execute.ts

Lines changed: 46 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@ import { WASI } from "node:wasi";
22
import { promises } from "node:fs";
33
import { ensureDirSync } from "fs-extra";
44
import { instantiate, Imports as ASImports } from "@assemblyscript/loader";
5-
import { ExecutionResult } from "../executionResult.js";
5+
import { ExecutionResultSummary } from "../executionResult.js";
66
import { Imports, ImportsArgument, InstrumentResult } from "../interface.js";
77
import { mockInstrumentFunc } from "../utils/import.js";
88
import { supplyDefaultFunction } from "../utils/index.js";
99
import { parseImportFunctionInfo } from "../utils/wasmparser.js";
10-
import { ExecutionRecorder, SingleExecutionResult } from "./executionRecorder.js";
10+
import { ExecutionRecorder, ExecutionResult } from "./executionRecorder.js";
1111
import { CoverageRecorder } from "./covRecorder.js";
1212
import assert from "node:assert";
13+
import { ExecutionError, handleWebAssemblyError } from "../utils/errorTraceHandler.js";
1314

1415
const readFile = promises.readFile;
1516

@@ -18,7 +19,7 @@ async function nodeExecutor(
1819
outFolder: string,
1920
matchedTestNames: string[],
2021
imports?: Imports
21-
): Promise<SingleExecutionResult> {
22+
): Promise<ExecutionResult> {
2223
const wasi = new WASI({
2324
args: ["node", instrumentResult.baseName],
2425
env: process.env,
@@ -48,36 +49,51 @@ async function nodeExecutor(
4849
importsArg.module = ins.module;
4950
importsArg.instance = ins.instance;
5051
importsArg.exports = ins.exports;
52+
53+
let isCrashed = false; // we don't want to crash any code after crash. AS' heap may be broken.
54+
55+
const exceptionHandler = async (error: unknown) => {
56+
if (error instanceof WebAssembly.RuntimeError) {
57+
isCrashed = true;
58+
const errorMessage: ExecutionError = await handleWebAssemblyError(error, instrumentResult.instrumentedWasm);
59+
executionRecorder.notifyTestCrash(errorMessage);
60+
return;
61+
}
62+
// unrecoverable error, rethrow
63+
if (error instanceof Error) {
64+
console.error(error.stack);
65+
}
66+
throw new Error("node executor abort");
67+
};
68+
5169
try {
70+
executionRecorder.startTestFunction(`${instrumentResult.baseName} - init`);
5271
wasi.start(ins);
53-
const execTestFunction = ins.exports["executeTestFunction"];
54-
assert(typeof execTestFunction === "function");
55-
if (matchedTestNames.length === 0) {
56-
// By default, all testcases are executed
57-
for (const functionInfo of executionRecorder.registerFunctions) {
58-
const [testCaseName, functionIndex] = functionInfo;
59-
executionRecorder.startTestFunction(testCaseName);
72+
} catch (error) {
73+
await exceptionHandler(error);
74+
}
75+
executionRecorder.finishTestFunction();
76+
77+
const execTestFunction = ins.exports["executeTestFunction"];
78+
assert(typeof execTestFunction === "function");
79+
80+
for (const functionInfo of executionRecorder.registerFunctions) {
81+
if (isCrashed) {
82+
break;
83+
}
84+
const [testCaseName, functionIndex] = functionInfo;
85+
if (matchedTestNames.length === 0 || matchedTestNames.includes(testCaseName)) {
86+
executionRecorder.startTestFunction(testCaseName);
87+
try {
6088
(execTestFunction as (a: number) => void)(functionIndex);
61-
executionRecorder.finishTestFunction();
62-
mockInstrumentFunc["mockFunctionStatus.clear"]();
89+
} catch (error) {
90+
await exceptionHandler(error);
6391
}
64-
} else {
65-
for (const functionInfo of executionRecorder.registerFunctions) {
66-
const [testCaseName, functionIndex] = functionInfo;
67-
if (matchedTestNames.includes(testCaseName)) {
68-
executionRecorder.startTestFunction(testCaseName);
69-
(execTestFunction as (a: number) => void)(functionIndex);
70-
executionRecorder.finishTestFunction();
71-
mockInstrumentFunc["mockFunctionStatus.clear"]();
72-
}
73-
}
74-
}
75-
} catch (error) {
76-
if (error instanceof Error) {
77-
console.error(error.stack);
92+
executionRecorder.finishTestFunction();
93+
mockInstrumentFunc["mockFunctionStatus.clear"]();
7894
}
79-
throw new Error("node executor abort.");
8095
}
96+
8197
coverageRecorder.outputTrace(instrumentResult.traceFile);
8298
return executionRecorder.result;
8399
}
@@ -87,12 +103,12 @@ export async function execWasmBinaries(
87103
instrumentResults: InstrumentResult[],
88104
matchedTestNames: string[],
89105
imports?: Imports
90-
): Promise<ExecutionResult> {
91-
const assertRes = new ExecutionResult();
106+
): Promise<ExecutionResultSummary> {
107+
const assertRes = new ExecutionResultSummary();
92108
ensureDirSync(outFolder);
93109
await Promise.all<void>(
94110
instrumentResults.map(async (instrumentResult): Promise<void> => {
95-
const result: SingleExecutionResult = await nodeExecutor(instrumentResult, outFolder, matchedTestNames, imports);
111+
const result: ExecutionResult = await nodeExecutor(instrumentResult, outFolder, matchedTestNames, imports);
96112
await assertRes.merge(result, instrumentResult.expectInfo);
97113
})
98114
);

src/core/executionRecorder.ts

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1+
import chalk from "chalk";
12
import {
23
UnitTestFramework,
34
ImportsArgument,
45
AssertFailMessage,
56
AssertMessage,
6-
IAssertResult,
7+
IExecutionResult,
78
FailedLogMessages,
9+
CrashInfo,
810
} from "../interface.js";
11+
import { ExecutionError } from "../utils/errorTraceHandler.js";
912

1013
class LogRecorder {
1114
#currentTestLogMessages: string[] = [];
@@ -18,7 +21,7 @@ class LogRecorder {
1821
this.#isTestFailed = true;
1922
}
2023

21-
onStartTest(): void {
24+
reset(): void {
2225
this.#currentTestLogMessages = [];
2326
this.#isTestFailed = false;
2427
}
@@ -33,16 +36,16 @@ class LogRecorder {
3336
}
3437
}
3538

36-
export class SingleExecutionResult implements IAssertResult {
39+
export class ExecutionResult implements IExecutionResult {
3740
total: number = 0;
3841
fail: number = 0;
3942
failedInfo: AssertFailMessage = {};
43+
crashInfo: CrashInfo = new Set();
4044
failedLogMessages: FailedLogMessages = {};
4145
}
4246

43-
// to do: split execution environment and recorder
4447
export class ExecutionRecorder implements UnitTestFramework {
45-
result = new SingleExecutionResult();
48+
result = new ExecutionResult();
4649

4750
registerFunctions: [string, number][] = [];
4851
#currentTestDescriptions: string[] = [];
@@ -63,9 +66,10 @@ export class ExecutionRecorder implements UnitTestFramework {
6366
const testCaseFullName = this.#currentTestDescriptions.join(" ");
6467
this.registerFunctions.push([testCaseFullName, fncIndex]);
6568
}
69+
6670
startTestFunction(testCaseFullName: string): void {
6771
this.#testCaseFullName = testCaseFullName;
68-
this.logRecorder.onStartTest();
72+
this.logRecorder.reset();
6973
}
7074
finishTestFunction(): void {
7175
const logMessages: string[] | null = this.logRecorder.onFinishTest();
@@ -76,11 +80,22 @@ export class ExecutionRecorder implements UnitTestFramework {
7680
}
7781
}
7882

83+
notifyTestCrash(error: ExecutionError): void {
84+
this.logRecorder.addLog(`Reason: ${chalk.red(error.message)}`);
85+
this.logRecorder.addLog(
86+
error.stacks
87+
.map((stack) => ` at ${stack.functionName} (${stack.fileName}:${stack.lineNumber}:${stack.columnNumber})`)
88+
.join("\n")
89+
);
90+
this.result.crashInfo.add(this.#testCaseFullName);
91+
this.result.total++; // fake test cases
92+
this.#increaseFailureCount();
93+
}
94+
7995
collectCheckResult(result: boolean, codeInfoIndex: number, actualValue: string, expectValue: string): void {
8096
this.result.total++;
8197
if (!result) {
82-
this.logRecorder.markTestFailed();
83-
this.result.fail++;
98+
this.#increaseFailureCount();
8499
const testCaseFullName = this.#testCaseFullName;
85100
const assertMessage: AssertMessage = [codeInfoIndex.toString(), actualValue, expectValue];
86101
this.result.failedInfo[testCaseFullName] = this.result.failedInfo[testCaseFullName] || [];
@@ -115,4 +130,9 @@ export class ExecutionRecorder implements UnitTestFramework {
115130
},
116131
};
117132
}
133+
134+
#increaseFailureCount(): void {
135+
this.result.fail++;
136+
this.logRecorder.markTestFailed();
137+
}
118138
}

0 commit comments

Comments
 (0)