diff --git a/assembly/assertCollector.ts b/assembly/assertCollector.ts deleted file mode 100644 index 78d18a3..0000000 --- a/assembly/assertCollector.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { toJson } from "./formatPrint"; - -class AssertResultCollector { - total: u32 = 0; - fail: u32 = 0; - failed_info: Map = new Map(); - currentTestDescriptions: string[] = []; - - addDescription(description: string): void { - this.currentTestDescriptions.push(description); - } - removeDescription(): void { - this.currentTestDescriptions.pop(); - } - collectCheckResult( - result: bool, - codeInfoIndex: u32, - actualValue: string, - expectValue: string, - ): void { - this.total++; - if (!result) { - this.fail++; - const testCaseFullName = this.currentTestDescriptions.join(" - "); - const assertMessage = [ - codeInfoIndex.toString(), - actualValue, - expectValue, - ]; - if (this.failed_info.has(testCaseFullName)) { - this.failed_info.get(testCaseFullName).push(assertMessage); - } else { - this.failed_info.set(testCaseFullName, [assertMessage]); - } - } - } - - clear(): void { - this.failed_info = new Map(); - this.currentTestDescriptions = []; - __collect(); - } - - totalString(): string { - return `"total":` + this.total.toString(); - } - failString(): string { - return `"fail":` + this.fail.toString(); - } - failInfoString(): string { - return `"failed_info":` + toJson(this.failed_info); - } -} - -export const assertResult = new AssertResultCollector(); diff --git a/assembly/env.ts b/assembly/env.ts new file mode 100644 index 0000000..bae6bcf --- /dev/null +++ b/assembly/env.ts @@ -0,0 +1,18 @@ +export namespace assertResult { + + @external("__unittest_framework_env","addDescription") + export declare function addDescription(description: string): void; + + + @external("__unittest_framework_env","removeDescription") + export declare function removeDescription(): void; + + + @external("__unittest_framework_env","collectCheckResult") + export declare function collectCheckResult( + result: bool, + codeInfoIndex: number, + actualValue: string, + expectValue: string, + ): void; +} diff --git a/assembly/expect.ts b/assembly/expect.ts index d58a20c..ac38205 100644 --- a/assembly/expect.ts +++ b/assembly/expect.ts @@ -1,5 +1,5 @@ import { equal, isNull } from "./comparison"; -import { assertResult } from "./assertCollector"; +import { assertResult } from "./env"; import { toJson } from "./formatPrint"; diff --git a/assembly/implement.ts b/assembly/implement.ts index 908f66f..92ab21f 100644 --- a/assembly/implement.ts +++ b/assembly/implement.ts @@ -1,4 +1,4 @@ -import { assertResult } from "./assertCollector"; +import { assertResult } from "./env"; import { MockFn, mockFunctionStatus } from "./mockInstrument"; export function describeImpl( diff --git a/assembly/output.ts b/assembly/output.ts index e9aeb7f..9cba50c 100644 --- a/assembly/output.ts +++ b/assembly/output.ts @@ -1,128 +1,5 @@ -import { assertResult } from "./assertCollector"; import { outputTrace } from "./covInstrument"; -import { - args_sizes_get, - args_get, - errno, - errnoToString, - fd, - path_open, - lookupflags, - rights, - oflags, - fdflags, - iovec, - fd_write, - fd_close, -} from "@assemblyscript/wasi-shim/assembly/bindings/wasi_snapshot_preview1"; - export function output(): void { - const kvPair: string[] = []; - kvPair.push(assertResult.totalString()); - kvPair.push(assertResult.failString()); - kvPair.push(assertResult.failInfoString()); - assertResult.clear(); - outputTrace(); - - const outputString = "{" + kvPair.join(",") + "}"; - - const outputFileName = getArgs()[1].slice(0, -5) + ".assert.log"; - writeFile(outputFileName, outputString); -} - -let ret: errno; - -function perror(msg: string): void { - if (ret == errno.SUCCESS) { - return; - } - assert(false, errnoToString(ret) + " " + msg); -} - -function checkMemory(offset: usize): void { - assert(offset < usize(i32.MAX_VALUE), "OOM"); -} - -function fromCString(cstring: usize): string { - let size = 0; - while (load(cstring + size) !== 0) { - size++; - } - return String.UTF8.decodeUnsafe(cstring, size); -} - -function getArgs(): string[] { - const args: string[] = []; - - const count_and_size = memory.data(sizeof() * 2); - checkMemory(count_and_size + sizeof() * 2); - ret = args_sizes_get(count_and_size, count_and_size + 4); - perror("args_sizes_get"); - const argc = load(count_and_size, 0); - const argv_total_size = load(count_and_size, sizeof()); - - const argv_ptr_array = new ArrayBuffer(i32((argc + 1) * sizeof())); - const argv_total_string = new ArrayBuffer(i32(argv_total_size)); - checkMemory(changetype(argv_ptr_array) + (argc + 1) * sizeof()); - checkMemory(changetype(argv_total_string) + argv_total_size); - ret = args_get( - changetype(argv_ptr_array), - changetype(argv_total_string), - ); - perror("args_get"); - for (let i: usize = 0; i < argc; i++) { - const argv_ptr = load( - changetype(argv_ptr_array) + i * sizeof(), - ); - const arg = fromCString(argv_ptr); - args.push(arg); - } - - return args; -} - -function writeFile(path: string, data: string): void { - if (data.length == 0) return; - // open - const utf8path = String.UTF8.encode(path); - const fdptr = memory.data(sizeof()); - checkMemory(changetype(utf8path) + utf8path.byteLength); - ret = path_open( - 3, - lookupflags.SYMLINK_FOLLOW, - changetype(utf8path), - utf8path.byteLength, - oflags.CREAT | oflags.TRUNC, - rights.FD_WRITE | - rights.FD_SEEK | - rights.FD_TELL | - rights.FD_FILESTAT_GET | - rights.PATH_CREATE_FILE, - rights.FD_WRITE | - rights.FD_SEEK | - rights.FD_TELL | - rights.FD_FILESTAT_GET | - rights.PATH_CREATE_FILE, - fdflags.SYNC, - fdptr, - ); - perror("path_open"); - const fileDescriptor: fd = load(fdptr); - - // write - const buf = String.UTF8.encode(data); - const iov = changetype(memory.data(sizeof())); - iov.buf = changetype(buf); - iov.buf_len = buf.byteLength; - const written_ptr = memory.data(sizeof()); - checkMemory(changetype(buf) + buf.byteLength); - do { - ret = fd_write(fileDescriptor, changetype(iov), 1, written_ptr); - perror("fd_write"); - iov.buf_len -= load(written_ptr); - iov.buf += load(written_ptr); - } while (iov.buf_len > 0); - fd_close(fileDescriptor); } diff --git a/src/assertResult.ts b/src/assertResult.ts index c81a571..6179f10 100644 --- a/src/assertResult.ts +++ b/src/assertResult.ts @@ -1,6 +1,6 @@ import { promises } from "node:fs"; import { json2map } from "./utils/index.js"; -import { AssertErrorMessages, AssertMessages, ErrorMessages, ExpectInfo, IAssertResult } from "./interface.js"; +import { AssertErrorMessages, AssertMessage, ErrorMessages, ExpectInfo, IAssertResult } from "./interface.js"; const readFile = promises.readFile; @@ -17,7 +17,7 @@ export class AssertResult { try { const expectContent = await readFile(expectInfoFilePath, { encoding: "utf8" }); expectInfo = json2map(JSON.parse(expectContent) as ExpectInfo); - for (const [key, value] of json2map(result.failed_info)) { + for (const [key, value] of json2map(result.failed_info)) { const errorMsgs: ErrorMessages = []; for (const msg of value) { const [index, actualValue, expectValue] = msg; diff --git a/src/core/execute.ts b/src/core/execute.ts index 2afafc2..2e8a39b 100644 --- a/src/core/execute.ts +++ b/src/core/execute.ts @@ -1,17 +1,19 @@ import { WASI } from "node:wasi"; import { promises } from "node:fs"; import { ensureDirSync } from "fs-extra"; -import { basename, join } from "node:path"; +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 { IAssertResult, InstrumentResult } from "../interface.js"; +import { InstrumentResult } from "../interface.js"; import { mockInstruFunc, covInstruFunc } from "../utils/import.js"; import { supplyDefaultFunction } from "../utils/index.js"; import { parseImportFunctionInfo } from "../utils/wasmparser.js"; +import { ExecutionRecorder } from "./executionRecorder.js"; + const readFile = promises.readFile; -async function nodeExecutor(wasm: string, outFolder: string, imports: Imports) { +async function nodeExecutor(wasm: string, outFolder: string, imports: Imports): Promise { const wasi = new WASI({ args: ["node", basename(wasm)], env: process.env, @@ -21,10 +23,13 @@ async function nodeExecutor(wasm: string, outFolder: string, imports: Imports) { version: "preview1", }); + const recorder = new ExecutionRecorder(); + const importsArg = new ImportsArgument(); const userDefinedImportsObject = imports === null ? {} : imports(importsArg); const importObject: ASImports = { wasi_snapshot_preview1: wasi.wasiImport, + ...recorder.getCollectionFuncSet(importsArg), mockInstrument: mockInstruFunc, ...covInstruFunc(wasm), ...userDefinedImportsObject, @@ -45,6 +50,7 @@ async function nodeExecutor(wasm: string, outFolder: string, imports: Imports) { } throw new Error("node executor abort."); } + return recorder; } export async function execWasmBinarys( @@ -56,22 +62,9 @@ export async function execWasmBinarys( ensureDirSync(outFolder); await Promise.all( instrumentResult.map(async (res): Promise => { - await nodeExecutor(res.instrumentedWasm, outFolder, imports); const { instrumentedWasm, expectInfo } = res; - const assertLogFilePath = join(outFolder, basename(instrumentedWasm).slice(0, -4).concat("assert.log")); - - let content; - try { - content = await readFile(assertLogFilePath, { encoding: "utf8" }); - } catch (error) { - if (error instanceof Error) { - console.error(error.stack); - } - throw new Error(`maybe forget call "endTest()" at the end of "*.test.ts" or Job abort before output`); - } - const assertResult = JSON.parse(content) as IAssertResult; - - await assertRes.merge(assertResult, expectInfo); + const recorder: ExecutionRecorder = await nodeExecutor(instrumentedWasm, outFolder, imports); + await assertRes.merge(recorder, expectInfo); }) ); return assertRes; diff --git a/src/core/executionRecorder.ts b/src/core/executionRecorder.ts new file mode 100644 index 0000000..a55219e --- /dev/null +++ b/src/core/executionRecorder.ts @@ -0,0 +1,47 @@ +import { ImportsArgument } from "../index.js"; +import { AssertFailMessage, AssertMessage, IAssertResult } from "../interface.js"; + +export class ExecutionRecorder implements IAssertResult { + total: number = 0; + fail: number = 0; + failed_info: AssertFailMessage = {}; + _currentTestDescriptions: string[] = []; + + _addDescription(description: string): void { + this._currentTestDescriptions.push(description); + } + _removeDescription(): void { + this._currentTestDescriptions.pop(); + } + collectCheckResult(result: boolean, codeInfoIndex: number, actualValue: string, expectValue: string): void { + this.total++; + if (!result) { + this.fail++; + 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); + } + } + + getCollectionFuncSet(arg: ImportsArgument): Record> { + return { + __unittest_framework_env: { + addDescription: (description: number): void => { + this._addDescription(arg.exports!.__getString(description)); + }, + removeDescription: (): void => { + this._removeDescription(); + }, + collectCheckResult: (result: number, codeInfoIndex: number, actualValue: number, expectValue: number): void => { + this.collectCheckResult( + result !== 0, + codeInfoIndex, + arg.exports!.__getString(actualValue), + arg.exports!.__getString(expectValue) + ); + }, + }, + }; + } +} diff --git a/src/interface.ts b/src/interface.ts index 5ccbf33..705a646 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -51,8 +51,8 @@ export type TestCaseName = string; export type ExpectInfoIndex = string; export type AssertExpectValue = string; export type AssertActualValue = string; -export type AssertMessages = [ExpectInfoIndex, AssertActualValue, AssertExpectValue][]; -export type AssertFailMessage = Record; +export type AssertMessage = [ExpectInfoIndex, AssertActualValue, AssertExpectValue]; +export type AssertFailMessage = Record; export type ErrorMessages = string[]; export type AssertErrorMessages = Map; diff --git a/tests/ts/test/core/executionRecorder.test.ts b/tests/ts/test/core/executionRecorder.test.ts new file mode 100644 index 0000000..27fa02e --- /dev/null +++ b/tests/ts/test/core/executionRecorder.test.ts @@ -0,0 +1,56 @@ +import { ExecutionRecorder } from "../../../../src/core/executionRecorder.js"; + +describe("execution recorder", () => { + describe("description", () => { + test("add single description", () => { + const recorder = new ExecutionRecorder(); + recorder._addDescription("description"); + + recorder.collectCheckResult(false, 0, "", ""); + expect(recorder.failed_info).toHaveProperty("description"); + }); + test("add multiple descriptions", () => { + const recorder = new ExecutionRecorder(); + recorder._addDescription("description1"); + recorder._addDescription("description2"); + + recorder.collectCheckResult(false, 0, "", ""); + expect(recorder.failed_info).toHaveProperty("description1 - description2"); + }); + + test("remove descriptions", () => { + const recorder = new ExecutionRecorder(); + recorder._addDescription("description1"); + recorder._addDescription("description2"); + recorder._removeDescription(); + + recorder.collectCheckResult(false, 0, "", ""); + expect(recorder.failed_info).toHaveProperty("description1"); + }); + }); + + describe("collectCheckResult", () => { + test("collect false result", () => { + const recorder = new ExecutionRecorder(); + recorder.collectCheckResult(false, 0, "actual", "expect"); + + expect(recorder.total).toBe(1); + expect(recorder.fail).toBe(1); + }); + test("collect true results", () => { + const recorder = new ExecutionRecorder(); + recorder.collectCheckResult(true, 0, "actual1", "expect1"); + + expect(recorder.total).toBe(1); + expect(recorder.fail).toBe(0); + }); + test("collect multiple results", () => { + const recorder = new ExecutionRecorder(); + recorder.collectCheckResult(true, 0, "actual1", "expect1"); + recorder.collectCheckResult(false, 1, "actual2", "expect2"); + + expect(recorder.total).toBe(2); + expect(recorder.fail).toBe(1); + }); + }); +});