Skip to content

support --onlyFailures #53

New issue

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

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

Already on GitHub? Sign in to your account

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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion bin/as-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Expand Down
12 changes: 9 additions & 3 deletions bin/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ program
.option("--coverageLimit [error warning...]", "set warn(yellow) and error(red) upper limit in coverage report")
.option("--testcase <testcases...>", "run only specified test cases")
.option("--testNamePattern <test name pattern>", "run only tests with a name that matches the regex pattern")
.option("--collectCoverage <boolean>", "whether to collect coverage information and report");
.option("--collectCoverage <boolean>", "whether to collect coverage information and report")
.option("--onlyFailures", "Run tests that failed in the previous");

program.parse(process.argv);
const options = program.opts();
Expand All @@ -39,16 +40,21 @@ 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,
excludes,
testcases: options.testcase,
testNamePattern: options.testNamePattern,
collectCoverage,
onlyFailures,

flags: config.flags || "",
imports: config.imports || undefined,
Expand Down
9 changes: 7 additions & 2 deletions docs/api-documents/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,12 @@ There are command line options which can override the configuration in `as-test.
```
--testcase <testcases...> only run specified test cases
--testNamePattern <test name pattern> 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.

Expand All @@ -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.

Expand Down Expand Up @@ -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

```
Expand Down
11 changes: 0 additions & 11 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
6 changes: 3 additions & 3 deletions src/core/execute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const readFile = promises.readFile;
async function nodeExecutor(
instrumentResult: InstrumentResult,
outFolder: string,
matchedTestNames?: string[],
matchedTestNames: string[],
imports?: Imports
): Promise<SingleExecutionResult> {
const wasi = new WASI({
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -85,7 +85,7 @@ async function nodeExecutor(
export async function execWasmBinaries(
outFolder: string,
instrumentResults: InstrumentResult[],
matchedTestNames?: string[],
matchedTestNames: string[],
imports?: Imports
): Promise<ExecutionResult> {
const assertRes = new ExecutionResult();
Expand Down
54 changes: 33 additions & 21 deletions src/core/precompile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,47 +10,59 @@ 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<UnittestPackage> {
// 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<string>();
let matchedTestNames: string[] = [];

if (testNamePattern) {
const matchedTestNames: string[] = [];
const matchedTestFiles = new Set<string>();
if (testNamePattern || failedTestNames.length > 0) {
// if enabled testNamePattern or enabled onlyFailures, need listTestName transform
const testNameInfos = new Map<string, string[]>();
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);
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<string, SourceFunctionInfo[]>();
if (collectCoverage) {
const sourceFunctions = new Map<string, SourceFunctionInfo[]>();
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
Expand All @@ -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) {
Expand Down
6 changes: 5 additions & 1 deletion src/executionResult.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)) +
Expand Down
21 changes: 20 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
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";
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)) {
Expand All @@ -31,13 +34,28 @@ 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<boolean> {
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(
options.includes,
options.excludes,
options.testcases,
options.testNamePattern,
failedTestCases,
options.collectCoverage,
options.flags
);
Expand All @@ -58,6 +76,7 @@ export async function start_unit_test(options: TestOption): Promise<boolean> {
);
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();
Expand Down
3 changes: 2 additions & 1 deletion src/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ export class CodeCoverage {

export interface UnittestPackage {
readonly testCodePaths: string[];
readonly matchedTestNames?: string[];
readonly matchedTestNames: string[];
readonly sourceFunctions?: Map<string, SourceFunctionInfo[]>;
}

Expand Down Expand Up @@ -197,6 +197,7 @@ export interface TestOption {
testcases?: string[];
testNamePattern?: string;
collectCoverage: boolean;
onlyFailures: boolean;

flags: string;
imports?: Imports;
Expand Down
1 change: 1 addition & 0 deletions tests/ts/test/core/precompile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ test("listFunction transform", async () => {
[],
undefined,
undefined,
[],
true,
""
);
Expand Down
5 changes: 1 addition & 4 deletions tests/ts/test/core/throwError.test.ts
Original file line number Diff line number Diff line change
@@ -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(() => {
Expand All @@ -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");
});

Expand Down