From a6d5784b20f586cbec0aec58e1ccc15ddaca7469 Mon Sep 17 00:00:00 2001 From: XMadrid Date: Wed, 11 Jun 2025 17:55:37 +0800 Subject: [PATCH 01/11] support listTestName transform --- bin/cli.js | 7 +- src/core/precompile.ts | 64 +++-- src/index.ts | 3 +- src/interface.ts | 6 + src/type/global.d.ts | 2 + transform/listTestNames.mjs | 435 +++++++++++++++++++++++++++++ transform/listTestNames.mts | 529 ++++++++++++++++++++++++++++++++++++ transform/tsconfig.json | 2 +- 8 files changed, 1024 insertions(+), 24 deletions(-) create mode 100644 transform/listTestNames.mjs create mode 100644 transform/listTestNames.mts diff --git a/bin/cli.js b/bin/cli.js index 8134565..c529148 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -16,7 +16,8 @@ program .option("--temp ", "test template file folder") .option("--output ", "coverage report output folder") .option("--mode ", "test result output format") - .option("--coverageLimit [error warning...]", "set warn(yellow) and error(red) upper limit in coverage report"); + .option("--coverageLimit [error warning...]", "set warn(yellow) and error(red) upper limit in coverage report") + .option("--testNamePattern ", "run only tests with a name that matches the regex pattern"); program.parse(process.argv); const options = program.opts(); @@ -53,9 +54,11 @@ let outputFolder = options.output || config.output || "coverage"; let errorLimit = options.coverageLimit?.at(0); let warnLimit = options.coverageLimit?.at(1); +let testNamePattern = options.testNamePattern; + validatArgument(includes, excludes); start_unit_test( - { includes, excludes, testcases }, + { includes, excludes, testcases, testNamePattern }, { flags, imports }, { tempFolder, outputFolder, mode, warnLimit, errorLimit } ) diff --git a/src/core/precompile.ts b/src/core/precompile.ts index 1fe290b..d2a8378 100644 --- a/src/core/precompile.ts +++ b/src/core/precompile.ts @@ -9,34 +9,73 @@ import { getIncludeFiles } from "../utils/pathResolver.js"; import { SourceFunctionInfo, UnittestPackage } from "../interface.js"; import { projectRoot } from "../utils/projectRoot.js"; -const sourceFunctions = new Map(); export async function precompile( includes: string[], excludes: string[], testcases: string[] | undefined, - flags: string, - transformFunction = join(projectRoot, "transform", "listFunctions.mjs") + testNamePattern: string | undefined, + flags: string ): Promise { // if specify testcases, use testcases for unittest // otherwise, get testcases(*.test.ts) in includes directory const testCodePaths = testcases ?? getRelatedFiles(includes, excludes, (path: string) => path.endsWith(".test.ts")); - const sourceCodePaths = getRelatedFiles(includes, excludes, (path: string) => !path.endsWith(".test.ts")); + const matchedTestNames: string[] = []; + if (testNamePattern) { + const testNameInfos = new Map(); + const testNameTransformFunction = join(projectRoot, "transform", "listTestNames.mjs"); + for (const testCodePath of testCodePaths) { + await transform(testNameTransformFunction, testCodePath, flags, () => { + testNameInfos.set(testCodePath, testNames); + }); + } + const regexPattern = new RegExp(testNamePattern); + for (const [fileName, testNames] of testNameInfos) { + for (const testName of testNames) { + if (regexPattern.test(testName)) { + matchedTestNames.push(testName); + } + } + } + } + const sourceCodePaths = getRelatedFiles(includes, excludes, (path: string) => !path.endsWith(".test.ts")); + const sourceFunctions = new Map(); + const sourceTransformFunction = join(projectRoot, "transform", "listFunctions.mjs"); // The batchSize = 2 is empirical data after benchmarking const batchSize = 2; for (let i = 0; i < sourceCodePaths.length; i += batchSize) { await Promise.all( - sourceCodePaths.slice(i, i + batchSize).map((sourcePath) => transform(sourcePath, transformFunction, flags)) + sourceCodePaths.slice(i, i + batchSize).map((sourcePath) => + transform(sourceTransformFunction, sourcePath, flags, () => { + sourceFunctions.set(sourcePath, functionInfos); + }) + ) ); } return { testCodePaths, + matchedTestNames, sourceFunctions, }; } +async function transform(transformFunction: string, codePath: string, flags: string, collectCallback: () => void) { + let ascArgv = [codePath, "--noEmit", "--disableWarning", "--transform", transformFunction, "-O0"]; + if (flags) { + const argv = flags.split(" "); + ascArgv = ascArgv.concat(argv); + } + const { error, stderr } = await main(ascArgv); + if (error) { + // eslint-disable-next-line @typescript-eslint/no-base-to-string + console.error(stderr.toString()); + throw error; + } + collectCallback(); +} + // a. include in config // b. exclude in config export function getRelatedFiles(includes: string[], excludes: string[], filter: (path: string) => boolean) { @@ -58,18 +97,3 @@ export function getRelatedFiles(includes: string[], excludes: string[], filter: } return result; } - -async function transform(sourceCodePath: string, transformFunction: string, flags: string) { - let ascArgv = [sourceCodePath, "--noEmit", "--disableWarning", "--transform", transformFunction, "-O0"]; - if (flags) { - const argv = flags.split(" "); - ascArgv = ascArgv.concat(argv); - } - const { error, stderr } = await main(ascArgv); - if (error) { - // eslint-disable-next-line @typescript-eslint/no-base-to-string - console.error(stderr.toString()); - throw error; - } - sourceFunctions.set(sourceCodePath, functionInfos); -} diff --git a/src/index.ts b/src/index.ts index e6458cf..30e9bbf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -55,6 +55,7 @@ export interface FileOption { includes: string[]; excludes: string[]; testcases: string[] | undefined; + testNamePattern: string | undefined; } export interface TestOption { flags: string; @@ -75,7 +76,7 @@ export type OutputMode = "html" | "json" | "table"; export async function start_unit_test(fo: FileOption, to: TestOption, oo: OutputOption): Promise { emptydirSync(oo.outputFolder); emptydirSync(oo.tempFolder); - const unittestPackage = await precompile(fo.includes, fo.excludes, fo.testcases, to.flags); + const unittestPackage = await precompile(fo.includes, fo.excludes, fo.testcases, fo.testNamePattern, to.flags); console.log(chalk.blueBright("code analysis: ") + chalk.bold.greenBright("OK")); const wasmPaths = await compile(unittestPackage.testCodePaths, oo.tempFolder, to.flags); console.log(chalk.blueBright("compile testcases: ") + chalk.bold.greenBright("OK")); diff --git a/src/interface.ts b/src/interface.ts index 705a646..d9e5d43 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -150,6 +150,7 @@ export class CodeCoverage { export interface UnittestPackage { readonly testCodePaths: string[]; + readonly matchedTestNames: string[]; readonly sourceFunctions: Map; } @@ -158,5 +159,10 @@ export interface SourceFunctionInfo { range: [number, number]; } +export interface TestNameInfo { + testName: string; + testFilePath: string; +} + export const OrganizationName = "wasm-ecosystem"; export const Repository = "https://github.com/wasm-ecosystem/assemblyscript-unittest-framework"; diff --git a/src/type/global.d.ts b/src/type/global.d.ts index 36acbbd..c4caa07 100644 --- a/src/type/global.d.ts +++ b/src/type/global.d.ts @@ -3,6 +3,8 @@ import { SourceFunctionInfo } from "../interface.ts"; declare global { // store listFunctions transform results in global let functionInfos: SourceFunctionInfo[]; + // store listTestNames transform results in global + let testNames: string[]; } export {}; diff --git a/transform/listTestNames.mjs b/transform/listTestNames.mjs new file mode 100644 index 0000000..ad22cf4 --- /dev/null +++ b/transform/listTestNames.mjs @@ -0,0 +1,435 @@ +import { Transform } from "assemblyscript/transform"; +import assert from "node:assert"; +class SourceFunctionTransform extends Transform { + constructor() { + super(...arguments); + this.testNames = []; + this.currentTestDescriptions = []; + } + afterInitialize(program) { + // There will be two sources with SourceKind.UserEntry, ~lib/rt/index-incremental.ts should be filtered + const entrySource = program.sources.find((source) => source.sourceKind === 1 /* SourceKind.UserEntry */ && !source.normalizedPath.startsWith("~lib/")); + this.testFileName = entrySource.normalizedPath; + this.visitNode(entrySource); + globalThis.testNames = this.testNames; + } + visitNode(node) { + // eslint-disable-next-line sonarjs/max-switch-cases + switch (node.kind) { + case 0 /* NodeKind.Source */: { + this.visitSource(node); + break; + } + // types + case 1 /* NodeKind.NamedType */: + case 2 /* NodeKind.FunctionType */: + case 3 /* NodeKind.TypeName */: + case 4 /* NodeKind.TypeParameter */: { + break; + } + case 5 /* NodeKind.Parameter */: { + this.visitParameterNode(node); + break; + } + // Expressions + case 6 /* NodeKind.Identifier */: + case 13 /* NodeKind.False */: + case 16 /* NodeKind.Literal */: + case 18 /* NodeKind.Null */: + case 19 /* NodeKind.Omitted */: + case 23 /* NodeKind.Super */: + case 24 /* NodeKind.This */: + case 25 /* NodeKind.True */: + case 26 /* NodeKind.Constructor */: + case 29 /* NodeKind.Compiled */: { + break; + } + case 7 /* NodeKind.Assertion */: { + this.visitAssertionExpression(node); + break; + } + case 8 /* NodeKind.Binary */: { + this.visitBinaryExpression(node); + break; + } + case 9 /* NodeKind.Call */: { + this.visitCallExpression(node); + break; + } + case 10 /* NodeKind.Class */: { + this.visitClassExpression(node); + break; + } + case 11 /* NodeKind.Comma */: { + this.visitCommaExpression(node); + break; + } + case 12 /* NodeKind.ElementAccess */: { + this.visitElementAccessExpression(node); + break; + } + case 14 /* NodeKind.Function */: { + this.visitFunctionExpression(node); + break; + } + case 15 /* NodeKind.InstanceOf */: { + this.visitInstanceOfExpression(node); + break; + } + case 17 /* NodeKind.New */: { + this.visitNewExpression(node); + break; + } + case 20 /* NodeKind.Parenthesized */: { + this.visitParenthesizedExpression(node); + break; + } + case 21 /* NodeKind.PropertyAccess */: { + this.visitPropertyAccessExpression(node); + break; + } + case 22 /* NodeKind.Ternary */: { + this.visitTernaryExpression(node); + break; + } + case 27 /* NodeKind.UnaryPostfix */: { + this.visitUnaryPostfixExpression(node); + break; + } + case 28 /* NodeKind.UnaryPrefix */: { + this.visitUnaryPrefixExpression(node); + break; + } + // statements: + case 31 /* NodeKind.Break */: + case 34 /* NodeKind.Empty */: + case 35 /* NodeKind.Export */: + case 36 /* NodeKind.ExportDefault */: + case 37 /* NodeKind.ExportImport */: + case 32 /* NodeKind.Continue */: + case 42 /* NodeKind.Import */: + case 50 /* NodeKind.Module */: { + break; + } + case 30 /* NodeKind.Block */: { + this.visitBlockStatement(node); + break; + } + case 33 /* NodeKind.Do */: { + this.visitDoStatement(node); + break; + } + case 38 /* NodeKind.Expression */: { + this.visitExpressionStatement(node); + break; + } + case 39 /* NodeKind.For */: { + this.visitForStatement(node); + break; + } + case 40 /* NodeKind.ForOf */: { + this.visitForOfStatement(node); + break; + } + case 41 /* NodeKind.If */: { + this.visitIfStatement(node); + break; + } + case 43 /* NodeKind.Return */: { + this.visitReturnStatement(node); + break; + } + case 44 /* NodeKind.Switch */: { + this.visitSwitchStatement(node); + break; + } + case 45 /* NodeKind.Throw */: { + this.visitThrowStatement(node); + break; + } + case 46 /* NodeKind.Try */: { + this.visitTryStatement(node); + break; + } + case 47 /* NodeKind.Variable */: { + this.visitVariableStatement(node); + break; + } + case 48 /* NodeKind.Void */: { + this.visitVoidStatement(node); + break; + } + case 49 /* NodeKind.While */: { + this.visitWhileStatement(node); + break; + } + // declaration statements + case 56 /* NodeKind.ImportDeclaration */: + case 60 /* NodeKind.TypeDeclaration */: { + break; + } + case 51 /* NodeKind.ClassDeclaration */: { + this.visitClassDeclaration(node); + break; + } + case 52 /* NodeKind.EnumDeclaration */: { + this.visitEnumDeclaration(node); + break; + } + case 53 /* NodeKind.EnumValueDeclaration */: { + this.visitEnumValueDeclaration(node); + break; + } + case 54 /* NodeKind.FieldDeclaration */: { + this.visitFieldDeclaration(node); + break; + } + case 55 /* NodeKind.FunctionDeclaration */: { + this.visitFunctionDeclaration(node); + break; + } + case 57 /* NodeKind.InterfaceDeclaration */: { + this.visitInterfaceDeclaration(node); + break; + } + case 58 /* NodeKind.MethodDeclaration */: { + this.visitMethodDeclaration(node); + break; + } + case 59 /* NodeKind.NamespaceDeclaration */: { + this.visitNamespaceDeclaration(node); + break; + } + case 61 /* NodeKind.VariableDeclaration */: { + this.visitVariableDeclaration(node); + break; + } + // special + case 63 /* NodeKind.ExportMember */: + case 65 /* NodeKind.IndexSignature */: + case 66 /* NodeKind.Comment */: + case 62 /* NodeKind.Decorator */: { + break; + } + case 64 /* NodeKind.SwitchCase */: { + this.visitSwitchCase(node); + break; + } + } + } + visitSource(node) { + for (const statement of node.statements) { + this.visitNode(statement); + } + } + visitParameterNode(node) { + if (node.initializer) { + this.visitNode(node.initializer); + } + } + visitAssertionExpression(node) { + this.visitNode(node.expression); + } + visitBinaryExpression(node) { + this.visitNode(node.left); + this.visitNode(node.right); + } + visitCallExpression(node) { + if (node.expression.kind === 6 /* NodeKind.Identifier */) { + const fncName = node.expression.text; + if (fncName === "describe" || fncName === "test") { + assert(node.args.length === 2); + assert(node.args[0].kind === 16 /* NodeKind.Literal */ && + node.args[0].literalKind === 2 /* LiteralKind.String */); + const testName = node.args[0].value; + this.currentTestDescriptions.push(testName); + if (fncName === "test") { + this.testNames.push(this.currentTestDescriptions.join("")); + } + this.visitNode(node.expression); + for (const arg of node.args) { + this.visitNode(arg); + } + this.currentTestDescriptions.pop(); + } + } + else { + this.visitNode(node.expression); + for (const arg of node.args) { + this.visitNode(arg); + } + } + } + visitClassExpression(node) { + this.visitClassDeclaration(node.declaration); + } + visitCommaExpression(node) { + for (const expr of node.expressions) { + this.visitNode(expr); + } + } + visitElementAccessExpression(node) { + this.visitNode(node.expression); + this.visitNode(node.elementExpression); + } + visitFunctionExpression(node) { + this.visitFunctionDeclaration(node.declaration); + } + visitInstanceOfExpression(node) { + this.visitNode(node.expression); + } + visitNewExpression(node) { + for (const arg of node.args) { + this.visitNode(arg); + } + } + visitParenthesizedExpression(node) { + this.visitNode(node.expression); + } + visitPropertyAccessExpression(node) { + this.visitNode(node.expression); + } + visitTernaryExpression(node) { + this.visitNode(node.condition); + this.visitNode(node.ifThen); + this.visitNode(node.ifElse); + } + visitUnaryPostfixExpression(node) { + this.visitNode(node.operand); + } + visitUnaryPrefixExpression(node) { + this.visitNode(node.operand); + } + // eslint-disable-next-line sonarjs/no-identical-functions + visitBlockStatement(node) { + for (const statement of node.statements) { + this.visitNode(statement); + } + } + visitDoStatement(node) { + this.visitNode(node.body); + this.visitNode(node.condition); + } + visitExpressionStatement(node) { + this.visitNode(node.expression); + } + visitForStatement(node) { + if (node.initializer) { + this.visitNode(node.initializer); + } + if (node.condition) { + this.visitNode(node.condition); + } + if (node.incrementor) { + this.visitNode(node.incrementor); + } + this.visitNode(node.body); + } + visitForOfStatement(node) { + this.visitNode(node.variable); + this.visitNode(node.iterable); + this.visitNode(node.body); + } + visitIfStatement(node) { + this.visitNode(node.condition); + this.visitNode(node.ifTrue); + if (node.ifFalse) { + this.visitNode(node.ifFalse); + } + } + visitReturnStatement(node) { + if (node.value) { + this.visitNode(node.value); + } + } + visitSwitchStatement(node) { + this.visitNode(node.condition); + for (const switchCase of node.cases) { + this.visitSwitchCase(switchCase); + } + } + visitThrowStatement(node) { + this.visitNode(node.value); + } + visitTryStatement(node) { + for (const stat of node.bodyStatements) { + this.visitNode(stat); + } + if (node.catchStatements) { + for (const stat of node.catchStatements) { + this.visitNode(stat); + } + } + if (node.finallyStatements) { + for (const stat of node.finallyStatements) { + this.visitNode(stat); + } + } + } + visitVariableStatement(node) { + for (const declaration of node.declarations) { + this.visitVariableDeclaration(declaration); + } + } + visitVoidStatement(node) { + this.visitNode(node.expression); + } + visitWhileStatement(node) { + this.visitNode(node.condition); + this.visitNode(node.body); + } + visitClassDeclaration(node) { + for (const member of node.members) { + this.visitNode(member); + } + } + visitEnumDeclaration(node) { + for (const value of node.values) { + this.visitEnumValueDeclaration(value); + } + } + // eslint-disable-next-line sonarjs/no-identical-functions + visitEnumValueDeclaration(node) { + if (node.initializer) { + this.visitNode(node.initializer); + } + } + // eslint-disable-next-line sonarjs/no-identical-functions + visitFieldDeclaration(node) { + if (node.initializer) { + this.visitNode(node.initializer); + } + } + visitFunctionDeclaration(node) { + if (node.body) { + this.visitNode(node.body); + } + } + visitInterfaceDeclaration(node) { + this.visitClassDeclaration(node); + } + visitMethodDeclaration(node) { + this.visitFunctionDeclaration(node); + } + // eslint-disable-next-line sonarjs/no-identical-functions + visitNamespaceDeclaration(node) { + for (const member of node.members) { + this.visitNode(member); + } + } + // eslint-disable-next-line sonarjs/no-identical-functions + visitVariableDeclaration(node) { + if (node.initializer) { + this.visitNode(node.initializer); + } + } + visitSwitchCase(node) { + if (node.label) { + this.visitNode(node.label); + } + for (const stat of node.statements) { + this.visitNode(stat); + } + } +} +export default SourceFunctionTransform; diff --git a/transform/listTestNames.mts b/transform/listTestNames.mts new file mode 100644 index 0000000..5d95f03 --- /dev/null +++ b/transform/listTestNames.mts @@ -0,0 +1,529 @@ +import { Transform } from "assemblyscript/transform"; +import { + NodeKind, + SourceKind, + Node, + Source, + ParameterNode, + AssertionExpression, + BinaryExpression, + CallExpression, + ClassExpression, + CommaExpression, + ElementAccessExpression, + FunctionExpression, + InstanceOfExpression, + NewExpression, + ParenthesizedExpression, + PropertyAccessExpression, + TernaryExpression, + UnaryPostfixExpression, + UnaryPrefixExpression, + BlockStatement, + DoStatement, + ExpressionStatement, + ForStatement, + ForOfStatement, + IfStatement, + ReturnStatement, + SwitchStatement, + ThrowStatement, + TryStatement, + VariableStatement, + VoidStatement, + WhileStatement, + ClassDeclaration, + EnumDeclaration, + EnumValueDeclaration, + FieldDeclaration, + FunctionDeclaration, + InterfaceDeclaration, + MethodDeclaration, + NamespaceDeclaration, + VariableDeclaration, + SwitchCase, + Program, + IdentifierExpression, + LiteralExpression, + LiteralKind, + StringLiteralExpression, +} from "assemblyscript"; +import assert from "node:assert"; + +class SourceFunctionTransform extends Transform { + testNames: string[] = []; + currentTestDescriptions: string[] = []; + testFileName: string; + + afterInitialize(program: Program) { + // There will be two sources with SourceKind.UserEntry, ~lib/rt/index-incremental.ts should be filtered + const entrySource = program.sources.find( + (source) => source.sourceKind === SourceKind.UserEntry && !source.normalizedPath.startsWith("~lib/") + ); + this.testFileName = entrySource.normalizedPath; + this.visitNode(entrySource); + globalThis.testNames = this.testNames; + } + + visitNode(node: Node) { + // eslint-disable-next-line sonarjs/max-switch-cases + switch (node.kind) { + case NodeKind.Source: { + this.visitSource(node as Source); + break; + } + + // types + case NodeKind.NamedType: + case NodeKind.FunctionType: + case NodeKind.TypeName: + case NodeKind.TypeParameter: { + break; + } + case NodeKind.Parameter: { + this.visitParameterNode(node as ParameterNode); + break; + } + + // Expressions + case NodeKind.Identifier: + case NodeKind.False: + case NodeKind.Literal: + case NodeKind.Null: + case NodeKind.Omitted: + case NodeKind.Super: + case NodeKind.This: + case NodeKind.True: + case NodeKind.Constructor: + case NodeKind.Compiled: { + break; + } + case NodeKind.Assertion: { + this.visitAssertionExpression(node as AssertionExpression); + break; + } + case NodeKind.Binary: { + this.visitBinaryExpression(node as BinaryExpression); + break; + } + case NodeKind.Call: { + this.visitCallExpression(node as CallExpression); + break; + } + case NodeKind.Class: { + this.visitClassExpression(node as ClassExpression); + break; + } + case NodeKind.Comma: { + this.visitCommaExpression(node as CommaExpression); + break; + } + case NodeKind.ElementAccess: { + this.visitElementAccessExpression(node as ElementAccessExpression); + break; + } + case NodeKind.Function: { + this.visitFunctionExpression(node as FunctionExpression); + break; + } + case NodeKind.InstanceOf: { + this.visitInstanceOfExpression(node as InstanceOfExpression); + break; + } + case NodeKind.New: { + this.visitNewExpression(node as NewExpression); + break; + } + case NodeKind.Parenthesized: { + this.visitParenthesizedExpression(node as ParenthesizedExpression); + break; + } + case NodeKind.PropertyAccess: { + this.visitPropertyAccessExpression(node as PropertyAccessExpression); + break; + } + case NodeKind.Ternary: { + this.visitTernaryExpression(node as TernaryExpression); + break; + } + case NodeKind.UnaryPostfix: { + this.visitUnaryPostfixExpression(node as UnaryPostfixExpression); + break; + } + case NodeKind.UnaryPrefix: { + this.visitUnaryPrefixExpression(node as UnaryPrefixExpression); + break; + } + + // statements: + + case NodeKind.Break: + case NodeKind.Empty: + case NodeKind.Export: + case NodeKind.ExportDefault: + case NodeKind.ExportImport: + case NodeKind.Continue: + case NodeKind.Import: + case NodeKind.Module: { + break; + } + case NodeKind.Block: { + this.visitBlockStatement(node as BlockStatement); + break; + } + case NodeKind.Do: { + this.visitDoStatement(node as DoStatement); + break; + } + case NodeKind.Expression: { + this.visitExpressionStatement(node as ExpressionStatement); + break; + } + case NodeKind.For: { + this.visitForStatement(node as ForStatement); + break; + } + case NodeKind.ForOf: { + this.visitForOfStatement(node as ForOfStatement); + break; + } + case NodeKind.If: { + this.visitIfStatement(node as IfStatement); + break; + } + case NodeKind.Return: { + this.visitReturnStatement(node as ReturnStatement); + break; + } + case NodeKind.Switch: { + this.visitSwitchStatement(node as SwitchStatement); + break; + } + case NodeKind.Throw: { + this.visitThrowStatement(node as ThrowStatement); + break; + } + case NodeKind.Try: { + this.visitTryStatement(node as TryStatement); + break; + } + case NodeKind.Variable: { + this.visitVariableStatement(node as VariableStatement); + break; + } + case NodeKind.Void: { + this.visitVoidStatement(node as VoidStatement); + break; + } + case NodeKind.While: { + this.visitWhileStatement(node as WhileStatement); + break; + } + + // declaration statements + case NodeKind.ImportDeclaration: + case NodeKind.TypeDeclaration: { + break; + } + case NodeKind.ClassDeclaration: { + this.visitClassDeclaration(node as ClassDeclaration); + break; + } + case NodeKind.EnumDeclaration: { + this.visitEnumDeclaration(node as EnumDeclaration); + break; + } + case NodeKind.EnumValueDeclaration: { + this.visitEnumValueDeclaration(node as EnumValueDeclaration); + break; + } + case NodeKind.FieldDeclaration: { + this.visitFieldDeclaration(node as FieldDeclaration); + break; + } + case NodeKind.FunctionDeclaration: { + this.visitFunctionDeclaration(node as FunctionDeclaration); + break; + } + case NodeKind.InterfaceDeclaration: { + this.visitInterfaceDeclaration(node as InterfaceDeclaration); + break; + } + case NodeKind.MethodDeclaration: { + this.visitMethodDeclaration(node as MethodDeclaration); + break; + } + case NodeKind.NamespaceDeclaration: { + this.visitNamespaceDeclaration(node as NamespaceDeclaration); + break; + } + case NodeKind.VariableDeclaration: { + this.visitVariableDeclaration(node as VariableDeclaration); + break; + } + + // special + case NodeKind.ExportMember: + case NodeKind.IndexSignature: + case NodeKind.Comment: + case NodeKind.Decorator: { + break; + } + case NodeKind.SwitchCase: { + this.visitSwitchCase(node as SwitchCase); + break; + } + } + } + + visitSource(node: Source) { + for (const statement of node.statements) { + this.visitNode(statement); + } + } + + visitParameterNode(node: ParameterNode) { + if (node.initializer) { + this.visitNode(node.initializer); + } + } + + visitAssertionExpression(node: AssertionExpression) { + this.visitNode(node.expression); + } + + visitBinaryExpression(node: BinaryExpression) { + this.visitNode(node.left); + this.visitNode(node.right); + } + + visitCallExpression(node: CallExpression) { + if (node.expression.kind === NodeKind.Identifier) { + const fncName = (node.expression as IdentifierExpression).text; + if (fncName === "describe" || fncName === "test") { + assert(node.args.length === 2); + assert( + node.args[0].kind === NodeKind.Literal && + (node.args[0] as LiteralExpression).literalKind === LiteralKind.String + ); + const testName = (node.args[0] as StringLiteralExpression).value; + this.currentTestDescriptions.push(testName); + if (fncName === "test") { + this.testNames.push(this.currentTestDescriptions.join("")); + } + this.visitNode(node.expression); + for (const arg of node.args) { + this.visitNode(arg); + } + this.currentTestDescriptions.pop(); + } + } else { + this.visitNode(node.expression); + for (const arg of node.args) { + this.visitNode(arg); + } + } + } + + visitClassExpression(node: ClassExpression) { + this.visitClassDeclaration(node.declaration); + } + + visitCommaExpression(node: CommaExpression) { + for (const expr of node.expressions) { + this.visitNode(expr); + } + } + + visitElementAccessExpression(node: ElementAccessExpression) { + this.visitNode(node.expression); + this.visitNode(node.elementExpression); + } + + visitFunctionExpression(node: FunctionExpression) { + this.visitFunctionDeclaration(node.declaration); + } + + visitInstanceOfExpression(node: InstanceOfExpression) { + this.visitNode(node.expression); + } + + visitNewExpression(node: NewExpression) { + for (const arg of node.args) { + this.visitNode(arg); + } + } + + visitParenthesizedExpression(node: ParenthesizedExpression) { + this.visitNode(node.expression); + } + + visitPropertyAccessExpression(node: PropertyAccessExpression) { + this.visitNode(node.expression); + } + + visitTernaryExpression(node: TernaryExpression) { + this.visitNode(node.condition); + this.visitNode(node.ifThen); + this.visitNode(node.ifElse); + } + + visitUnaryPostfixExpression(node: UnaryPostfixExpression) { + this.visitNode(node.operand); + } + + visitUnaryPrefixExpression(node: UnaryPrefixExpression) { + this.visitNode(node.operand); + } + // eslint-disable-next-line sonarjs/no-identical-functions + visitBlockStatement(node: BlockStatement) { + for (const statement of node.statements) { + this.visitNode(statement); + } + } + + visitDoStatement(node: DoStatement) { + this.visitNode(node.body); + this.visitNode(node.condition); + } + + visitExpressionStatement(node: ExpressionStatement) { + this.visitNode(node.expression); + } + + visitForStatement(node: ForStatement) { + if (node.initializer) { + this.visitNode(node.initializer); + } + if (node.condition) { + this.visitNode(node.condition); + } + if (node.incrementor) { + this.visitNode(node.incrementor); + } + this.visitNode(node.body); + } + + visitForOfStatement(node: ForOfStatement) { + this.visitNode(node.variable); + this.visitNode(node.iterable); + this.visitNode(node.body); + } + + visitIfStatement(node: IfStatement) { + this.visitNode(node.condition); + this.visitNode(node.ifTrue); + if (node.ifFalse) { + this.visitNode(node.ifFalse); + } + } + + visitReturnStatement(node: ReturnStatement) { + if (node.value) { + this.visitNode(node.value); + } + } + + visitSwitchStatement(node: SwitchStatement) { + this.visitNode(node.condition); + for (const switchCase of node.cases) { + this.visitSwitchCase(switchCase); + } + } + + visitThrowStatement(node: ThrowStatement) { + this.visitNode(node.value); + } + + visitTryStatement(node: TryStatement) { + for (const stat of node.bodyStatements) { + this.visitNode(stat); + } + if (node.catchStatements) { + for (const stat of node.catchStatements) { + this.visitNode(stat); + } + } + if (node.finallyStatements) { + for (const stat of node.finallyStatements) { + this.visitNode(stat); + } + } + } + + visitVariableStatement(node: VariableStatement) { + for (const declaration of node.declarations) { + this.visitVariableDeclaration(declaration); + } + } + + visitVoidStatement(node: VoidStatement) { + this.visitNode(node.expression); + } + + visitWhileStatement(node: WhileStatement) { + this.visitNode(node.condition); + this.visitNode(node.body); + } + + visitClassDeclaration(node: ClassDeclaration) { + for (const member of node.members) { + this.visitNode(member); + } + } + + visitEnumDeclaration(node: EnumDeclaration) { + for (const value of node.values) { + this.visitEnumValueDeclaration(value); + } + } + // eslint-disable-next-line sonarjs/no-identical-functions + visitEnumValueDeclaration(node: EnumValueDeclaration) { + if (node.initializer) { + this.visitNode(node.initializer); + } + } + // eslint-disable-next-line sonarjs/no-identical-functions + visitFieldDeclaration(node: FieldDeclaration) { + if (node.initializer) { + this.visitNode(node.initializer); + } + } + + visitFunctionDeclaration(node: FunctionDeclaration) { + if (node.body) { + this.visitNode(node.body); + } + } + + visitInterfaceDeclaration(node: InterfaceDeclaration) { + this.visitClassDeclaration(node); + } + + visitMethodDeclaration(node: MethodDeclaration) { + this.visitFunctionDeclaration(node); + } + // eslint-disable-next-line sonarjs/no-identical-functions + visitNamespaceDeclaration(node: NamespaceDeclaration) { + for (const member of node.members) { + this.visitNode(member); + } + } + // eslint-disable-next-line sonarjs/no-identical-functions + visitVariableDeclaration(node: VariableDeclaration) { + if (node.initializer) { + this.visitNode(node.initializer); + } + } + + visitSwitchCase(node: SwitchCase) { + if (node.label) { + this.visitNode(node.label); + } + for (const stat of node.statements) { + this.visitNode(stat); + } + } +} + +export default SourceFunctionTransform; diff --git a/transform/tsconfig.json b/transform/tsconfig.json index 6bce599..1bfcd67 100644 --- a/transform/tsconfig.json +++ b/transform/tsconfig.json @@ -10,6 +10,6 @@ "assemblyscript/transform": ["../node_modules/assemblyscript/dist/transform"] } }, - "files": ["listFunctions.mts"], + "files": ["listFunctions.mts", "listTestNames.mts"], "exclude": [] } From ccca0a326aa1962eca64c8056594497bddc3344b Mon Sep 17 00:00:00 2001 From: XMadrid Date: Mon, 16 Jun 2025 15:49:09 +0800 Subject: [PATCH 02/11] support collectCoverage options --- bin/cli.js | 50 ++++---- instrumentation/CoverageInstru.cpp | 132 ++++++++++++---------- instrumentation/CoverageInstru.hpp | 6 +- instrumentation/wasm-instrumentation.d.ts | 3 +- src/core/execute.ts | 9 +- src/core/executionRecorder.ts | 3 +- src/core/instrument.ts | 19 +++- src/core/precompile.ts | 47 ++++---- src/generator/index.ts | 3 +- src/index.ts | 69 +++++------ src/interface.ts | 32 +++++- 11 files changed, 211 insertions(+), 162 deletions(-) diff --git a/bin/cli.js b/bin/cli.js index c529148..1fa4964 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -12,21 +12,17 @@ import { validatArgument, start_unit_test } from "../dist/index.js"; const program = new Command(); program .option("--config ", "path of config file", "as-test.config.js") - .option("--testcase ", "only run specified test cases") + .option("--testcase ", "run only specified test cases") .option("--temp ", "test template file folder") .option("--output ", "coverage report output folder") - .option("--mode ", "test result output format") + .option("--mode ", "coverage report output format") .option("--coverageLimit [error warning...]", "set warn(yellow) and error(red) upper limit in coverage report") - .option("--testNamePattern ", "run only tests with a name that matches the regex pattern"); + .option("--testNamePattern ", "run only tests with a name that matches the regex pattern") + .option("--collectCoverage ", "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"); @@ -35,33 +31,35 @@ 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"); diff --git a/instrumentation/CoverageInstru.cpp b/instrumentation/CoverageInstru.cpp index d56d496..afb07c8 100644 --- a/instrumentation/CoverageInstru.cpp +++ b/instrumentation/CoverageInstru.cpp @@ -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; @@ -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 &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(-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 &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(-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 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 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(); @@ -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; @@ -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(); } diff --git a/instrumentation/CoverageInstru.hpp b/instrumentation/CoverageInstru.hpp index 80e8240..340b1e7 100644 --- a/instrumentation/CoverageInstru.hpp +++ b/instrumentation/CoverageInstru.hpp @@ -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 @@ -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; } }; @@ -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 diff --git a/instrumentation/wasm-instrumentation.d.ts b/instrumentation/wasm-instrumentation.d.ts index e96b4ad..c8153dc 100644 --- a/instrumentation/wasm-instrumentation.d.ts +++ b/instrumentation/wasm-instrumentation.d.ts @@ -11,7 +11,8 @@ interface Instrumenter { debugInfoOutputFilePath: number, includes: number, excludes: number, - skipLib: boolean + skipLib: boolean, + collectCoverage: boolean ): void; _free(ptr: number): void; } diff --git a/src/core/execute.ts b/src/core/execute.ts index 2e8a39b..846b457 100644 --- a/src/core/execute.ts +++ b/src/core/execute.ts @@ -4,8 +4,7 @@ import { ensureDirSync } from "fs-extra"; import { basename } from "node:path"; import { instantiate, Imports as ASImports } from "@assemblyscript/loader"; import { AssertResult } from "../assertResult.js"; -import { Imports, ImportsArgument } from "../index.js"; -import { InstrumentResult } from "../interface.js"; +import { InstrumentResult, Imports, ImportsArgument } from "../interface.js"; import { mockInstruFunc, covInstruFunc } from "../utils/import.js"; import { supplyDefaultFunction } from "../utils/index.js"; import { parseImportFunctionInfo } from "../utils/wasmparser.js"; @@ -13,7 +12,7 @@ import { ExecutionRecorder } from "./executionRecorder.js"; const readFile = promises.readFile; -async function nodeExecutor(wasm: string, outFolder: string, imports: Imports): Promise { +async function nodeExecutor(wasm: string, outFolder: string, imports?: Imports): Promise { const wasi = new WASI({ args: ["node", basename(wasm)], env: process.env, @@ -26,7 +25,7 @@ async function nodeExecutor(wasm: string, outFolder: string, imports: Imports): const recorder = new ExecutionRecorder(); const importsArg = new ImportsArgument(); - const userDefinedImportsObject = imports === null ? {} : imports(importsArg); + const userDefinedImportsObject = imports === undefined ? {} : imports!(importsArg); const importObject: ASImports = { wasi_snapshot_preview1: wasi.wasiImport, ...recorder.getCollectionFuncSet(importsArg), @@ -56,7 +55,7 @@ async function nodeExecutor(wasm: string, outFolder: string, imports: Imports): export async function execWasmBinarys( outFolder: string, instrumentResult: InstrumentResult[], - imports: Imports + imports?: Imports ): Promise { const assertRes = new AssertResult(); ensureDirSync(outFolder); diff --git a/src/core/executionRecorder.ts b/src/core/executionRecorder.ts index a55219e..b899a9d 100644 --- a/src/core/executionRecorder.ts +++ b/src/core/executionRecorder.ts @@ -1,5 +1,4 @@ -import { ImportsArgument } from "../index.js"; -import { AssertFailMessage, AssertMessage, IAssertResult } from "../interface.js"; +import { AssertFailMessage, AssertMessage, IAssertResult, ImportsArgument } from "../interface.js"; export class ExecutionRecorder implements IAssertResult { total: number = 0; diff --git a/src/core/instrument.ts b/src/core/instrument.ts index b8cff25..728a57f 100644 --- a/src/core/instrument.ts +++ b/src/core/instrument.ts @@ -1,7 +1,11 @@ import initInstrumenter from "../../build_wasm/bin/wasm-instrumentation.js"; import { InstrumentResult } from "../interface.js"; -export async function instrument(sourceWasms: string[], sourceCodePaths: string[]): Promise { +export async function instrument( + sourceWasms: string[], + sourceCodePaths: string[], + collectCoverage: boolean +): Promise { const includeRegexs = sourceCodePaths.map((path) => { return `(start:)?${path.slice(0, -3)}.*`; }); @@ -24,7 +28,18 @@ export async function instrument(sourceWasms: string[], sourceCodePaths: string[ const expectInfo = instrumenter.allocateUTF8(expectInfoFile); const include = instrumenter.allocateUTF8(includeFilter); - instrumenter._wasm_instrument(source, output, report, sourceMap, expectInfo, debugInfo, include, 0, true); + instrumenter._wasm_instrument( + source, + output, + report, + sourceMap, + expectInfo, + debugInfo, + include, + 0, + true, + collectCoverage + ); const result: InstrumentResult = { sourceWasm: sourceFile, instrumentedWasm: outputFile, diff --git a/src/core/precompile.ts b/src/core/precompile.ts index d2a8378..8ec7bf3 100644 --- a/src/core/precompile.ts +++ b/src/core/precompile.ts @@ -14,14 +14,16 @@ export async function precompile( excludes: string[], testcases: string[] | undefined, testNamePattern: string | undefined, + collectCoverage: boolean, flags: string ): Promise { // if specify testcases, use testcases for unittest // otherwise, get testcases(*.test.ts) in includes directory const testCodePaths = testcases ?? getRelatedFiles(includes, excludes, (path: string) => path.endsWith(".test.ts")); - const matchedTestNames: string[] = []; if (testNamePattern) { + const matchedTestNames: string[] = []; + const matchedTestFiles = new Set(); const testNameInfos = new Map(); const testNameTransformFunction = join(projectRoot, "transform", "listTestNames.mjs"); for (const testCodePath of testCodePaths) { @@ -34,31 +36,38 @@ export async function precompile( for (const testName of testNames) { if (regexPattern.test(testName)) { matchedTestNames.push(testName); + matchedTestFiles.add(fileName); } } } + return { + testCodePaths: Array.from(matchedTestFiles), + matchedTestNames: matchedTestNames, + }; } - const sourceCodePaths = getRelatedFiles(includes, excludes, (path: string) => !path.endsWith(".test.ts")); - const sourceFunctions = new Map(); - const sourceTransformFunction = join(projectRoot, "transform", "listFunctions.mjs"); - // The batchSize = 2 is empirical data after benchmarking - const batchSize = 2; - for (let i = 0; i < sourceCodePaths.length; i += batchSize) { - await Promise.all( - sourceCodePaths.slice(i, i + batchSize).map((sourcePath) => - transform(sourceTransformFunction, sourcePath, flags, () => { - sourceFunctions.set(sourcePath, functionInfos); - }) - ) - ); + if (collectCoverage) { + const sourceFunctions = new Map(); + const sourceCodePaths = getRelatedFiles(includes, excludes, (path: string) => !path.endsWith(".test.ts")); + const sourceTransformFunction = join(projectRoot, "transform", "listFunctions.mjs"); + // The batchSize = 2 is empirical data after benchmarking + const batchSize = 2; + for (let i = 0; i < sourceCodePaths.length; i += batchSize) { + await Promise.all( + sourceCodePaths.slice(i, i + batchSize).map((sourcePath) => + transform(sourceTransformFunction, sourcePath, flags, () => { + sourceFunctions.set(sourcePath, functionInfos); + }) + ) + ); + } + return { + testCodePaths, + sourceFunctions, + }; } - return { - testCodePaths, - matchedTestNames, - sourceFunctions, - }; + return { testCodePaths }; } async function transform(transformFunction: string, codePath: string, flags: string, collectCallback: () => void) { diff --git a/src/generator/index.ts b/src/generator/index.ts index 29b100f..af8ad6b 100644 --- a/src/generator/index.ts +++ b/src/generator/index.ts @@ -1,5 +1,4 @@ -import { OutputMode } from "../index.js"; -import { FileCoverageResult } from "../interface.js"; +import { OutputMode, FileCoverageResult } from "../interface.js"; import { genHtml } from "./html-generator/index.js"; import { genJson } from "./json-generator/index.js"; import { genTable } from "./table-generator/index.js"; diff --git a/src/index.ts b/src/index.ts index 30e9bbf..fccc32f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,5 @@ import chalk from "chalk"; import { emptydirSync } from "fs-extra"; -import { ASUtil } from "@assemblyscript/loader"; import { Parser } from "./parser/index.js"; import { compile } from "./core/compile.js"; import { AssertResult } from "./assertResult.js"; @@ -8,6 +7,7 @@ import { precompile } from "./core/precompile.js"; import { instrument } from "./core/instrument.js"; import { execWasmBinarys } from "./core/execute.js"; import { generateReport, reportConfig } from "./generator/index.js"; +import { TestOption } from "./interface.js"; function logAssertResult(trace: AssertResult): void { const render = (failed: number, total: number) => @@ -43,54 +43,41 @@ export function validatArgument(includes: unknown, excludes: unknown) { } } -export class ImportsArgument { - module: WebAssembly.Module | null = null; - instance: WebAssembly.Instance | null = null; - exports: (ASUtil & Record) | null = null; -} - -export type Imports = ((arg: ImportsArgument) => Record) | null; - -export interface FileOption { - includes: string[]; - excludes: string[]; - testcases: string[] | undefined; - testNamePattern: string | undefined; -} -export interface TestOption { - flags: string; - imports: Imports; -} -export interface OutputOption { - tempFolder: string; - outputFolder: string; - mode: OutputMode | OutputMode[]; - warnLimit?: number; - errorLimit?: number; -} -export type OutputMode = "html" | "json" | "table"; - /** * main function of unit-test, will throw Exception in most condition except job carsh */ -export async function start_unit_test(fo: FileOption, to: TestOption, oo: OutputOption): Promise { - emptydirSync(oo.outputFolder); - emptydirSync(oo.tempFolder); - const unittestPackage = await precompile(fo.includes, fo.excludes, fo.testcases, fo.testNamePattern, to.flags); +export async function start_unit_test(options: TestOption): Promise { + emptydirSync(options.outputFolder); + emptydirSync(options.tempFolder); + const unittestPackage = await precompile( + options.includes, + options.excludes, + options.testcases, + options.testNamePattern, + options.collectCoverage, + options.flags + ); console.log(chalk.blueBright("code analysis: ") + chalk.bold.greenBright("OK")); - const wasmPaths = await compile(unittestPackage.testCodePaths, oo.tempFolder, to.flags); + + const wasmPaths = await compile(unittestPackage.testCodePaths, options.tempFolder, options.flags); console.log(chalk.blueBright("compile testcases: ") + chalk.bold.greenBright("OK")); - const instrumentResult = await instrument(wasmPaths, Array.from(unittestPackage.sourceFunctions.keys())); + + const sourcePaths = unittestPackage.sourceFunctions ? Array.from(unittestPackage.sourceFunctions.keys()) : []; + const instrumentResult = await instrument(wasmPaths, sourcePaths, options.collectCoverage); console.log(chalk.blueBright("instrument: ") + chalk.bold.greenBright("OK")); - const executedResult = await execWasmBinarys(oo.tempFolder, instrumentResult, to.imports); + + const executedResult = await execWasmBinarys(options.tempFolder, instrumentResult, options.imports); console.log(chalk.blueBright("execute testcases: ") + chalk.bold.greenBright("OK")); + logAssertResult(executedResult); - const debugInfoFiles = instrumentResult.map((res) => res.debugInfo); - const parser = new Parser(); - const fileCoverageInfo = await parser.parse(debugInfoFiles, unittestPackage.sourceFunctions); - reportConfig.warningLimit = oo.warnLimit ?? reportConfig.warningLimit; - reportConfig.errorLimit = oo.errorLimit ?? reportConfig.errorLimit; - generateReport(oo.mode, oo.outputFolder, fileCoverageInfo); + if (options.collectCoverage) { + const debugInfoFiles = instrumentResult.map((res) => res.debugInfo); + const parser = new Parser(); + const fileCoverageInfo = await parser.parse(debugInfoFiles, unittestPackage.sourceFunctions!); + reportConfig.warningLimit = options.warnLimit || reportConfig.warningLimit; + reportConfig.errorLimit = options.errorLimit || reportConfig.errorLimit; + generateReport(options.mode, options.outputFolder, fileCoverageInfo); + } return executedResult.fail === 0; } diff --git a/src/interface.ts b/src/interface.ts index d9e5d43..946e34f 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -4,6 +4,7 @@ // input import { Type } from "wasmparser"; +import { ASUtil } from "@assemblyscript/loader"; // instrumented file information export interface InstrumentResult { @@ -150,8 +151,8 @@ export class CodeCoverage { export interface UnittestPackage { readonly testCodePaths: string[]; - readonly matchedTestNames: string[]; - readonly sourceFunctions: Map; + readonly matchedTestNames?: string[]; + readonly sourceFunctions?: Map; } export interface SourceFunctionInfo { @@ -164,5 +165,32 @@ export interface TestNameInfo { testFilePath: string; } +export class ImportsArgument { + module: WebAssembly.Module | null = null; + instance: WebAssembly.Instance | null = null; + exports: (ASUtil & Record) | null = null; +} + +export type Imports = ((arg: ImportsArgument) => Record) | null; + +export interface TestOption { + includes: string[]; + excludes: string[]; + testcases?: string[]; + testNamePattern?: string; + collectCoverage: boolean; + + flags: string; + imports?: Imports; + + tempFolder: string; + outputFolder: string; + mode: OutputMode | OutputMode[]; + warnLimit?: number; + errorLimit?: number; +} + +export type OutputMode = "html" | "json" | "table"; + export const OrganizationName = "wasm-ecosystem"; export const Repository = "https://github.com/wasm-ecosystem/assemblyscript-unittest-framework"; From 8ba11558aa0ffb9c2ca1b1ee4cbd99d094e125dc Mon Sep 17 00:00:00 2001 From: XMadrid Date: Tue, 17 Jun 2025 10:59:05 +0800 Subject: [PATCH 03/11] wip --- assembly/env.ts | 4 ++++ assembly/implement.ts | 2 +- bin/cli.js | 5 +++-- src/core/executionRecorder.ts | 8 ++++++++ tests/ts/test/core/precompile.test.ts | 12 ++++++++---- 5 files changed, 24 insertions(+), 7 deletions(-) diff --git a/assembly/env.ts b/assembly/env.ts index bae6bcf..f2a2e2a 100644 --- a/assembly/env.ts +++ b/assembly/env.ts @@ -8,6 +8,10 @@ export namespace assertResult { export declare function removeDescription(): void; + @external("__unittest_framework_env","registerTestFunction") + export declare function registerTestFunction(index: u32): void; + + @external("__unittest_framework_env","collectCheckResult") export declare function collectCheckResult( result: bool, diff --git a/assembly/implement.ts b/assembly/implement.ts index 92ab21f..5c6e0df 100644 --- a/assembly/implement.ts +++ b/assembly/implement.ts @@ -11,7 +11,7 @@ export function describeImpl( } export function testImpl(description: string, testFunction: () => void): void { assertResult.addDescription(description); - testFunction(); + assertResult.registerTestFunction(testFunction.index); assertResult.removeDescription(); mockFunctionStatus.clear(); } diff --git a/bin/cli.js b/bin/cli.js index 1fa4964..734cbbf 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -40,7 +40,8 @@ const excludes = config.exclude || []; validatArgument(includes, excludes); // if enabled testcase or testNamePattern, disable collectCoverage by default -const collectCoverage = Boolean(options.collectCoverage) || config.collectCoverage || (!options.testcase && !options.testNamePattern); +const collectCoverage = + Boolean(options.collectCoverage) || config.collectCoverage || (!options.testcase && !options.testNamePattern); const testOption = { includes, @@ -57,7 +58,7 @@ const testOption = { mode: options.mode || config.mode || "table", warnLimit: Number(options.coverageLimit?.at(1)), errorLimit: Number(options.coverageLimit?.at(0)), -} +}; start_unit_test(testOption) .then((success) => { diff --git a/src/core/executionRecorder.ts b/src/core/executionRecorder.ts index b899a9d..7339ddf 100644 --- a/src/core/executionRecorder.ts +++ b/src/core/executionRecorder.ts @@ -4,6 +4,7 @@ export class ExecutionRecorder implements IAssertResult { total: number = 0; fail: number = 0; failed_info: AssertFailMessage = {}; + registerFunctions: [string, number][] = []; _currentTestDescriptions: string[] = []; _addDescription(description: string): void { @@ -12,6 +13,10 @@ export class ExecutionRecorder implements IAssertResult { _removeDescription(): void { this._currentTestDescriptions.pop(); } + registerTestFunction(fncIndex: number): void { + const testCaseFullName = this._currentTestDescriptions.join(" - "); + this.registerFunctions.push([testCaseFullName, fncIndex]); + } collectCheckResult(result: boolean, codeInfoIndex: number, actualValue: string, expectValue: string): void { this.total++; if (!result) { @@ -32,6 +37,9 @@ export class ExecutionRecorder implements IAssertResult { removeDescription: (): void => { this._removeDescription(); }, + registerTestFunction: (index: number): void => { + this.registerTestFunction(index); + }, collectCheckResult: (result: number, codeInfoIndex: number, actualValue: number, expectValue: number): void => { this.collectCheckResult( result !== 0, diff --git a/tests/ts/test/core/precompile.test.ts b/tests/ts/test/core/precompile.test.ts index bdf2e98..6eaa4df 100644 --- a/tests/ts/test/core/precompile.test.ts +++ b/tests/ts/test/core/precompile.test.ts @@ -1,10 +1,14 @@ -import { join } from "node:path"; import { precompile } from "../../../../src/core/precompile.js"; -import { projectRoot } from "../../../../src/utils/projectRoot.js"; test("listFunction transform", async () => { - const transformFunction = join(projectRoot, "transform", "listFunctions.mjs"); - const unittestPackages = await precompile(["tests/ts/fixture/transformFunction.ts"], [], [], "", transformFunction); + const unittestPackages = await precompile( + ["tests/ts/fixture/transformFunction.ts"], + [], + undefined, + undefined, + false, + "" + ); expect(unittestPackages.testCodePaths).toEqual([]); expect(unittestPackages.sourceFunctions).toMatchSnapshot(); }); From e70b6a123070fae7bdef9b8e70d3a9d300b5e4d1 Mon Sep 17 00:00:00 2001 From: XMadrid Date: Tue, 17 Jun 2025 11:19:08 +0800 Subject: [PATCH 04/11] wip2 --- tests/ts/test/core/precompile.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ts/test/core/precompile.test.ts b/tests/ts/test/core/precompile.test.ts index 6eaa4df..72056c7 100644 --- a/tests/ts/test/core/precompile.test.ts +++ b/tests/ts/test/core/precompile.test.ts @@ -6,7 +6,7 @@ test("listFunction transform", async () => { [], undefined, undefined, - false, + true, "" ); expect(unittestPackages.testCodePaths).toEqual([]); From c2d4d02666e8ebeaee233cc4ec7f6c0965b0abfe Mon Sep 17 00:00:00 2001 From: XMadrid Date: Tue, 17 Jun 2025 18:19:32 +0800 Subject: [PATCH 05/11] support executeTestFunction --- .gitignore | 1 + instrumentation/MockInstrumentationWalker.cpp | 17 +++++++++++++++++ instrumentation/MockInstrumentationWalker.hpp | 5 +++++ src/core/execute.ts | 8 ++++++++ 4 files changed, 31 insertions(+) diff --git a/.gitignore b/.gitignore index b57a61e..8262af7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .vscode +.cache /node_modules /dist diff --git a/instrumentation/MockInstrumentationWalker.cpp b/instrumentation/MockInstrumentationWalker.cpp index cb991c2..63de582 100644 --- a/instrumentation/MockInstrumentationWalker.cpp +++ b/instrumentation/MockInstrumentationWalker.cpp @@ -1,6 +1,8 @@ #include "MockInstrumentationWalker.hpp" +#include #include #include +#include #include // mock test will be tested with wasm-testing-framework project, escape this class // LCOV_EXCL_START @@ -86,6 +88,20 @@ bool MockInstrumentationWalker::mockFunctionDuplicateImportedCheck() const noexc return checkRepeat; } +void MockInstrumentationWalker::addExecuteTestFunction() noexcept { + std::vector operands{}; + 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 @@ -93,6 +109,7 @@ uint32_t MockInstrumentationWalker::mockWalk() noexcept { wasm::ModuleUtils::iterDefinedFunctions(*module, [this](wasm::Function *const func) noexcept { walkFunctionInModule(func, this->module); }); + addExecuteTestFunction(); return 0U; } } diff --git a/instrumentation/MockInstrumentationWalker.hpp b/instrumentation/MockInstrumentationWalker.hpp index f45f009..39db9b8 100644 --- a/instrumentation/MockInstrumentationWalker.hpp +++ b/instrumentation/MockInstrumentationWalker.hpp @@ -130,6 +130,11 @@ class MockInstrumentationWalker final : public wasm::PostWalker void)(functions); + mockInstrumentFunc["mockFunctionStatus.clear"](); + } + } } catch (error) { if (error instanceof Error) { console.error(error.stack); From b8d87770f7297cb89f63017b7f14dc5aee3551e4 Mon Sep 17 00:00:00 2001 From: XMadrid Date: Wed, 18 Jun 2025 12:04:47 +0800 Subject: [PATCH 06/11] support testNamePattern option --- .gitignore | 2 +- src/core/execute.ts | 30 ++- src/core/executionRecorder.ts | 4 +- src/core/precompile.ts | 3 + src/index.ts | 7 +- transform/listTestNames.mjs | 435 ---------------------------------- transform/listTestNames.mts | 2 +- 7 files changed, 37 insertions(+), 446 deletions(-) delete mode 100644 transform/listTestNames.mjs diff --git a/.gitignore b/.gitignore index 8262af7..cd1e584 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,7 @@ /dist /build* -/transform/listFunctions.mjs +/transform/*.mjs /transform/tsconfig.tsbuildinfo /coverage diff --git a/src/core/execute.ts b/src/core/execute.ts index 1da8f0b..9aceda5 100644 --- a/src/core/execute.ts +++ b/src/core/execute.ts @@ -10,10 +10,16 @@ import { supplyDefaultFunction } from "../utils/index.js"; import { parseImportFunctionInfo } from "../utils/wasmparser.js"; import { ExecutionRecorder } from "./executionRecorder.js"; import { CoverageRecorder } from "./covRecorder.js"; +import assert from "node:assert"; const readFile = promises.readFile; -async function nodeExecutor(wasm: string, outFolder: string, imports?: Imports): Promise { +async function nodeExecutor( + wasm: string, + outFolder: string, + matchedTestNames?: string[], + imports?: Imports +): Promise { const wasi = new WASI({ args: ["node", basename(wasm)], env: process.env, @@ -46,12 +52,23 @@ async function nodeExecutor(wasm: string, outFolder: string, imports?: Imports): try { wasi.start(ins); const execTestFunction = ins.exports["executeTestFunction"]; - if (typeof execTestFunction === "function") { - for (const fncs of executionRecorder.registerFunctions) { - const functions = fncs[1]; - (execTestFunction as (a: number) => void)(functions); + assert(typeof execTestFunction === "function"); + if (matchedTestNames === undefined) { + // means execute all testFunctions + for (const functionInfo of executionRecorder.registerFunctions) { + const functionIndex = functionInfo[1]; + (execTestFunction as (a: number) => void)(functionIndex); mockInstrumentFunc["mockFunctionStatus.clear"](); } + } else { + for (const functionInfo of executionRecorder.registerFunctions) { + const [testName, functionIndex] = functionInfo; + if (matchedTestNames.includes(testName)) { + console.log(testName); + (execTestFunction as (a: number) => void)(functionIndex); + mockInstrumentFunc["mockFunctionStatus.clear"](); + } + } } } catch (error) { if (error instanceof Error) { @@ -66,6 +83,7 @@ async function nodeExecutor(wasm: string, outFolder: string, imports?: Imports): export async function execWasmBinarys( outFolder: string, instrumentResult: InstrumentResult[], + matchedTestNames?: string[], imports?: Imports ): Promise { const assertRes = new AssertResult(); @@ -73,7 +91,7 @@ export async function execWasmBinarys( await Promise.all( instrumentResult.map(async (res): Promise => { const { instrumentedWasm, expectInfo } = res; - const recorder: ExecutionRecorder = await nodeExecutor(instrumentedWasm, outFolder, imports); + const recorder: ExecutionRecorder = await nodeExecutor(instrumentedWasm, outFolder, matchedTestNames, imports); await assertRes.merge(recorder, expectInfo); }) ); diff --git a/src/core/executionRecorder.ts b/src/core/executionRecorder.ts index 7339ddf..734689a 100644 --- a/src/core/executionRecorder.ts +++ b/src/core/executionRecorder.ts @@ -14,14 +14,14 @@ export class ExecutionRecorder implements IAssertResult { this._currentTestDescriptions.pop(); } registerTestFunction(fncIndex: number): void { - const testCaseFullName = this._currentTestDescriptions.join(" - "); + const testCaseFullName = this._currentTestDescriptions.join(" "); this.registerFunctions.push([testCaseFullName, fncIndex]); } collectCheckResult(result: boolean, codeInfoIndex: number, actualValue: string, expectValue: string): void { this.total++; if (!result) { this.fail++; - const testCaseFullName = this._currentTestDescriptions.join(" - "); + const testCaseFullName = this._currentTestDescriptions.join(" "); const assertMessage: AssertMessage = [codeInfoIndex.toString(), actualValue, expectValue]; this.failed_info[testCaseFullName] = this.failed_info[testCaseFullName] || []; this.failed_info[testCaseFullName].push(assertMessage); diff --git a/src/core/precompile.ts b/src/core/precompile.ts index 8ec7bf3..03f0eeb 100644 --- a/src/core/precompile.ts +++ b/src/core/precompile.ts @@ -8,6 +8,7 @@ import { join, relative, resolve } from "node:path"; import { getIncludeFiles } from "../utils/pathResolver.js"; import { SourceFunctionInfo, UnittestPackage } from "../interface.js"; import { projectRoot } from "../utils/projectRoot.js"; +import assert from "node:assert"; export async function precompile( includes: string[], @@ -40,6 +41,8 @@ export async function precompile( } } } + + assert(matchedTestFiles.size > 0, `No matched testname using ${testNamePattern}`); return { testCodePaths: Array.from(matchedTestFiles), matchedTestNames: matchedTestNames, diff --git a/src/index.ts b/src/index.ts index fccc32f..cc61f13 100644 --- a/src/index.ts +++ b/src/index.ts @@ -66,7 +66,12 @@ export async function start_unit_test(options: TestOption): Promise { const instrumentResult = await instrument(wasmPaths, sourcePaths, options.collectCoverage); console.log(chalk.blueBright("instrument: ") + chalk.bold.greenBright("OK")); - const executedResult = await execWasmBinarys(options.tempFolder, instrumentResult, options.imports); + const executedResult = await execWasmBinarys( + options.tempFolder, + instrumentResult, + unittestPackage.matchedTestNames, + options.imports + ); console.log(chalk.blueBright("execute testcases: ") + chalk.bold.greenBright("OK")); logAssertResult(executedResult); diff --git a/transform/listTestNames.mjs b/transform/listTestNames.mjs deleted file mode 100644 index ad22cf4..0000000 --- a/transform/listTestNames.mjs +++ /dev/null @@ -1,435 +0,0 @@ -import { Transform } from "assemblyscript/transform"; -import assert from "node:assert"; -class SourceFunctionTransform extends Transform { - constructor() { - super(...arguments); - this.testNames = []; - this.currentTestDescriptions = []; - } - afterInitialize(program) { - // There will be two sources with SourceKind.UserEntry, ~lib/rt/index-incremental.ts should be filtered - const entrySource = program.sources.find((source) => source.sourceKind === 1 /* SourceKind.UserEntry */ && !source.normalizedPath.startsWith("~lib/")); - this.testFileName = entrySource.normalizedPath; - this.visitNode(entrySource); - globalThis.testNames = this.testNames; - } - visitNode(node) { - // eslint-disable-next-line sonarjs/max-switch-cases - switch (node.kind) { - case 0 /* NodeKind.Source */: { - this.visitSource(node); - break; - } - // types - case 1 /* NodeKind.NamedType */: - case 2 /* NodeKind.FunctionType */: - case 3 /* NodeKind.TypeName */: - case 4 /* NodeKind.TypeParameter */: { - break; - } - case 5 /* NodeKind.Parameter */: { - this.visitParameterNode(node); - break; - } - // Expressions - case 6 /* NodeKind.Identifier */: - case 13 /* NodeKind.False */: - case 16 /* NodeKind.Literal */: - case 18 /* NodeKind.Null */: - case 19 /* NodeKind.Omitted */: - case 23 /* NodeKind.Super */: - case 24 /* NodeKind.This */: - case 25 /* NodeKind.True */: - case 26 /* NodeKind.Constructor */: - case 29 /* NodeKind.Compiled */: { - break; - } - case 7 /* NodeKind.Assertion */: { - this.visitAssertionExpression(node); - break; - } - case 8 /* NodeKind.Binary */: { - this.visitBinaryExpression(node); - break; - } - case 9 /* NodeKind.Call */: { - this.visitCallExpression(node); - break; - } - case 10 /* NodeKind.Class */: { - this.visitClassExpression(node); - break; - } - case 11 /* NodeKind.Comma */: { - this.visitCommaExpression(node); - break; - } - case 12 /* NodeKind.ElementAccess */: { - this.visitElementAccessExpression(node); - break; - } - case 14 /* NodeKind.Function */: { - this.visitFunctionExpression(node); - break; - } - case 15 /* NodeKind.InstanceOf */: { - this.visitInstanceOfExpression(node); - break; - } - case 17 /* NodeKind.New */: { - this.visitNewExpression(node); - break; - } - case 20 /* NodeKind.Parenthesized */: { - this.visitParenthesizedExpression(node); - break; - } - case 21 /* NodeKind.PropertyAccess */: { - this.visitPropertyAccessExpression(node); - break; - } - case 22 /* NodeKind.Ternary */: { - this.visitTernaryExpression(node); - break; - } - case 27 /* NodeKind.UnaryPostfix */: { - this.visitUnaryPostfixExpression(node); - break; - } - case 28 /* NodeKind.UnaryPrefix */: { - this.visitUnaryPrefixExpression(node); - break; - } - // statements: - case 31 /* NodeKind.Break */: - case 34 /* NodeKind.Empty */: - case 35 /* NodeKind.Export */: - case 36 /* NodeKind.ExportDefault */: - case 37 /* NodeKind.ExportImport */: - case 32 /* NodeKind.Continue */: - case 42 /* NodeKind.Import */: - case 50 /* NodeKind.Module */: { - break; - } - case 30 /* NodeKind.Block */: { - this.visitBlockStatement(node); - break; - } - case 33 /* NodeKind.Do */: { - this.visitDoStatement(node); - break; - } - case 38 /* NodeKind.Expression */: { - this.visitExpressionStatement(node); - break; - } - case 39 /* NodeKind.For */: { - this.visitForStatement(node); - break; - } - case 40 /* NodeKind.ForOf */: { - this.visitForOfStatement(node); - break; - } - case 41 /* NodeKind.If */: { - this.visitIfStatement(node); - break; - } - case 43 /* NodeKind.Return */: { - this.visitReturnStatement(node); - break; - } - case 44 /* NodeKind.Switch */: { - this.visitSwitchStatement(node); - break; - } - case 45 /* NodeKind.Throw */: { - this.visitThrowStatement(node); - break; - } - case 46 /* NodeKind.Try */: { - this.visitTryStatement(node); - break; - } - case 47 /* NodeKind.Variable */: { - this.visitVariableStatement(node); - break; - } - case 48 /* NodeKind.Void */: { - this.visitVoidStatement(node); - break; - } - case 49 /* NodeKind.While */: { - this.visitWhileStatement(node); - break; - } - // declaration statements - case 56 /* NodeKind.ImportDeclaration */: - case 60 /* NodeKind.TypeDeclaration */: { - break; - } - case 51 /* NodeKind.ClassDeclaration */: { - this.visitClassDeclaration(node); - break; - } - case 52 /* NodeKind.EnumDeclaration */: { - this.visitEnumDeclaration(node); - break; - } - case 53 /* NodeKind.EnumValueDeclaration */: { - this.visitEnumValueDeclaration(node); - break; - } - case 54 /* NodeKind.FieldDeclaration */: { - this.visitFieldDeclaration(node); - break; - } - case 55 /* NodeKind.FunctionDeclaration */: { - this.visitFunctionDeclaration(node); - break; - } - case 57 /* NodeKind.InterfaceDeclaration */: { - this.visitInterfaceDeclaration(node); - break; - } - case 58 /* NodeKind.MethodDeclaration */: { - this.visitMethodDeclaration(node); - break; - } - case 59 /* NodeKind.NamespaceDeclaration */: { - this.visitNamespaceDeclaration(node); - break; - } - case 61 /* NodeKind.VariableDeclaration */: { - this.visitVariableDeclaration(node); - break; - } - // special - case 63 /* NodeKind.ExportMember */: - case 65 /* NodeKind.IndexSignature */: - case 66 /* NodeKind.Comment */: - case 62 /* NodeKind.Decorator */: { - break; - } - case 64 /* NodeKind.SwitchCase */: { - this.visitSwitchCase(node); - break; - } - } - } - visitSource(node) { - for (const statement of node.statements) { - this.visitNode(statement); - } - } - visitParameterNode(node) { - if (node.initializer) { - this.visitNode(node.initializer); - } - } - visitAssertionExpression(node) { - this.visitNode(node.expression); - } - visitBinaryExpression(node) { - this.visitNode(node.left); - this.visitNode(node.right); - } - visitCallExpression(node) { - if (node.expression.kind === 6 /* NodeKind.Identifier */) { - const fncName = node.expression.text; - if (fncName === "describe" || fncName === "test") { - assert(node.args.length === 2); - assert(node.args[0].kind === 16 /* NodeKind.Literal */ && - node.args[0].literalKind === 2 /* LiteralKind.String */); - const testName = node.args[0].value; - this.currentTestDescriptions.push(testName); - if (fncName === "test") { - this.testNames.push(this.currentTestDescriptions.join("")); - } - this.visitNode(node.expression); - for (const arg of node.args) { - this.visitNode(arg); - } - this.currentTestDescriptions.pop(); - } - } - else { - this.visitNode(node.expression); - for (const arg of node.args) { - this.visitNode(arg); - } - } - } - visitClassExpression(node) { - this.visitClassDeclaration(node.declaration); - } - visitCommaExpression(node) { - for (const expr of node.expressions) { - this.visitNode(expr); - } - } - visitElementAccessExpression(node) { - this.visitNode(node.expression); - this.visitNode(node.elementExpression); - } - visitFunctionExpression(node) { - this.visitFunctionDeclaration(node.declaration); - } - visitInstanceOfExpression(node) { - this.visitNode(node.expression); - } - visitNewExpression(node) { - for (const arg of node.args) { - this.visitNode(arg); - } - } - visitParenthesizedExpression(node) { - this.visitNode(node.expression); - } - visitPropertyAccessExpression(node) { - this.visitNode(node.expression); - } - visitTernaryExpression(node) { - this.visitNode(node.condition); - this.visitNode(node.ifThen); - this.visitNode(node.ifElse); - } - visitUnaryPostfixExpression(node) { - this.visitNode(node.operand); - } - visitUnaryPrefixExpression(node) { - this.visitNode(node.operand); - } - // eslint-disable-next-line sonarjs/no-identical-functions - visitBlockStatement(node) { - for (const statement of node.statements) { - this.visitNode(statement); - } - } - visitDoStatement(node) { - this.visitNode(node.body); - this.visitNode(node.condition); - } - visitExpressionStatement(node) { - this.visitNode(node.expression); - } - visitForStatement(node) { - if (node.initializer) { - this.visitNode(node.initializer); - } - if (node.condition) { - this.visitNode(node.condition); - } - if (node.incrementor) { - this.visitNode(node.incrementor); - } - this.visitNode(node.body); - } - visitForOfStatement(node) { - this.visitNode(node.variable); - this.visitNode(node.iterable); - this.visitNode(node.body); - } - visitIfStatement(node) { - this.visitNode(node.condition); - this.visitNode(node.ifTrue); - if (node.ifFalse) { - this.visitNode(node.ifFalse); - } - } - visitReturnStatement(node) { - if (node.value) { - this.visitNode(node.value); - } - } - visitSwitchStatement(node) { - this.visitNode(node.condition); - for (const switchCase of node.cases) { - this.visitSwitchCase(switchCase); - } - } - visitThrowStatement(node) { - this.visitNode(node.value); - } - visitTryStatement(node) { - for (const stat of node.bodyStatements) { - this.visitNode(stat); - } - if (node.catchStatements) { - for (const stat of node.catchStatements) { - this.visitNode(stat); - } - } - if (node.finallyStatements) { - for (const stat of node.finallyStatements) { - this.visitNode(stat); - } - } - } - visitVariableStatement(node) { - for (const declaration of node.declarations) { - this.visitVariableDeclaration(declaration); - } - } - visitVoidStatement(node) { - this.visitNode(node.expression); - } - visitWhileStatement(node) { - this.visitNode(node.condition); - this.visitNode(node.body); - } - visitClassDeclaration(node) { - for (const member of node.members) { - this.visitNode(member); - } - } - visitEnumDeclaration(node) { - for (const value of node.values) { - this.visitEnumValueDeclaration(value); - } - } - // eslint-disable-next-line sonarjs/no-identical-functions - visitEnumValueDeclaration(node) { - if (node.initializer) { - this.visitNode(node.initializer); - } - } - // eslint-disable-next-line sonarjs/no-identical-functions - visitFieldDeclaration(node) { - if (node.initializer) { - this.visitNode(node.initializer); - } - } - visitFunctionDeclaration(node) { - if (node.body) { - this.visitNode(node.body); - } - } - visitInterfaceDeclaration(node) { - this.visitClassDeclaration(node); - } - visitMethodDeclaration(node) { - this.visitFunctionDeclaration(node); - } - // eslint-disable-next-line sonarjs/no-identical-functions - visitNamespaceDeclaration(node) { - for (const member of node.members) { - this.visitNode(member); - } - } - // eslint-disable-next-line sonarjs/no-identical-functions - visitVariableDeclaration(node) { - if (node.initializer) { - this.visitNode(node.initializer); - } - } - visitSwitchCase(node) { - if (node.label) { - this.visitNode(node.label); - } - for (const stat of node.statements) { - this.visitNode(stat); - } - } -} -export default SourceFunctionTransform; diff --git a/transform/listTestNames.mts b/transform/listTestNames.mts index 5d95f03..6dd1fed 100644 --- a/transform/listTestNames.mts +++ b/transform/listTestNames.mts @@ -309,7 +309,7 @@ class SourceFunctionTransform extends Transform { const testName = (node.args[0] as StringLiteralExpression).value; this.currentTestDescriptions.push(testName); if (fncName === "test") { - this.testNames.push(this.currentTestDescriptions.join("")); + this.testNames.push(this.currentTestDescriptions.join(" ")); } this.visitNode(node.expression); for (const arg of node.args) { From ce5e7f5837d552983681fd1f6e7a0fa3f43ad6c1 Mon Sep 17 00:00:00 2001 From: XMadrid Date: Wed, 18 Jun 2025 15:43:32 +0800 Subject: [PATCH 07/11] fix --- assembly/env.ts | 4 -- assembly/implement.ts | 1 - src/core/execute.ts | 15 +++++--- src/core/executionRecorder.ts | 39 ++++++++++---------- src/interface.ts | 1 + tests/ts/test/core/executionRecorder.test.ts | 9 +++++ 6 files changed, 39 insertions(+), 30 deletions(-) diff --git a/assembly/env.ts b/assembly/env.ts index dc578ea..f2a2e2a 100644 --- a/assembly/env.ts +++ b/assembly/env.ts @@ -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, diff --git a/assembly/implement.ts b/assembly/implement.ts index 8223ace..bb1769c 100644 --- a/assembly/implement.ts +++ b/assembly/implement.ts @@ -12,7 +12,6 @@ export function describeImpl( export function testImpl(description: string, testFunction: () => void): void { assertResult.addDescription(description); assertResult.registerTestFunction(testFunction.index); - assertResult.finishTestFunction(); assertResult.removeDescription(); } diff --git a/src/core/execute.ts b/src/core/execute.ts index 505eed5..c68938b 100644 --- a/src/core/execute.ts +++ b/src/core/execute.ts @@ -31,7 +31,7 @@ async function nodeExecutor( const executionRecorder = new ExecutionRecorder(); const coverageRecorder = new CoverageRecorder(); - const importsArg = new ImportsArgument(); + const importsArg = new ImportsArgument(executionRecorder); const userDefinedImportsObject = imports === undefined ? {} : imports!(importsArg); const importObject: ASImports = { wasi_snapshot_preview1: wasi.wasiImport, @@ -53,18 +53,21 @@ async function nodeExecutor( const execTestFunction = ins.exports["executeTestFunction"]; assert(typeof execTestFunction === "function"); if (matchedTestNames === undefined) { - // means execute all testFunctions + // By default, all testcases are executed for (const functionInfo of executionRecorder.registerFunctions) { - const functionIndex = functionInfo[1]; + const [testCaseName, functionIndex] = functionInfo; + executionRecorder.startTestFunction(testCaseName); (execTestFunction as (a: number) => void)(functionIndex); + executionRecorder.finishTestFunction(); mockInstrumentFunc["mockFunctionStatus.clear"](); } } else { for (const functionInfo of executionRecorder.registerFunctions) { - const [testName, functionIndex] = functionInfo; - if (matchedTestNames.includes(testName)) { - console.log(testName); + const [testCaseName, functionIndex] = functionInfo; + if (matchedTestNames.includes(testCaseName)) { + executionRecorder.startTestFunction(testCaseName); (execTestFunction as (a: number) => void)(functionIndex); + executionRecorder.finishTestFunction(); mockInstrumentFunc["mockFunctionStatus.clear"](); } } diff --git a/src/core/executionRecorder.ts b/src/core/executionRecorder.ts index 096bb0f..1e7288f 100644 --- a/src/core/executionRecorder.ts +++ b/src/core/executionRecorder.ts @@ -46,10 +46,11 @@ export class ExecutionRecorder implements UnitTestFramework { registerFunctions: [string, number][] = []; #currentTestDescriptions: string[] = []; - #logRecorder = new LogRecorder(); + #testCaseFullName: string = ""; + logRecorder = new LogRecorder(); - get #currentTestDescription(): string { - return this.#currentTestDescriptions.join(" "); + set testCaseFullName(testCaseFullName: string) { + this.#testCaseFullName = testCaseFullName; } _addDescription(description: string): void { @@ -58,26 +59,29 @@ export class ExecutionRecorder implements UnitTestFramework { _removeDescription(): void { this.#currentTestDescriptions.pop(); } - registerTestFunction(fncIndex: number): void { - this.registerFunctions.push([this.#currentTestDescription, fncIndex]); - this.#logRecorder.onStartTest(); + _registerTestFunction(fncIndex: number): void { + const testCaseFullName = this.#currentTestDescriptions.join(" "); + this.registerFunctions.push([testCaseFullName, fncIndex]); } - _finishTestFunction(): void { - const logMessages: string[] | null = this.#logRecorder.onFinishTest(); + startTestFunction(testCaseFullName: string): void { + this.#testCaseFullName = testCaseFullName; + this.logRecorder.onStartTest(); + } + finishTestFunction(): void { + const logMessages: string[] | null = this.logRecorder.onFinishTest(); if (logMessages !== null) { - const testCaseFullName = this.#currentTestDescription; - this.result.failedLogMessages[testCaseFullName] = (this.result.failedLogMessages[testCaseFullName] || []).concat( - logMessages - ); + this.result.failedLogMessages[this.#testCaseFullName] = ( + this.result.failedLogMessages[this.#testCaseFullName] || [] + ).concat(logMessages); } } collectCheckResult(result: boolean, codeInfoIndex: number, actualValue: string, expectValue: string): void { this.result.total++; if (!result) { - this.#logRecorder.markTestFailed(); + this.logRecorder.markTestFailed(); this.result.fail++; - const testCaseFullName = this.#currentTestDescription; + const testCaseFullName = this.#testCaseFullName; const assertMessage: AssertMessage = [codeInfoIndex.toString(), actualValue, expectValue]; this.result.failedInfo[testCaseFullName] = this.result.failedInfo[testCaseFullName] || []; this.result.failedInfo[testCaseFullName].push(assertMessage); @@ -85,7 +89,7 @@ export class ExecutionRecorder implements UnitTestFramework { } log(msg: string): void { - this.#logRecorder.addLog(msg); + this.logRecorder.addLog(msg); } getCollectionFuncSet(arg: ImportsArgument): Record> { @@ -98,10 +102,7 @@ export class ExecutionRecorder implements UnitTestFramework { this._removeDescription(); }, registerTestFunction: (index: number): void => { - this.registerTestFunction(index); - }, - finishTestFunction: () => { - this._finishTestFunction(); + this._registerTestFunction(index); }, collectCheckResult: (result: number, codeInfoIndex: number, actualValue: number, expectValue: number): void => { this.collectCheckResult( diff --git a/src/interface.ts b/src/interface.ts index eb9a331..087e284 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -186,6 +186,7 @@ export class ImportsArgument { module: WebAssembly.Module | null = null; instance: WebAssembly.Instance | null = null; exports: (ASUtil & Record) | null = null; + constructor(public framework: UnitTestFramework) {} } export type Imports = ((arg: ImportsArgument) => Record) | null; diff --git a/tests/ts/test/core/executionRecorder.test.ts b/tests/ts/test/core/executionRecorder.test.ts index cf95e4a..c5a1b79 100644 --- a/tests/ts/test/core/executionRecorder.test.ts +++ b/tests/ts/test/core/executionRecorder.test.ts @@ -5,7 +5,10 @@ describe("execution recorder", () => { test("add single description", () => { const recorder = new ExecutionRecorder(); recorder._addDescription("description"); + recorder._registerTestFunction(1); + expect(recorder.registerFunctions).toEqual([["description", 1]]); + recorder.startTestFunction("description"); recorder.collectCheckResult(false, 0, "", ""); expect(recorder.result.failedInfo).toHaveProperty("description"); }); @@ -13,7 +16,10 @@ describe("execution recorder", () => { const recorder = new ExecutionRecorder(); recorder._addDescription("description1"); recorder._addDescription("description2"); + recorder._registerTestFunction(1); + expect(recorder.registerFunctions).toEqual([["description1 description2", 1]]); + recorder.startTestFunction("description1 description2"); recorder.collectCheckResult(false, 0, "", ""); expect(recorder.result.failedInfo).toHaveProperty("description1 description2"); }); @@ -23,7 +29,10 @@ describe("execution recorder", () => { recorder._addDescription("description1"); recorder._addDescription("description2"); recorder._removeDescription(); + recorder._registerTestFunction(1); + expect(recorder.registerFunctions).toEqual([["description1", 1]]); + recorder.startTestFunction("description1"); recorder.collectCheckResult(false, 0, "", ""); expect(recorder.result.failedInfo).toHaveProperty("description1"); }); From d78d229c00e78f55ae46f9c86aabd83e2ab1845b Mon Sep 17 00:00:00 2001 From: XMadrid Date: Wed, 18 Jun 2025 16:25:57 +0800 Subject: [PATCH 08/11] fix --- instrumentation/MockInstrumentationWalker.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/instrumentation/MockInstrumentationWalker.cpp b/instrumentation/MockInstrumentationWalker.cpp index 63de582..19967be 100644 --- a/instrumentation/MockInstrumentationWalker.cpp +++ b/instrumentation/MockInstrumentationWalker.cpp @@ -1,4 +1,5 @@ #include "MockInstrumentationWalker.hpp" +#include #include #include #include @@ -90,6 +91,10 @@ bool MockInstrumentationWalker::mockFunctionDuplicateImportedCheck() const noexc void MockInstrumentationWalker::addExecuteTestFunction() noexcept { std::vector 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), From 31a00c03ac7fe9f26de222940339174b81edeeb4 Mon Sep 17 00:00:00 2001 From: XMadrid Date: Wed, 18 Jun 2025 16:41:41 +0800 Subject: [PATCH 09/11] fix --- tests/ts/test/core/instrument.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ts/test/core/instrument.test.ts b/tests/ts/test/core/instrument.test.ts index 22f8195..4cae7fa 100644 --- a/tests/ts/test/core/instrument.test.ts +++ b/tests/ts/test/core/instrument.test.ts @@ -13,7 +13,7 @@ test("Instrument", async () => { const base = join(outputDir, "constructor"); const wasmPath = join(outputDir, "constructor.wasm"); const sourceCodePath = "tests/ts/fixture/constructor.ts"; - const results = await instrument([wasmPath], [sourceCodePath]); + const results = await instrument([wasmPath], [sourceCodePath], true); expect(results.length).toEqual(1); const result = results[0]!; const instrumentedWasm = join(outputDir, "constructor.instrumented.wasm"); From b68081000e72cb03d27a640bc41a84c0b6f6510b Mon Sep 17 00:00:00 2001 From: XMadrid Date: Wed, 18 Jun 2025 18:13:22 +0800 Subject: [PATCH 10/11] update docs --- assembly/implement.ts | 4 +-- assembly/index.ts | 6 ++-- bin/cli.js | 2 +- docs/api-documents.md | 2 +- docs/api-documents/mock-function.md | 1 - docs/api-documents/options.md | 49 +++++++++++++++++++++++------ docs/quick-start.md | 30 ++++++++++++------ tests/as/comparison.test.ts | 5 +-- tests/as/expect.test.ts | 4 +-- tests/as/format.test.ts | 4 +-- tests/as/mock.test.ts | 4 +-- 11 files changed, 71 insertions(+), 40 deletions(-) diff --git a/assembly/implement.ts b/assembly/implement.ts index bb1769c..adab134 100644 --- a/assembly/implement.ts +++ b/assembly/implement.ts @@ -9,8 +9,8 @@ export function describeImpl( testsFunction(); assertResult.removeDescription(); } -export function testImpl(description: string, testFunction: () => void): void { - assertResult.addDescription(description); +export function testImpl(name: string, testFunction: () => void): void { + assertResult.addDescription(name); assertResult.registerTestFunction(testFunction.index); assertResult.removeDescription(); } diff --git a/assembly/index.ts b/assembly/index.ts index 14d5ad0..c1696ae 100644 --- a/assembly/index.ts +++ b/assembly/index.ts @@ -20,11 +20,11 @@ export function describe(description: string, testsFunction: () => void): void { /** * run a test - * @param description test description + * @param name test name * @param testFunction main function of test */ -export function test(description: string, testFunction: () => void): void { - testImpl(description, testFunction); +export function test(name: string, testFunction: () => void): void { + testImpl(name, testFunction); } /** diff --git a/bin/cli.js b/bin/cli.js index 9be78c7..0fdbd3f 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -12,11 +12,11 @@ import { validatArgument, start_unit_test } from "../dist/index.js"; const program = new Command(); program .option("--config ", "path of config file", "as-test.config.js") - .option("--testcase ", "run only specified test cases") .option("--temp ", "test template file folder") .option("--output ", "coverage report output folder") .option("--mode ", "coverage report output format") .option("--coverageLimit [error warning...]", "set warn(yellow) and error(red) upper limit in coverage report") + .option("--testcase ", "run only specified test cases") .option("--testNamePattern ", "run only tests with a name that matches the regex pattern") .option("--collectCoverage ", "whether to collect coverage information and report"); diff --git a/docs/api-documents.md b/docs/api-documents.md index 8490474..e46877d 100644 --- a/docs/api-documents.md +++ b/docs/api-documents.md @@ -2,4 +2,4 @@ A comprehensive AssemblyScript testing solution, offering developers a robust su - Function Mocking - Coverage statistics -- Expectations +- Matchers diff --git a/docs/api-documents/mock-function.md b/docs/api-documents/mock-function.md index 87f3862..f84034a 100644 --- a/docs/api-documents/mock-function.md +++ b/docs/api-documents/mock-function.md @@ -31,7 +31,6 @@ test("getTime error handle", () => { expect(getTime()).equal(false); // success expect(fn.calls).equal(1); // success }); -endTest(); ``` mock API can temporary change the behavior of function, effective scope is each test. diff --git a/docs/api-documents/options.md b/docs/api-documents/options.md index 4293fae..5629150 100644 --- a/docs/api-documents/options.md +++ b/docs/api-documents/options.md @@ -33,7 +33,7 @@ There are several ways to run partial test cases: #### Partial Test Files -Providing file path to `--testcase`. it can specify a certain group of files for testing. +Providing file path to `--testcase`, it can specify a certain group of files for testing. ::: tip `--testcase` can accept multiple file paths. @@ -57,20 +57,49 @@ run `as-test --testcase a.test.ts b.test.ts` will match all tests in `a.test.ts` #### Partial Tests -Providing regex which can match targeted test name to `--testNamePattern`. it can specify a certain group of tests for testing. +Providing regex which can match targeted test name to `--testNamePattern`, it can specify a certain group of tests for testing. ::: details ``` -- a.test.ts -|- case_1 -|- case_2 -- b.test.ts -|- case_A -- c.test.ts -|- case_4 +describe("groupA", () => { + test("case_1", () => { + ... + }); + test("case_2", () => { + ... + }); + test("case_3", () => { + ... + }); +}); + +describe("groupB", () => { + test("case_A", () => { + ... + }); + test("case_B", () => { + ... + }); + test("case_C", () => { + ... + }); +}); ``` -run `as-test --testNamePattern "case_\d"` will match `case 1`, `case 2`, `case 4` +run `as-test --testNamePattern "groupA case_\d"` will run `case_1`, `case_2`, `case_3`. + +::: tip +The framework join `DescriptionName` and `TestName` with `" "` by default, e.g. `groupA case_1` is the fullTestCaseName of `case_1`. ::: + +### Whether collect coverage information + +``` + --collectCoverage whether to collect coverage information and report +``` + +The framework collects coverage and generates reports by default, but it will be disablea while running partial test cases by `--testcase` or `--testNamePattern`. + +You can control the coverage collection manually with `--collectCoverage` option. diff --git a/docs/quick-start.md b/docs/quick-start.md index 94a9b39..c176f4d 100644 --- a/docs/quick-start.md +++ b/docs/quick-start.md @@ -19,14 +19,13 @@ export function add(a: i32, b: i32): i32 { Then, create a file named `tests/sum.test.ts`. This will contain our actual test: ```Typescript -import { test, expect, endTest } from "assemblyscript-unittest-framework/assembly"; +import { test, expect } from "assemblyscript-unittest-framework/assembly"; import { add } from "../source/sum"; test("sum", () => { expect(add(1, 2)).equal(3); expect(add(1, 1)).equal(3); }); -endTest(); // Don't forget it! ``` Create a config file in project root `as-test.config.js`: @@ -60,16 +59,29 @@ Add the following section to your `package.json` Finally, run `npm run test` and as-test will print this message: ``` -transform source/sum.ts => build/source/sum.ts.cov -transform build/source/sum.ts.cov => build/source/sum.ts -transform tests/sum.test.ts => build/tests/sum.test.ts -(node:489815) ExperimentalWarning: WASI is an experimental feature. This feature could change at any time +> as-test@1.0.0 test +> as-test + +(node:144985) ExperimentalWarning: WASI is an experimental feature and might change at any time +(Use `node --trace-warnings ...` to show where the warning was created) +code analysis: OK +compile testcases: OK +instrument: OK +execute testcases: OK test case: 1/2 (success/total) -Error Message: - sum: - tests/sum.test.ts:6:3 (6:3, 6:29) +Error Message: + sum: + tests/sum.test.ts:6:2 value: 2 expect: = 3 +---------|---------|----------|---------|-------- +File | % Stmts | % Branch | % Funcs | % Lines +---------|---------|----------|---------|-------- +source | 100 | 100 | 100 | 100 + sum.ts | 100 | 100 | 100 | 100 +---------|---------|----------|---------|-------- + +Test Failed ``` You can also use `npx as-test -h` for more information to control detail configurations diff --git a/tests/as/comparison.test.ts b/tests/as/comparison.test.ts index 37a4cde..5391d8f 100644 --- a/tests/as/comparison.test.ts +++ b/tests/as/comparison.test.ts @@ -1,4 +1,4 @@ -import { describe, endTest, expect, test } from "../../assembly"; +import { describe, expect, test } from "../../assembly"; describe("base type equal", () => { test("i32", () => { @@ -177,7 +177,6 @@ describe("single level container type equal", () => { }); test("nullable equal normal", () => {}); }); - // end test }); describe("mutli-level container type equal", () => { @@ -209,5 +208,3 @@ describe("mutli-level container type equal", () => { expect(arr).equal(arr2); }); }); - -endTest(); diff --git a/tests/as/expect.test.ts b/tests/as/expect.test.ts index d3c7d64..bcd375d 100644 --- a/tests/as/expect.test.ts +++ b/tests/as/expect.test.ts @@ -1,4 +1,4 @@ -import { describe, endTest, expect, test } from "../../assembly"; +import { describe, expect, test } from "../../assembly"; describe("expect", () => { test("< = >", () => { @@ -14,5 +14,3 @@ describe("expect", () => { expect("test").notNull(); }); }); - -endTest(); diff --git a/tests/as/format.test.ts b/tests/as/format.test.ts index e2eb156..0962bd0 100644 --- a/tests/as/format.test.ts +++ b/tests/as/format.test.ts @@ -1,4 +1,4 @@ -import { describe, endTest, expect, test } from "../../assembly"; +import { describe, expect, test } from "../../assembly"; import { toJson } from "../../assembly/formatPrint"; class A {} @@ -57,5 +57,3 @@ describe("print", () => { expect(toJson(new A())).equal("[Object A]"); }); }); - -endTest(); diff --git a/tests/as/mock.test.ts b/tests/as/mock.test.ts index 3fa8588..c1992c7 100644 --- a/tests/as/mock.test.ts +++ b/tests/as/mock.test.ts @@ -1,4 +1,4 @@ -import { describe, endTest, expect, mock, remock, test, unmock } from "../../assembly"; +import { describe, expect, mock, remock, test, unmock } from "../../assembly"; import { add, callee, caller, incr, MockClass, call_incr } from "./mockBaseFunc"; const mockReturnValue: i32 = 123; @@ -71,5 +71,3 @@ describe("mock test", () => { expect(call_incr(incr)).equal(180); }); }); - -endTest(); From a10a4ebc9e26711540cba32837711087d77c6d37 Mon Sep 17 00:00:00 2001 From: XMadrid Date: Wed, 18 Jun 2025 18:28:51 +0800 Subject: [PATCH 11/11] fix --- docs/api-documents/options.md | 2 +- docs/quick-start.md | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/api-documents/options.md b/docs/api-documents/options.md index 5629150..c038708 100644 --- a/docs/api-documents/options.md +++ b/docs/api-documents/options.md @@ -87,7 +87,7 @@ describe("groupB", () => { }); ``` -run `as-test --testNamePattern "groupA case_\d"` will run `case_1`, `case_2`, `case_3`. +run `as-test --testNamePattern "groupA case_\d"` will run `case_1`, `case_2`, `case_3`. ::: tip The framework join `DescriptionName` and `TestName` with `" "` by default, e.g. `groupA case_1` is the fullTestCaseName of `case_1`. diff --git a/docs/quick-start.md b/docs/quick-start.md index c176f4d..1034d9c 100644 --- a/docs/quick-start.md +++ b/docs/quick-start.md @@ -71,14 +71,14 @@ execute testcases: OK test case: 1/2 (success/total) -Error Message: - sum: +Error Message: + sum: tests/sum.test.ts:6:2 value: 2 expect: = 3 ---------|---------|----------|---------|-------- File | % Stmts | % Branch | % Funcs | % Lines ---------|---------|----------|---------|-------- -source | 100 | 100 | 100 | 100 - sum.ts | 100 | 100 | 100 | 100 +source | 100 | 100 | 100 | 100 + sum.ts | 100 | 100 | 100 | 100 ---------|---------|----------|---------|-------- Test Failed