Skip to content

Feat/support test name pattern #50

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Jun 19, 2025
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.vscode
.cache

/node_modules
/dist
Expand Down
Empty file removed a.ts
Empty file.
4 changes: 0 additions & 4 deletions assembly/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,6 @@ export namespace assertResult {
export declare function registerTestFunction(index: u32): void;


@external("__unittest_framework_env","finishTestFunction")
export declare function finishTestFunction(): void;


@external("__unittest_framework_env","collectCheckResult")
export declare function collectCheckResult(
result: bool,
Expand Down
3 changes: 0 additions & 3 deletions assembly/implement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,7 @@ export function describeImpl(
export function testImpl(description: string, testFunction: () => void): void {
assertResult.addDescription(description);
assertResult.registerTestFunction(testFunction.index);
testFunction();
assertResult.finishTestFunction();
assertResult.removeDescription();
mockFunctionStatus.clear();
}

export function mockImpl<T extends Function>(
Expand Down
51 changes: 25 additions & 26 deletions bin/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,17 @@ import { validatArgument, start_unit_test } from "../dist/index.js";
const program = new Command();
program
.option("--config <config file>", "path of config file", "as-test.config.js")
.option("--testcase <testcases...>", "only run specified test cases")
.option("--testcase <testcases...>", "run only specified test cases")
.option("--temp <path>", "test template file folder")
.option("--output <path>", "coverage report output folder")
.option("--mode <output mode>", "test result output format")
.option("--mode <output mode>", "coverage report output format")
.option("--coverageLimit [error warning...]", "set warn(yellow) and error(red) upper limit in coverage report")
.option("--testNamePattern <test name pattern>", "run only tests with a name that matches the regex pattern");
.option("--testNamePattern <test name pattern>", "run only tests with a name that matches the regex pattern")
.option("--collectCoverage <boolean>", "whether to collect coverage information and report");

program.parse(process.argv);
const options = program.opts();

if (options.config === undefined) {
console.error(chalk.redBright("Miss config file") + "\n");
console.error(program.helpInformation());
exit(-1);
}
const configPath = resolve(".", options.config);
if (!fs.pathExistsSync(configPath)) {
console.error(chalk.redBright("Miss config file") + "\n");
Expand All @@ -35,33 +31,36 @@ if (!fs.pathExistsSync(configPath)) {
}
const config = (await import(pathToFileURL(configPath))).default;

let includes = config.include;
const includes = config.include;
if (includes === undefined) {
console.error(chalk.redBright("Miss include in config file") + "\n");
exit(-1);
}
let excludes = config.exclude || [];
let testcases = options.testcase;

let flags = config.flags || "";
let imports = config.imports || null;
const excludes = config.exclude || [];
validatArgument(includes, excludes);

let mode = options.mode || config.mode || "table";
// if enabled testcase or testNamePattern, disable collectCoverage by default
const collectCoverage =
Boolean(options.collectCoverage) || config.collectCoverage || (!options.testcase && !options.testNamePattern);

let tempFolder = options.temp || config.temp || "coverage";
let outputFolder = options.output || config.output || "coverage";
const testOption = {
includes,
excludes,
testcases: options.testcase,
testNamePattern: options.testNamePattern,
collectCoverage,

let errorLimit = options.coverageLimit?.at(0);
let warnLimit = options.coverageLimit?.at(1);
flags: config.flags || "",
imports: config.imports || undefined,

let testNamePattern = options.testNamePattern;
tempFolder: options.temp || config.temp || "coverage",
outputFolder: options.output || config.output || "coverage",
mode: options.mode || config.mode || "table",
warnLimit: Number(options.coverageLimit?.at(1)),
errorLimit: Number(options.coverageLimit?.at(0)),
};

validatArgument(includes, excludes);
start_unit_test(
{ includes, excludes, testcases, testNamePattern },
{ flags, imports },
{ tempFolder, outputFolder, mode, warnLimit, errorLimit }
)
start_unit_test(testOption)
.then((success) => {
if (!success) {
console.error(chalk.redBright("Test Failed") + "\n");
Expand Down
132 changes: 72 additions & 60 deletions instrumentation/CoverageInstru.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,15 @@ void CoverageInstru::innerAnalysis(BasicBlockAnalysis &basicBlockAnalysis) const

InstrumentationResponse CoverageInstru::instrument() const noexcept {
if (config->fileName.empty() || config->reportFunction.empty() || config->sourceMap.empty() ||
config->targetName.empty() || config->expectInfoOutputFilePath.empty() ||
config->debugInfoOutputFilePath.empty()) {
config->targetName.empty() || config->expectInfoOutputFilePath.empty()
) {
std::cout << *config << std::endl;
return InstrumentationResponse::CONFIG_ERROR; // config error
}
std::filesystem::path filePath(config->fileName);
std::filesystem::path targetFilePath(config->targetName);
std::filesystem::path debugInfoPath(config->debugInfoOutputFilePath);
std::filesystem::path sourceMapPath(config->sourceMap);
if ((!std::filesystem::exists(filePath)) ||
(!std::filesystem::exists(debugInfoPath.parent_path())) ||
(!std::filesystem::exists(sourceMapPath)) ||
(!std::filesystem::exists(targetFilePath.parent_path()))) {
std::cout << *config << std::endl;
Expand All @@ -49,69 +47,82 @@ InstrumentationResponse CoverageInstru::instrument() const noexcept {

wasm::Module module;
wasm::ModuleReader reader;

Json::StreamWriterBuilder jsonBuilder;
jsonBuilder["indentation"] = "";
reader.read(std::string(config->fileName), module, std::string(config->sourceMap));
BasicBlockAnalysis basicBlockAnalysis = BasicBlockAnalysis();
innerAnalysis(basicBlockAnalysis);
BasicBlockWalker basicBlockWalker = BasicBlockWalker(&module, basicBlockAnalysis);
basicBlockWalker.basicBlockWalk();
const std::unordered_map<std::string_view, FunctionAnalysisResult> &results =
basicBlockWalker.getResults();
Json::Value json;
Json::Value debugInfoJson;
Json::Value debugFileJson;
for (auto &[function, result] : results) {
Json::Value innerJson;
innerJson["index"] = result.functionIndex;
Json::Value branchInfoArray(Json::ValueType::arrayValue);
for (const auto &branchInfo : result.branchInfo) {
Json::Value inner_array;
inner_array.append(branchInfo.first);
inner_array.append(branchInfo.second);
branchInfoArray.append(std::move(inner_array));

if (config->collectCoverage) {
if (config->debugInfoOutputFilePath.empty()) {
std::cout << *config << std::endl;
return InstrumentationResponse::CONFIG_ERROR; // config error
}
std::filesystem::path debugInfoPath(config->debugInfoOutputFilePath);
if ((!std::filesystem::exists(debugInfoPath.parent_path()))) {
std::cout << *config << std::endl;
return InstrumentationResponse::CONFIG_FILEPATH_ERROR; // config file path error
}
innerJson["branchInfo"] = branchInfoArray;
Json::Value debugLineJson;
for (const auto &basicBlock : result.basicBlocks) {
if (basicBlock.basicBlockIndex != static_cast<wasm::Index>(-1)) {
Json::Value debugLineItemJsonArray(Json::ValueType::arrayValue);
for (const auto &debugLine : basicBlock.debugLocations) {
Json::Value debugInfo;
debugInfo.append(debugLine.fileIndex);
debugInfo.append(debugLine.lineNumber);
debugInfo.append(debugLine.columnNumber);
debugLineItemJsonArray.append(std::move(debugInfo));

BasicBlockWalker basicBlockWalker = BasicBlockWalker(&module, basicBlockAnalysis);
basicBlockWalker.basicBlockWalk();
const std::unordered_map<std::string_view, FunctionAnalysisResult> &results =
basicBlockWalker.getResults();
Json::Value json;
Json::Value debugInfoJson;
Json::Value debugFileJson;
for (auto &[function, result] : results) {
Json::Value innerJson;
innerJson["index"] = result.functionIndex;
Json::Value branchInfoArray(Json::ValueType::arrayValue);
for (const auto &branchInfo : result.branchInfo) {
Json::Value inner_array;
inner_array.append(branchInfo.first);
inner_array.append(branchInfo.second);
branchInfoArray.append(std::move(inner_array));
}
innerJson["branchInfo"] = branchInfoArray;
Json::Value debugLineJson;
for (const auto &basicBlock : result.basicBlocks) {
if (basicBlock.basicBlockIndex != static_cast<wasm::Index>(-1)) {
Json::Value debugLineItemJsonArray(Json::ValueType::arrayValue);
for (const auto &debugLine : basicBlock.debugLocations) {
Json::Value debugInfo;
debugInfo.append(debugLine.fileIndex);
debugInfo.append(debugLine.lineNumber);
debugInfo.append(debugLine.columnNumber);
debugLineItemJsonArray.append(std::move(debugInfo));
}
debugLineJson[basicBlock.basicBlockIndex] = debugLineItemJsonArray;
}
debugLineJson[basicBlock.basicBlockIndex] = debugLineItemJsonArray;
}
innerJson["lineInfo"] = debugLineJson;
debugInfoJson[function.data()] = innerJson;
}
innerJson["lineInfo"] = debugLineJson;
debugInfoJson[function.data()] = innerJson;
}
for (const std::string &debugInfoFileName : module.debugInfoFileNames) {
debugFileJson.append(debugInfoFileName);
}
json["debugInfos"] = debugInfoJson;
json["debugFiles"] = debugFileJson;
std::ofstream jsonWriteStream(config->debugInfoOutputFilePath.data(), std::ios::trunc);
Json::StreamWriterBuilder jsonBuilder;
jsonBuilder["indentation"] = "";
std::unique_ptr<Json::StreamWriter> jsonWriter(jsonBuilder.newStreamWriter());
if (jsonWriter->write(json, &jsonWriteStream) != 0) {
// Hard to control IO error
// LCOV_EXCL_START
return InstrumentationResponse::DEBUG_INFO_GENERATION_ERROR; // debug info json write failed
// LCOV_EXCL_STOP
}
jsonWriteStream.close();
if (jsonWriteStream.fail() || jsonWriteStream.bad()) {
// Hard to control IO error
// LCOV_EXCL_START
return InstrumentationResponse::DEBUG_INFO_GENERATION_ERROR; // debug info json write failed
// LCOV_EXCL_STOP
for (const std::string &debugInfoFileName : module.debugInfoFileNames) {
debugFileJson.append(debugInfoFileName);
}
json["debugInfos"] = debugInfoJson;
json["debugFiles"] = debugFileJson;
std::ofstream jsonWriteStream(config->debugInfoOutputFilePath.data(), std::ios::trunc);

std::unique_ptr<Json::StreamWriter> jsonWriter(jsonBuilder.newStreamWriter());
if (jsonWriter->write(json, &jsonWriteStream) != 0) {
// Hard to control IO error
// LCOV_EXCL_START
return InstrumentationResponse::DEBUG_INFO_GENERATION_ERROR; // debug info json write failed
// LCOV_EXCL_STOP
}
jsonWriteStream.close();
if (jsonWriteStream.fail() || jsonWriteStream.bad()) {
// Hard to control IO error
// LCOV_EXCL_START
return InstrumentationResponse::DEBUG_INFO_GENERATION_ERROR; // debug info json write failed
// LCOV_EXCL_STOP
}
CovInstrumentationWalker covWalker(&module, config->reportFunction.data(), basicBlockWalker);
covWalker.covWalk();
}
CovInstrumentationWalker covWalker(&module, config->reportFunction.data(), basicBlockWalker);
covWalker.covWalk();

MockInstrumentationWalker mockWalker(&module);
mockWalker.mockWalk();
Expand Down Expand Up @@ -167,7 +178,7 @@ wasm_instrument(char const *const fileName, char const *const targetName,
char const *const reportFunction, char const *const sourceMap,
char const *const expectInfoOutputFilePath,
char const *const debugInfoOutputFilePath, char const *const includes,
char const *const excludes, bool skipLib) noexcept {
char const *const excludes, bool skipLib, bool collectCoverage) noexcept {

wasmInstrumentation::InstrumentationConfig config;
config.fileName = fileName;
Expand All @@ -179,6 +190,7 @@ wasm_instrument(char const *const fileName, char const *const targetName,
config.includes = includes;
config.excludes = excludes;
config.skipLib = skipLib;
config.collectCoverage = collectCoverage;
wasmInstrumentation::CoverageInstru instrumentor(&config);
return instrumentor.instrument();
}
Expand Down
6 changes: 4 additions & 2 deletions instrumentation/CoverageInstru.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ class InstrumentationConfig final {
std::string_view excludes; ///< function exclude filter
std::string_view expectInfoOutputFilePath; ///< exception info output file name
bool skipLib = true; ///< if skip lib functions
bool collectCoverage = true; ///< whether collect coverage information

///
///@brief Print information of InstrumentationConfig to output stream
Expand All @@ -57,7 +58,8 @@ class InstrumentationConfig final {
<< ", sourceMap: " << instance.sourceMap << ", reportFunction:" << instance.reportFunction
<< ", includes: " << instance.includes << ", excludes: " << instance.excludes
<< ", expectInfoOutputFilePath: " << instance.expectInfoOutputFilePath
<< ", skipLib: " << std::boolalpha << instance.skipLib << std::endl;
<< ", skipLib: " << std::boolalpha << instance.skipLib
<< ", collectCoverage: " << std::boolalpha << instance.collectCoverage << std::endl;
return out;
}
};
Expand Down Expand Up @@ -119,6 +121,6 @@ wasm_instrument(char const *const fileName, char const *const targetName,
char const *const reportFunction, char const *const sourceMap,
char const *const expectInfoOutputFilePath,
char const *const debugInfoOutputFilePath, char const *const includes = NULL,
char const *const excludes = NULL, bool skipLib = true) noexcept;
char const *const excludes = NULL, bool skipLib = true, bool collectCoverage = true) noexcept;
#endif
#endif
22 changes: 22 additions & 0 deletions instrumentation/MockInstrumentationWalker.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
#include "MockInstrumentationWalker.hpp"
#include <asmjs/shared-constants.h>
#include <binaryen-c.h>
#include <string_view>
#include <support/index.h>
#include <wasm-type.h>
#include <wasm.h>
// mock test will be tested with wasm-testing-framework project, escape this class
// LCOV_EXCL_START
Expand Down Expand Up @@ -86,13 +89,32 @@ bool MockInstrumentationWalker::mockFunctionDuplicateImportedCheck() const noexc
return checkRepeat;
}

void MockInstrumentationWalker::addExecuteTestFunction() noexcept {
std::vector<BinaryenExpressionRef> operands{};
if (module->tables.empty()) {
auto * table = module->addTable(wasm::Builder::makeTable(wasm::Name::fromInt(0)));
table->base = "__indirect_function_table";
}
BinaryenExpressionRef body = moduleBuilder.makeCallIndirect(
module->tables[0]->name,
BinaryenLocalGet(module, 0, wasm::Type::i32),
operands,
wasm::Signature(wasm::Type::none, wasm::Type::none)
);

body->finalize();
BinaryenAddFunction(module, "executeTestFunction", BinaryenTypeInt32(), BinaryenTypeNone(), {}, 0, body);
BinaryenAddFunctionExport(module, "executeTestFunction", "executeTestFunction");
}

uint32_t MockInstrumentationWalker::mockWalk() noexcept {
if (mockFunctionDuplicateImportedCheck()) {
return 1U; // failed
} else {
wasm::ModuleUtils::iterDefinedFunctions(*module, [this](wasm::Function *const func) noexcept {
walkFunctionInModule(func, this->module);
});
addExecuteTestFunction();
return 0U;
}
}
Expand Down
5 changes: 5 additions & 0 deletions instrumentation/MockInstrumentationWalker.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,11 @@ class MockInstrumentationWalker final : public wasm::PostWalker<MockInstrumentat
/// @brief bool, true when the mock function is duplicated imported
bool mockFunctionDuplicateImportedCheck() const noexcept;

///
/// @brief Add export function executeTestFunction()
///
void addExecuteTestFunction() noexcept;

///
/// @brief Main API for mock instrumentation
///
Expand Down
3 changes: 2 additions & 1 deletion instrumentation/wasm-instrumentation.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ interface Instrumenter {
debugInfoOutputFilePath: number,
includes: number,
excludes: number,
skipLib: boolean
skipLib: boolean,
collectCoverage: boolean
): void;
_free(ptr: number): void;
}
Expand Down
Loading
Loading