From 1fb5f5794040f6b6329518c5f3ed232b37f5e385 Mon Sep 17 00:00:00 2001 From: Congcong Cai Date: Thu, 9 Oct 2025 18:08:36 +0800 Subject: [PATCH 1/2] feat: implement setup and teardown --- assembly/env.ts | 8 ++++ assembly/implement.ts | 10 ++++- src/core/execute.ts | 12 ++++-- src/core/executionRecorder.ts | 38 ++++++++++++++++++- tests/e2e/run.js | 8 ++-- tests/e2e/setup-teardown/as-test.config.js | 14 +++++++ tests/e2e/setup-teardown/setup.test.ts | 26 +++++++++++++ .../setup-teardown/setup_out_of_block.test.ts | 3 ++ tests/e2e/setup-teardown/stdout.txt | 16 ++++++++ tests/e2e/setup-teardown/teardown.test.ts | 26 +++++++++++++ tests/e2e/setup-teardown/tsconfig.json | 4 ++ tests/ts/test/core/executionRecorder.test.ts | 36 +++++++++++++++++- 12 files changed, 190 insertions(+), 11 deletions(-) create mode 100644 tests/e2e/setup-teardown/as-test.config.js create mode 100644 tests/e2e/setup-teardown/setup.test.ts create mode 100644 tests/e2e/setup-teardown/setup_out_of_block.test.ts create mode 100644 tests/e2e/setup-teardown/stdout.txt create mode 100644 tests/e2e/setup-teardown/teardown.test.ts create mode 100644 tests/e2e/setup-teardown/tsconfig.json diff --git a/assembly/env.ts b/assembly/env.ts index f2a2e2a..656058b 100644 --- a/assembly/env.ts +++ b/assembly/env.ts @@ -12,6 +12,14 @@ export namespace assertResult { export declare function registerTestFunction(index: u32): void; + @external("__unittest_framework_env","registerBeforeEachFunction") + export declare function registerBeforeEachFunction(index: u32): boolean; + + + @external("__unittest_framework_env","registerAfterEachFunction") + export declare function registerAfterEachFunction(index: u32): boolean; + + @external("__unittest_framework_env","collectCheckResult") export declare function collectCheckResult( result: bool, diff --git a/assembly/implement.ts b/assembly/implement.ts index 82ad50a..a7448ac 100644 --- a/assembly/implement.ts +++ b/assembly/implement.ts @@ -15,9 +15,15 @@ export function testImpl(name: string, testFunction: () => void): void { assertResult.removeDescription(); } -export function beforeEachImpl(func: () => void): void {} +export function beforeEachImpl(func: () => void): void { + const result = assertResult.registerBeforeEachFunction(func.index); + assert(result, "register setup function failed"); +} -export function afterEachImpl(func: () => void): void {} +export function afterEachImpl(func: () => void): void { + const result = assertResult.registerAfterEachFunction(func.index); + assert(result, "register teardown function failed"); +} export function mockImpl( originalFunction: T, diff --git a/src/core/execute.ts b/src/core/execute.ts index d2534da..07f1b20 100644 --- a/src/core/execute.ts +++ b/src/core/execute.ts @@ -77,18 +77,24 @@ async function nodeExecutor( } executionRecorder.finishTestFunction(); - const execTestFunction = ins.exports["executeTestFunction"]; + const execTestFunction = ins.exports["executeTestFunction"] as (a: number) => void; assert(typeof execTestFunction === "function"); for (const testCase of executionRecorder.testCases) { if (isCrashed) { break; } - const { fullName, functionIndex } = testCase; + const { fullName, functionIndex, setupFunctions, teardownFunctions } = testCase; if (matchedTestNames.length === 0 || matchedTestNames.includes(fullName)) { executionRecorder.startTestFunction(fullName); try { - (execTestFunction as (a: number) => void)(functionIndex); + for (const setupFuncIndex of setupFunctions) { + execTestFunction(setupFuncIndex); + } + execTestFunction(functionIndex); + for (const teardownFuncIndex of teardownFunctions) { + execTestFunction(teardownFuncIndex); + } } catch (error) { await exceptionHandler(error); } diff --git a/src/core/executionRecorder.ts b/src/core/executionRecorder.ts index c6412f1..91cfc4f 100644 --- a/src/core/executionRecorder.ts +++ b/src/core/executionRecorder.ts @@ -46,15 +46,21 @@ export class ExecutionResult implements IExecutionResult { class TestBlock { constructor(public description: string) {} + setupFunctions: number[] = []; + teardownFunctions: number[] = []; } -class TestCase { +export class TestCase { fullName: string; + setupFunctions: number[]; + teardownFunctions: number[]; constructor( testBlockStack: TestBlock[], public functionIndex: number ) { this.fullName = testBlockStack.map((block) => block.description).join(" "); + this.setupFunctions = testBlockStack.flatMap((block) => block.setupFunctions); + this.teardownFunctions = testBlockStack.flatMap((block) => block.teardownFunctions); } } @@ -73,6 +79,30 @@ export class ExecutionRecorder implements UnitTestFramework { _removeDescription(): void { this.testBlockStack.pop(); } + + get lastTestBlock(): TestBlock | undefined { + return this.testBlockStack[this.testBlockStack.length - 1]; + } + // return false if error + _registerSetup(functionIndex: number): boolean { + const lastTestBlock = this.lastTestBlock; + if (lastTestBlock === undefined) { + return false; + } else { + lastTestBlock.setupFunctions.push(functionIndex); + return true; + } + } + // return false if error + _registerTeardown(functionIndex: number): boolean { + const lastTestBlock = this.lastTestBlock; + if (lastTestBlock === undefined) { + return false; + } else { + lastTestBlock.teardownFunctions.push(functionIndex); + return true; + } + } _addTestCase(functionIndex: number): void { this.testCases.push(new TestCase(this.testBlockStack, functionIndex)); } @@ -128,6 +158,12 @@ export class ExecutionRecorder implements UnitTestFramework { registerTestFunction: (index: number): void => { this._addTestCase(index); }, + registerBeforeEachFunction: (index: number): boolean => { + return this._registerSetup(index); + }, + registerAfterEachFunction: (index: number): boolean => { + return this._registerTeardown(index); + }, collectCheckResult: (result: number, codeInfoIndex: number, actualValue: number, expectValue: number): void => { this.collectCheckResult( result !== 0, diff --git a/tests/e2e/run.js b/tests/e2e/run.js index 015c28f..481e6b4 100644 --- a/tests/e2e/run.js +++ b/tests/e2e/run.js @@ -55,13 +55,15 @@ runEndToEndTest("compilationFailed", "", (error, stdout, stderr) => { assert(error.code === 2); }); +runEndToEndTest("isolated-cli", "--isolated false", (error, stdout, stderr) => {}); +runEndToEndTest("isolated-false", "", (error, stdout, stderr) => {}); +runEndToEndTest("isolated-true", "", (error, stdout, stderr) => {}); + runEndToEndTest("printLogInFailedInfo", "", (error, stdout, stderr) => { assert(error.code === 1); }); -runEndToEndTest("isolated-true", "", (error, stdout, stderr) => {}); -runEndToEndTest("isolated-false", "", (error, stdout, stderr) => {}); -runEndToEndTest("isolated-cli", "--isolated false", (error, stdout, stderr) => {}); +runEndToEndTest("setup-teardown", "", (error, stdout, stderr) => {}); runEndToEndTest( "testFiles", diff --git a/tests/e2e/setup-teardown/as-test.config.js b/tests/e2e/setup-teardown/as-test.config.js new file mode 100644 index 0000000..44a0d6f --- /dev/null +++ b/tests/e2e/setup-teardown/as-test.config.js @@ -0,0 +1,14 @@ +import path from "node:path"; + +const __dirname = path.dirname(new URL(import.meta.url).pathname); + +/** + * @type {import("../../../config.d.ts").Config} + */ +export default { + include: [__dirname], + temp: path.join(__dirname, "tmp"), + output: path.join(__dirname, "tmp"), + mode: [], + isolated: true, +}; diff --git a/tests/e2e/setup-teardown/setup.test.ts b/tests/e2e/setup-teardown/setup.test.ts new file mode 100644 index 0000000..8f5e27e --- /dev/null +++ b/tests/e2e/setup-teardown/setup.test.ts @@ -0,0 +1,26 @@ +import { test, expect, describe, beforeEach } from "../../../assembly"; + +let setup0 = 0; +let setup1 = 0; +describe("setup", () => { + beforeEach(() => { + setup0 = 10; + setup1 = 20; + }); + test("1st", () => { + expect(setup0).equal(10); + expect(setup1).equal(20); + setup0 = 100; + setup1 = 200; + }); + test("2nd", () => { + expect(setup0).equal(10); + expect(setup1).equal(20); + setup0 = 100; + setup1 = 200; + }); + test("3nd", () => { + expect(setup0).equal(10); + expect(setup1).equal(20); + }); +}); diff --git a/tests/e2e/setup-teardown/setup_out_of_block.test.ts b/tests/e2e/setup-teardown/setup_out_of_block.test.ts new file mode 100644 index 0000000..2cc5674 --- /dev/null +++ b/tests/e2e/setup-teardown/setup_out_of_block.test.ts @@ -0,0 +1,3 @@ +import { beforeEach } from "../../../assembly"; + +beforeEach(() => {}); diff --git a/tests/e2e/setup-teardown/stdout.txt b/tests/e2e/setup-teardown/stdout.txt new file mode 100644 index 0000000..0513cc3 --- /dev/null +++ b/tests/e2e/setup-teardown/stdout.txt @@ -0,0 +1,16 @@ +code analysis: OK +compile test files: OK +instrument: OK +execute test files: OK + +test case: 12/13 (success/total) + +Error Message: + tests/e2e/setup-teardown/tmp/setup_out_of_block.test - init: + Test Crashed! +Reason: unreachable + at assembly/implement/beforeEachImpl (assembly/implement.ts:20:2) + at assembly/index/beforeEach (assembly/index.ts:47:2) + at start:tests/e2e/setup-teardown/setup_out_of_block.test (tests/e2e/setup-teardown/tmp/setup_out_of_block.test.instrumented.wasm:1:547) + at ~start (tests/e2e/setup-teardown/tmp/setup_out_of_block.test.instrumented.wasm:1:325) + diff --git a/tests/e2e/setup-teardown/teardown.test.ts b/tests/e2e/setup-teardown/teardown.test.ts new file mode 100644 index 0000000..6d47e29 --- /dev/null +++ b/tests/e2e/setup-teardown/teardown.test.ts @@ -0,0 +1,26 @@ +import { test, expect, describe, afterEach } from "../../../assembly"; + +let teardown0 = 0; +let teardown1 = 0; +describe("teardown", () => { + afterEach(() => { + teardown0 = 10; + teardown1 = 20; + }); + test("1st", () => { + expect(teardown0).equal(0); + expect(teardown1).equal(0); + teardown0 = 100; + teardown1 = 200; + }); + test("2nd", () => { + expect(teardown0).equal(10); + expect(teardown1).equal(20); + teardown0 = 100; + teardown1 = 200; + }); + test("3nd", () => { + expect(teardown0).equal(10); + expect(teardown1).equal(20); + }); +}); diff --git a/tests/e2e/setup-teardown/tsconfig.json b/tests/e2e/setup-teardown/tsconfig.json new file mode 100644 index 0000000..798b474 --- /dev/null +++ b/tests/e2e/setup-teardown/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "assemblyscript/std/assembly.json", + "include": ["./**/*.ts"] +} diff --git a/tests/ts/test/core/executionRecorder.test.ts b/tests/ts/test/core/executionRecorder.test.ts index e4f6099..df4a810 100644 --- a/tests/ts/test/core/executionRecorder.test.ts +++ b/tests/ts/test/core/executionRecorder.test.ts @@ -1,4 +1,4 @@ -import { ExecutionRecorder } from "../../../../src/core/executionRecorder.js"; +import { ExecutionRecorder, TestCase } from "../../../../src/core/executionRecorder.js"; describe("execution recorder", () => { describe("description", () => { @@ -6,7 +6,6 @@ describe("execution recorder", () => { const recorder = new ExecutionRecorder(); recorder._addDescription("description"); recorder._addTestCase(1); - console.log(recorder.testCases); expect(recorder.testCases).toMatchObject([{ functionIndex: 1, fullName: "description" }]); recorder.startTestFunction("description"); @@ -39,6 +38,39 @@ describe("execution recorder", () => { }); }); + describe("setup and teardown", () => { + test("base", () => { + const recorder = new ExecutionRecorder(); + recorder._addDescription("description"); + recorder._registerSetup(10); + recorder._registerSetup(11); + recorder._registerTeardown(20); + recorder._registerTeardown(21); + recorder._addTestCase(1); + expect(recorder.testCases).toMatchObject([{ setupFunctions: [10, 11], teardownFunctions: [20, 21] }]); + }); + test("pop", () => { + const recorder = new ExecutionRecorder(); + recorder._addDescription("description 1"); + recorder._registerSetup(10); + recorder._registerTeardown(20); + + recorder._addDescription("description 2"); + recorder._registerSetup(11); + recorder._registerTeardown(21); + recorder._removeDescription(); + + recorder._addTestCase(1); + expect(recorder.testCases).toMatchObject([{ setupFunctions: [10], teardownFunctions: [20] }]); + }); + + test("out of block", () => { + const recorder = new ExecutionRecorder(); + expect(recorder._registerSetup(10)).toBe(false); + expect(recorder._registerTeardown(20)).toBe(false); + }); + }); + describe("collectCheckResult", () => { test("collect false result", () => { const recorder = new ExecutionRecorder(); From c4b90ec0391bc0e83769e716326ba570443a5ac4 Mon Sep 17 00:00:00 2001 From: Congcong Cai Date: Fri, 10 Oct 2025 11:10:31 +0800 Subject: [PATCH 2/2] doc --- docs/.vitepress/config.ts | 1 + docs/api-documents/setup-teardown.md | 36 ++++++++++++++++++ docs/release-note.md | 1 + src/core/execute.ts | 40 ++++++++++---------- src/core/executionRecorder.ts | 24 ++++++++++-- tests/e2e/run.js | 4 +- tests/e2e/setup-teardown/nest.test.ts | 28 ++++++++++++++ tests/e2e/setup-teardown/stdout.txt | 2 +- tests/ts/test/core/executionRecorder.test.ts | 8 ++-- 9 files changed, 114 insertions(+), 30 deletions(-) create mode 100644 docs/api-documents/setup-teardown.md create mode 100644 tests/e2e/setup-teardown/nest.test.ts diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 3d4b610..7986464 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -32,6 +32,7 @@ export default defineConfig({ { text: "Configuration", link: "/api-documents/configuration" }, { text: "Options", link: "/api-documents/options" }, { text: "Matchers", link: "/api-documents/matchers" }, + { text: "Setup Teardown", link: "/api-documents/setup-teardown" }, { text: "Mock Function", link: "/api-documents/mock-function" }, { text: "Report", link: "/api-documents/coverage-report" }, { text: "Return Code", link: "/api-documents/return-code.md" }, diff --git a/docs/api-documents/setup-teardown.md b/docs/api-documents/setup-teardown.md new file mode 100644 index 0000000..833fca1 --- /dev/null +++ b/docs/api-documents/setup-teardown.md @@ -0,0 +1,36 @@ +## Setup And Teardown + +Often while writing tests you have some setup work that needs to happen before tests run, and you have some finishing work that needs to happen after tests run. unittest framework provides helper functions to handle this. + +If you have some work you need to do repeatedly for many tests, you can use `beforeEach` and `afterEach` hooks. + +::: info +`beforeEach` and `afterEach` can only work inside describe which will limit its scope +::: + +### How to Use + +```ts +let setup = 0; +describe("setup", () => { + // effect for the whole describe including sub-describe + beforeEach(() => { + setup = 10; + }); + test("1st", () => { + expect(setup).equal(10); + setup = 100; + }); + test("2nd", () => { + expect(setup).equal(10); + setup = 100; + }); + test("3nd", () => { + expect(setup).equal(10); + }); +}); +``` + +:::info +If multiple `beforeEach` or `afterEach` is registered, they will be call in order. +::: diff --git a/docs/release-note.md b/docs/release-note.md index c1d4330..781de7b 100644 --- a/docs/release-note.md +++ b/docs/release-note.md @@ -6,6 +6,7 @@ - Improved the as-test performances. - Introduce new features `isolated: false` to significantly reduce test execution time in large projects. ([#73](https://github.com/wasm-ecosystem/assemblyscript-unittest-framework/pull/73)) +- Introduce setup and teardown API. ([#77](https://github.com/wasm-ecosystem/assemblyscript-unittest-framework/pull/77)) ## 1.3.1 diff --git a/src/core/execute.ts b/src/core/execute.ts index 07f1b20..f28e002 100644 --- a/src/core/execute.ts +++ b/src/core/execute.ts @@ -69,13 +69,13 @@ async function nodeExecutor( throw new Error("node executor abort"); }; - try { - executionRecorder.startTestFunction(`${instrumentResult.baseName} - init`); - wasi.start(ins); - } catch (error) { - await exceptionHandler(error); - } - executionRecorder.finishTestFunction(); + await executionRecorder.runTestFunction( + `${instrumentResult.baseName} - init`, + () => { + wasi.start(ins); + }, + exceptionHandler + ); const execTestFunction = ins.exports["executeTestFunction"] as (a: number) => void; assert(typeof execTestFunction === "function"); @@ -86,19 +86,19 @@ async function nodeExecutor( } const { fullName, functionIndex, setupFunctions, teardownFunctions } = testCase; if (matchedTestNames.length === 0 || matchedTestNames.includes(fullName)) { - executionRecorder.startTestFunction(fullName); - try { - for (const setupFuncIndex of setupFunctions) { - execTestFunction(setupFuncIndex); - } - execTestFunction(functionIndex); - for (const teardownFuncIndex of teardownFunctions) { - execTestFunction(teardownFuncIndex); - } - } catch (error) { - await exceptionHandler(error); - } - executionRecorder.finishTestFunction(); + await executionRecorder.runTestFunction( + fullName, + () => { + for (const setupFuncIndex of setupFunctions) { + execTestFunction(setupFuncIndex); + } + execTestFunction(functionIndex); + for (const teardownFuncIndex of teardownFunctions) { + execTestFunction(teardownFuncIndex); + } + }, + exceptionHandler + ); mockStatusRecorder.clear(); } } diff --git a/src/core/executionRecorder.ts b/src/core/executionRecorder.ts index 91cfc4f..2c61903 100644 --- a/src/core/executionRecorder.ts +++ b/src/core/executionRecorder.ts @@ -81,7 +81,7 @@ export class ExecutionRecorder implements UnitTestFramework { } get lastTestBlock(): TestBlock | undefined { - return this.testBlockStack[this.testBlockStack.length - 1]; + return this.testBlockStack.at(-1); } // return false if error _registerSetup(functionIndex: number): boolean { @@ -107,11 +107,11 @@ export class ExecutionRecorder implements UnitTestFramework { this.testCases.push(new TestCase(this.testBlockStack, functionIndex)); } - startTestFunction(testCaseFullName: string): void { - this.currentExecutedTestCaseFullName = testCaseFullName; + _startTestFunction(fullName: string): void { + this.currentExecutedTestCaseFullName = fullName; this.logRecorder.reset(); } - finishTestFunction(): void { + _finishTestFunction(): void { const logMessages: string[] | null = this.logRecorder.onFinishTest(); if (logMessages !== null) { this.result.failedLogMessages[this.currentExecutedTestCaseFullName] = ( @@ -119,6 +119,22 @@ export class ExecutionRecorder implements UnitTestFramework { ).concat(logMessages); } } + async runTestFunction( + fullName: string, + runner: () => Promise | void, + exceptionHandler: (error: unknown) => Promise + ) { + this._startTestFunction(fullName); + try { + const r = runner(); + if (r instanceof Promise) { + await r; + } + } catch (error) { + await exceptionHandler(error); + } + this._finishTestFunction(); + } notifyTestCrash(error: ExecutionError): void { this.logRecorder.addLog(`Reason: ${chalk.red(error.message)}`); diff --git a/tests/e2e/run.js b/tests/e2e/run.js index 481e6b4..7b15221 100644 --- a/tests/e2e/run.js +++ b/tests/e2e/run.js @@ -63,7 +63,9 @@ runEndToEndTest("printLogInFailedInfo", "", (error, stdout, stderr) => { assert(error.code === 1); }); -runEndToEndTest("setup-teardown", "", (error, stdout, stderr) => {}); +runEndToEndTest("setup-teardown", "", (error, stdout, stderr) => { + assert(error.code === 1); +}); runEndToEndTest( "testFiles", diff --git a/tests/e2e/setup-teardown/nest.test.ts b/tests/e2e/setup-teardown/nest.test.ts new file mode 100644 index 0000000..8613d50 --- /dev/null +++ b/tests/e2e/setup-teardown/nest.test.ts @@ -0,0 +1,28 @@ +import { test, expect, describe, beforeEach } from "../../../assembly"; + +let setup0 = 0; +let setup1 = 0; +describe("setup", () => { + beforeEach(() => { + setup0 = 10; + setup1 = 20; + }); + describe("nested", () => { + test("1st", () => { + expect(setup0).equal(10); + expect(setup1).equal(20); + setup0 = 100; + setup1 = 200; + }); + test("2nd", () => { + expect(setup0).equal(10); + expect(setup1).equal(20); + setup0 = 100; + setup1 = 200; + }); + }); + test("3nd", () => { + expect(setup0).equal(10); + expect(setup1).equal(20); + }); +}); diff --git a/tests/e2e/setup-teardown/stdout.txt b/tests/e2e/setup-teardown/stdout.txt index 0513cc3..0c675e0 100644 --- a/tests/e2e/setup-teardown/stdout.txt +++ b/tests/e2e/setup-teardown/stdout.txt @@ -3,7 +3,7 @@ compile test files: OK instrument: OK execute test files: OK -test case: 12/13 (success/total) +test case: 18/19 (success/total) Error Message: tests/e2e/setup-teardown/tmp/setup_out_of_block.test - init: diff --git a/tests/ts/test/core/executionRecorder.test.ts b/tests/ts/test/core/executionRecorder.test.ts index df4a810..542e2bc 100644 --- a/tests/ts/test/core/executionRecorder.test.ts +++ b/tests/ts/test/core/executionRecorder.test.ts @@ -1,4 +1,4 @@ -import { ExecutionRecorder, TestCase } from "../../../../src/core/executionRecorder.js"; +import { ExecutionRecorder } from "../../../../src/core/executionRecorder.js"; describe("execution recorder", () => { describe("description", () => { @@ -8,7 +8,7 @@ describe("execution recorder", () => { recorder._addTestCase(1); expect(recorder.testCases).toMatchObject([{ functionIndex: 1, fullName: "description" }]); - recorder.startTestFunction("description"); + recorder._startTestFunction("description"); recorder.collectCheckResult(false, 0, "", ""); expect(recorder.result.failedInfo).toHaveProperty("description"); }); @@ -19,7 +19,7 @@ describe("execution recorder", () => { recorder._addTestCase(1); expect(recorder.testCases).toMatchObject([{ functionIndex: 1, fullName: "description1 description2" }]); - recorder.startTestFunction("description1 description2"); + recorder._startTestFunction("description1 description2"); recorder.collectCheckResult(false, 0, "", ""); expect(recorder.result.failedInfo).toHaveProperty("description1 description2"); }); @@ -32,7 +32,7 @@ describe("execution recorder", () => { recorder._addTestCase(1); expect(recorder.testCases).toMatchObject([{ functionIndex: 1, fullName: "description1" }]); - recorder.startTestFunction("description1"); + recorder._startTestFunction("description1"); recorder.collectCheckResult(false, 0, "", ""); expect(recorder.result.failedInfo).toHaveProperty("description1"); });