diff --git a/bin/as-test.js b/bin/as-test.js index 1a381a1..d57ee22 100755 --- a/bin/as-test.js +++ b/bin/as-test.js @@ -14,7 +14,7 @@ if (lt(version, "12.16.0")) { exit(-1); } const argv = []; -argv.push("--experimental-wasi-unstable-preview1"); +argv.push("--no-warnings"); if (lt(version, "15.0.0")) { argv.push("--experimental-wasm-bigint"); } diff --git a/bin/cli.js b/bin/cli.js index 0fdbd3f..fb13c70 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -18,7 +18,8 @@ program .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"); + .option("--collectCoverage ", "whether to collect coverage information and report") + .option("--onlyFailures", "Run tests that failed in the previous"); program.parse(process.argv); const options = program.opts(); @@ -39,9 +40,13 @@ if (includes === undefined) { const excludes = config.exclude || []; validatArgument(includes, excludes); -// if enabled testcase or testNamePattern, disable collectCoverage by default +const onlyFailures = options.onlyFailures || false; + +// if enabled testcase or testNamePattern or onlyFailures, disable collectCoverage by default const collectCoverage = - Boolean(options.collectCoverage) || config.collectCoverage || (!options.testcase && !options.testNamePattern); + Boolean(options.collectCoverage) || + config.collectCoverage || + (!options.testcase && !options.testNamePattern && !onlyFailures); const testOption = { includes, @@ -49,6 +54,7 @@ const testOption = { testcases: options.testcase, testNamePattern: options.testNamePattern, collectCoverage, + onlyFailures, flags: config.flags || "", imports: config.imports || undefined, diff --git a/docs/api-documents/options.md b/docs/api-documents/options.md index c038708..73642f5 100644 --- a/docs/api-documents/options.md +++ b/docs/api-documents/options.md @@ -27,11 +27,12 @@ There are command line options which can override the configuration in `as-test. ``` --testcase only run specified test cases --testNamePattern run only tests with a name that matches the regex pattern + --onlyFailures Run tests that failed in the previous ``` There are several ways to run partial test cases: -#### Partial Test Files +#### Run specified test files Providing file path to `--testcase`, it can specify a certain group of files for testing. @@ -55,7 +56,7 @@ run `as-test --testcase a.test.ts b.test.ts` will match all tests in `a.test.ts` ::: -#### Partial Tests +#### Run partial tests using a regex name pattern Providing regex which can match targeted test name to `--testNamePattern`, it can specify a certain group of tests for testing. @@ -94,6 +95,10 @@ The framework join `DescriptionName` and `TestName` with `" "` by default, e.g. ::: +#### Run only failures + +Provides `--onlyFailures` command line option to run the test cases that failed in the previous test only. + ### Whether collect coverage information ``` diff --git a/package-lock.json b/package-lock.json index 0e8b6ea..17d8aad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,6 @@ "license": "Apache-2.0", "dependencies": { "@assemblyscript/loader": ">=0.25.1", - "@assemblyscript/wasi-shim": "^0.1.0", "chalk": "^5.2.0", "commander": "^8.3.0", "cross-spawn": "^7.0.3", @@ -308,16 +307,6 @@ "integrity": "sha512-bHp5C5TQRnmZq+ppGQQclTIGBZ4YVd7eCpktQ+t7WaEs7caTxLofB4VgJ2BNb5n/FrzTh88T1HRsZNsbvXJzBg==", "license": "Apache-2.0" }, - "node_modules/@assemblyscript/wasi-shim": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@assemblyscript/wasi-shim/-/wasi-shim-0.1.0.tgz", - "integrity": "sha512-fSLH7MdJHf2uDW5llA5VCF/CG62Jp2WkYGui9/3vIWs3jDhViGeQF7nMYLUjpigluM5fnq61I6obtCETy39FZw==", - "license": "Apache-2.0", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/assemblyscript" - } - }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", diff --git a/package.json b/package.json index a0cd06b..cf785c2 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,6 @@ }, "dependencies": { "@assemblyscript/loader": ">=0.25.1", - "@assemblyscript/wasi-shim": "^0.1.0", "chalk": "^5.2.0", "commander": "^8.3.0", "cross-spawn": "^7.0.3", diff --git a/src/core/execute.ts b/src/core/execute.ts index c68938b..9eb00f0 100644 --- a/src/core/execute.ts +++ b/src/core/execute.ts @@ -16,7 +16,7 @@ const readFile = promises.readFile; async function nodeExecutor( instrumentResult: InstrumentResult, outFolder: string, - matchedTestNames?: string[], + matchedTestNames: string[], imports?: Imports ): Promise { const wasi = new WASI({ @@ -52,7 +52,7 @@ async function nodeExecutor( wasi.start(ins); const execTestFunction = ins.exports["executeTestFunction"]; assert(typeof execTestFunction === "function"); - if (matchedTestNames === undefined) { + if (matchedTestNames.length === 0) { // By default, all testcases are executed for (const functionInfo of executionRecorder.registerFunctions) { const [testCaseName, functionIndex] = functionInfo; @@ -85,7 +85,7 @@ async function nodeExecutor( export async function execWasmBinaries( outFolder: string, instrumentResults: InstrumentResult[], - matchedTestNames?: string[], + matchedTestNames: string[], imports?: Imports ): Promise { const assertRes = new ExecutionResult(); diff --git a/src/core/precompile.ts b/src/core/precompile.ts index 03f0eeb..e11e0de 100644 --- a/src/core/precompile.ts +++ b/src/core/precompile.ts @@ -10,21 +10,24 @@ import { SourceFunctionInfo, UnittestPackage } from "../interface.js"; import { projectRoot } from "../utils/projectRoot.js"; import assert from "node:assert"; +// eslint-disable-next-line sonarjs/cognitive-complexity export async function precompile( includes: string[], excludes: string[], - testcases: string[] | undefined, + testcases: string[] | undefined, // this field specifed test file names testNamePattern: string | undefined, + failedTestNames: string[], 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 matchedTestFiles = new Set(); + let matchedTestNames: string[] = []; - if (testNamePattern) { - const matchedTestNames: string[] = []; - const matchedTestFiles = new Set(); + if (testNamePattern || failedTestNames.length > 0) { + // if enabled testNamePattern or enabled onlyFailures, need listTestName transform const testNameInfos = new Map(); const testNameTransformFunction = join(projectRoot, "transform", "listTestNames.mjs"); for (const testCodePath of testCodePaths) { @@ -32,25 +35,34 @@ export async function precompile( 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); - matchedTestFiles.add(fileName); + if (testNamePattern) { + const regexPattern = new RegExp(testNamePattern); + for (const [fileName, testNames] of testNameInfos) { + for (const testName of testNames) { + if (regexPattern.test(testName)) { + matchedTestNames.push(testName); + matchedTestFiles.add(fileName); + } } } } - assert(matchedTestFiles.size > 0, `No matched testname using ${testNamePattern}`); - return { - testCodePaths: Array.from(matchedTestFiles), - matchedTestNames: matchedTestNames, - }; + if (failedTestNames.length > 0) { + matchedTestNames = failedTestNames; + for (const [fileName, testNames] of testNameInfos) { + for (const testName of testNames) { + if (matchedTestNames.includes(testName)) { + matchedTestFiles.add(fileName); + } + } + } + } + + assert(matchedTestFiles.size > 0, "No matched testname"); } + const sourceFunctions = new Map(); 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 @@ -64,13 +76,13 @@ export async function precompile( ) ); } - return { - testCodePaths, - sourceFunctions, - }; } - return { testCodePaths }; + return { + testCodePaths: matchedTestFiles.size > 0 ? Array.from(matchedTestFiles) : testCodePaths, + matchedTestNames, + sourceFunctions, + }; } async function transform(transformFunction: string, codePath: string, flags: string, collectCallback: () => void) { diff --git a/src/executionResult.ts b/src/executionResult.ts index 7594787..7149890 100644 --- a/src/executionResult.ts +++ b/src/executionResult.ts @@ -3,7 +3,7 @@ import { json2map } from "./utils/index.js"; import { FailedInfoMap, AssertMessage, ExpectInfo, IAssertResult } from "./interface.js"; import chalk from "chalk"; -const readFile = promises.readFile; +const { readFile, writeFile } = promises; export class ExecutionResult { fail = 0; @@ -43,6 +43,10 @@ export class ExecutionResult { } } + async writeFailures(failuresPath: string) { + await writeFile(failuresPath, JSON.stringify(Array.from(this.failedInfos.keys()))); + } + print(log: (msg: string) => void): void { const rate = (this.fail === 0 ? chalk.greenBright(this.total) : chalk.redBright(this.total - this.fail)) + diff --git a/src/index.ts b/src/index.ts index 19132ce..0dfb0af 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ import chalk from "chalk"; -import { emptydirSync } from "fs-extra"; +import pkg from "fs-extra"; import { Parser } from "./parser/index.js"; import { compile } from "./core/compile.js"; import { precompile } from "./core/precompile.js"; @@ -7,6 +7,9 @@ import { instrument } from "./core/instrument.js"; import { execWasmBinaries } from "./core/execute.js"; import { generateReport, reportConfig } from "./generator/index.js"; import { TestOption } from "./interface.js"; +import { join } from "node:path"; + +const { readFileSync, emptydirSync } = pkg; export function validatArgument(includes: unknown, excludes: unknown) { if (!Array.isArray(includes)) { @@ -31,6 +34,20 @@ export function validatArgument(includes: unknown, excludes: unknown) { * main function of unit-test, will throw Exception in most condition except job carsh */ export async function start_unit_test(options: TestOption): Promise { + const failurePath = join(options.outputFolder, "failures.json"); + let failedTestCases: string[] = []; + if (options.onlyFailures) { + failedTestCases = JSON.parse(readFileSync(failurePath, "utf8")) as string[]; + if (failedTestCases.length === 0) { + options.collectCoverage = true; + console.log( + chalk.yellowBright( + 'Warning: no failed test cases found while enabled "onlyFailures", execute all test cases by default' + ) + ); + } + } + emptydirSync(options.outputFolder); emptydirSync(options.tempFolder); const unittestPackage = await precompile( @@ -38,6 +55,7 @@ export async function start_unit_test(options: TestOption): Promise { options.excludes, options.testcases, options.testNamePattern, + failedTestCases, options.collectCoverage, options.flags ); @@ -58,6 +76,7 @@ export async function start_unit_test(options: TestOption): Promise { ); console.log(chalk.blueBright("execute testcases: ") + chalk.bold.greenBright("OK")); + await executedResult.writeFailures(failurePath); executedResult.print(console.log); if (options.collectCoverage) { const parser = new Parser(); diff --git a/src/interface.ts b/src/interface.ts index 087e284..5706f77 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -168,7 +168,7 @@ export class CodeCoverage { export interface UnittestPackage { readonly testCodePaths: string[]; - readonly matchedTestNames?: string[]; + readonly matchedTestNames: string[]; readonly sourceFunctions?: Map; } @@ -197,6 +197,7 @@ export interface TestOption { testcases?: string[]; testNamePattern?: string; collectCoverage: boolean; + onlyFailures: boolean; flags: string; imports?: Imports; diff --git a/tests/ts/test/core/precompile.test.ts b/tests/ts/test/core/precompile.test.ts index 72056c7..adbc6e2 100644 --- a/tests/ts/test/core/precompile.test.ts +++ b/tests/ts/test/core/precompile.test.ts @@ -6,6 +6,7 @@ test("listFunction transform", async () => { [], undefined, undefined, + [], true, "" ); diff --git a/tests/ts/test/core/throwError.test.ts b/tests/ts/test/core/throwError.test.ts index e24d188..b7dc2a3 100644 --- a/tests/ts/test/core/throwError.test.ts +++ b/tests/ts/test/core/throwError.test.ts @@ -1,7 +1,5 @@ -import { join } from "node:path"; // eslint-disable-next-line n/no-extraneous-import import { jest } from "@jest/globals"; -import { projectRoot } from "../../../../src/utils/projectRoot.js"; jest.unstable_mockModule("assemblyscript/asc", () => ({ main: jest.fn(() => { @@ -17,10 +15,9 @@ const { precompile } = await import("../../../../src/core/precompile.js"); const { compile } = await import("../../../../src/core/compile.js"); test("transform error", async () => { - const transformFunction = join(projectRoot, "transform", "listFunctions.mjs"); expect(jest.isMockFunction(main)).toBeTruthy(); await expect(async () => { - await precompile(["tests/ts/fixture/transformFunction.ts"], [], [], "", transformFunction); + await precompile(["tests/ts/fixture/transformFunction.ts"], [], undefined, undefined, [], true, ""); }).rejects.toThrow("mock asc.main() error"); });