diff --git a/packages/rspack-test-tools/etc/api.md b/packages/rspack-test-tools/etc/api.md index 99478b6c459..b6c2a216e46 100644 --- a/packages/rspack-test-tools/etc/api.md +++ b/packages/rspack-test-tools/etc/api.md @@ -98,6 +98,9 @@ export function createErrorCase(name: string, src: string, dist: string, testCon // @public (undocumented) export function createHashCase(name: string, src: string, dist: string): void; +// @public (undocumented) +export function createHookCase(name: string, src: string, dist: string, source: string): void; + // @public (undocumented) export function createHotCase(name: string, src: string, dist: string, target: TCompilerOptions["target"]): void; @@ -260,12 +263,43 @@ export function escapeSep(str: string): string; export function formatCode(name: string, raw: string, options: IFormatCodeOptions): string; // @public (undocumented) -export function getSimpleProcessorRunner(src: string, dist: string, env: ITestEnv): (name: string, processor: ITestProcessor) => Promise; +export function getSimpleProcessorRunner(src: string, dist: string, options?: { + env?: () => ITestEnv; + context?: (src: string, dist: string) => ITestContext; +}): (name: string, processor: ITestProcessor) => Promise; + +// @public (undocumented) +export class HookCasesContext extends TestContext { + constructor(src: string, testName: string, options: TTestContextOptions); + // @internal (undocumented) + _addSnapshot(content: unknown, name: string, group: string | number): void; + // @internal (undocumented) + collectSnapshots(options?: { + diff: {}; + }): Promise; + // (undocumented) + protected count: number; + // (undocumented) + protected options: TTestContextOptions; + // (undocumented) + protected promises: Promise[]; + snapped(cb: (...args: unknown[]) => Promise, prefix?: string): (this: any, ...args: unknown[]) => Promise; + // (undocumented) + protected snapshots: Record>; + // (undocumented) + protected snapshotsList: Array; + // (undocumented) + protected src: string; + // (undocumented) + protected testName: string; +} // @public (undocumented) export class HookTaskProcessor extends SnapshotProcessor { constructor(hookOptions: IHookProcessorOptions); // (undocumented) + check(env: ITestEnv, context: HookCasesContext): Promise; + // (undocumented) config(context: ITestContext): Promise; // (undocumented) protected hookOptions: IHookProcessorOptions; diff --git a/packages/rspack-test-tools/src/case/compiler.ts b/packages/rspack-test-tools/src/case/compiler.ts index 0deb9068c94..29c4268f6e5 100644 --- a/packages/rspack-test-tools/src/case/compiler.ts +++ b/packages/rspack-test-tools/src/case/compiler.ts @@ -16,12 +16,7 @@ export function createCompilerCase( testConfig: string ) { const caseConfig: TCompilerCaseConfig = require(testConfig); - - const runner = getSimpleProcessorRunner(src, dist, { - it, - beforeEach, - afterEach - }); + const runner = getSimpleProcessorRunner(src, dist); it(caseConfig.description, async () => { await runner( diff --git a/packages/rspack-test-tools/src/case/error.ts b/packages/rspack-test-tools/src/case/error.ts index beddafd2cc4..acdcfdf0fbf 100644 --- a/packages/rspack-test-tools/src/case/error.ts +++ b/packages/rspack-test-tools/src/case/error.ts @@ -22,11 +22,7 @@ export function createErrorCase( addedSerializer = true; } const caseConfig = require(testConfig); - const runner = getSimpleProcessorRunner(src, dist, { - it, - beforeEach, - afterEach - }); + const runner = getSimpleProcessorRunner(src, dist); it(caseConfig.description, async () => { await runner( diff --git a/packages/rspack-test-tools/src/case/hook.ts b/packages/rspack-test-tools/src/case/hook.ts new file mode 100644 index 00000000000..a712a77c124 --- /dev/null +++ b/packages/rspack-test-tools/src/case/hook.ts @@ -0,0 +1,46 @@ +import path from "path"; +import { getSimpleProcessorRunner } from "../test/simple"; +import { HookCasesContext, HookTaskProcessor } from "../processor"; +import { BasicRunnerFactory } from "../runner"; +import { ECompilerType, TCompilerOptions } from "../type"; +import createLazyTestEnv from "../helper/legacy/createLazyTestEnv"; + +export function createHookCase( + name: string, + src: string, + dist: string, + source: string +) { + const caseConfig = require(path.join(src, "test.js")); + const testName = path.basename( + name.slice(0, name.indexOf(path.extname(name))) + ); + const runner = getSimpleProcessorRunner(source, dist, { + env: () => env, + context: () => + new HookCasesContext(src, testName, { + src: source, + dist: dist, + runnerFactory: BasicRunnerFactory + }) + }); + + it(caseConfig.description, async () => { + await runner( + name, + new HookTaskProcessor({ + name, + compilerType: ECompilerType.Rspack, + findBundle: function ( + i: number, + options: TCompilerOptions + ) { + return ["main.js"]; + }, + snapshot: path.join(src, "output.snap.txt"), + ...caseConfig + }) + ); + }); + const env = createLazyTestEnv(10000); +} diff --git a/packages/rspack-test-tools/src/case/index.ts b/packages/rspack-test-tools/src/case/index.ts index 868bf01c014..07625264bf0 100644 --- a/packages/rspack-test-tools/src/case/index.ts +++ b/packages/rspack-test-tools/src/case/index.ts @@ -13,3 +13,4 @@ export * from "./hot-step"; export * from "./compiler"; export * from "./stats-api"; export * from "./error"; +export * from "./hook"; diff --git a/packages/rspack-test-tools/src/case/stats-api.ts b/packages/rspack-test-tools/src/case/stats-api.ts index eadea7a3932..ddcf6b84c64 100644 --- a/packages/rspack-test-tools/src/case/stats-api.ts +++ b/packages/rspack-test-tools/src/case/stats-api.ts @@ -25,11 +25,7 @@ export function createStatsAPICase( addedSerializer = true; } const caseConfig: TStatsAPICaseConfig = require(testConfig); - const runner = getSimpleProcessorRunner(src, dist, { - it, - beforeEach, - afterEach - }); + const runner = getSimpleProcessorRunner(src, dist); it(caseConfig.description, async () => { await runner( diff --git a/packages/rspack-test-tools/src/processor/diagnostic.ts b/packages/rspack-test-tools/src/processor/diagnostic.ts index 4ab1a6790a2..756c8af547d 100644 --- a/packages/rspack-test-tools/src/processor/diagnostic.ts +++ b/packages/rspack-test-tools/src/processor/diagnostic.ts @@ -53,7 +53,8 @@ export class RspackDiagnosticProcessor extends BasicTaskProcessor s.trim()) .join(""); diff --git a/packages/rspack-test-tools/src/processor/hook.ts b/packages/rspack-test-tools/src/processor/hook.ts index 4eff8be8802..5feba8c37d5 100644 --- a/packages/rspack-test-tools/src/processor/hook.ts +++ b/packages/rspack-test-tools/src/processor/hook.ts @@ -1,5 +1,178 @@ -import { ISnapshotProcessorOptions, SnapshotProcessor } from "."; -import { ECompilerType, ITestContext, TCompilerOptions } from "../type"; +import path from "path"; +import { + ECompilerType, + ITestContext, + ITestEnv, + TCompilerOptions +} from "../type"; +import { Source } from "webpack-sources"; +import { Compilation, Compiler } from "@rspack/core"; +import { format as prettyFormat, PrettyFormatOptions } from "pretty-format"; +import { getSerializers } from "jest-snapshot"; +import { TTestContextOptions, TestContext } from "../test/context"; +import { ISnapshotProcessorOptions, SnapshotProcessor } from "./snapshot"; + +const pathSerializer = require("jest-serializer-path"); +const normalizePaths = pathSerializer.normalizePaths; +const srcDir = path.resolve(__dirname, "../../tests/fixtures"); +const distDir = path.resolve(__dirname, "../../tests/js/hook"); + +const sourceSerializer = { + test(val: unknown) { + return val instanceof Source; + }, + print(val: Source) { + return val.source(); + } +}; + +const internalSerializer = { + test(val: unknown) { + return val instanceof Compiler || val instanceof Compilation; + }, + print(val: Compiler | Compilation) { + return JSON.stringify(`${val.constructor.name}(internal ignored)`); + } +}; + +const testPathSerializer = { + test(val: unknown) { + return typeof val === "string"; + }, + print(val: string) { + return JSON.stringify( + normalizePaths( + // @ts-ignore + val + .split(srcDir) + .join("") + .split(distDir) + .join("") + ) + ); + } +}; + +const escapeRegex = true; +const printFunctionName = false; +const normalizeNewlines = (str: string) => str.replace(/\r\n|\r/g, "\n"); +const serialize = (val: unknown, indent = 2, formatOverrides = {}) => + normalizeNewlines( + prettyFormat(val, { + escapeRegex, + indent, + plugins: [ + ...getSerializers(), + sourceSerializer, + internalSerializer, + testPathSerializer + ] as PrettyFormatOptions["plugins"], + printFunctionName, + ...formatOverrides + }) + ); + +export class HookCasesContext extends TestContext { + protected promises: Promise[] = []; + protected count: number = 0; + protected snapshots: Record< + string | number, + Array<[string | Buffer, string]> + > = {}; + protected snapshotsList: Array = []; + + constructor( + protected src: string, + protected testName: string, + protected options: TTestContextOptions + ) { + super(options); + this.snapped = this.snapped.bind(this); + } + + /** + * Snapshot function arguments and return value. + * Generated snapshot is located in the same directory with the test source. + * @example + * compiler.hooks.compilation("name", context.snapped((...args) => { ... })) + */ + snapped(cb: (...args: unknown[]) => Promise, prefix = "") { + // eslint-disable-next-line + let context = this; + return function SNAPPED_HOOK(this: any, ...args: unknown[]) { + let group = prefix ? prefix : context.count++; + context._addSnapshot(args, "input", group); + let output = cb.apply(this, args); + if (output && typeof output.then === "function") { + let resolve; + context.promises.push(new Promise(r => (resolve = r))); + return output + .then((o: unknown) => { + context._addSnapshot(o, "output (Promise resolved)", group); + return o; + }) + .catch((o: unknown) => { + context._addSnapshot(o, "output (Promise rejected)", group); + return o; + }) + .finally(resolve); + } + context._addSnapshot(output, "output", group); + return output; + }; + } + + /** + * @internal + */ + _addSnapshot(content: unknown, name: string, group: string | number) { + content = Buffer.isBuffer(content) + ? content + : serialize(content, undefined, { + escapeString: true, + printBasicPrototype: true + }).replace(/\r\n/g, "\n"); + (this.snapshots[group] = this.snapshots[group] || []).push([ + content as Buffer | string, + name + ]); + if (!this.snapshotsList.includes(group)) { + this.snapshotsList.push(group); + } + } + + /** + * @internal + */ + async collectSnapshots( + options = { + diff: {} + } + ) { + await Promise.allSettled(this.promises); + if (!this.snapshotsList.length) return; + + let snapshots = this.snapshotsList.reduce((acc, group, index) => { + let block = this.snapshots[group || index].reduce( + (acc, [content, name]) => { + name = `## ${name || `test: ${index}`}\n\n`; + let block = "```javascript\n" + content + "\n```\n"; + return (acc += name + block + "\n"); + }, + "" + ); + group = Number.isInteger(group) ? `Group: ${index}` : group; + group = `# ${group}\n\n`; + return (acc += group + block); + }, ""); + + // @ts-ignore + expect(snapshots).toMatchFileSnapshot( + path.join(this.src, "hooks.snap.txt"), + options + ); + } +} interface IHookProcessorOptions extends ISnapshotProcessorOptions { @@ -44,4 +217,9 @@ export class HookTaskProcessor extends SnapshotProcessor { compiler.mergeOptions(this.hookOptions.options(context)); } } + + async check(env: ITestEnv, context: HookCasesContext) { + await (context as any).collectSnapshots(); + await super.check(env, context); + } } diff --git a/packages/rspack-test-tools/src/test/simple.ts b/packages/rspack-test-tools/src/test/simple.ts index 999a01db2a2..b3e4c40590e 100644 --- a/packages/rspack-test-tools/src/test/simple.ts +++ b/packages/rspack-test-tools/src/test/simple.ts @@ -1,4 +1,4 @@ -import { ITestEnv, ITestProcessor } from "../type"; +import { ITestContext, ITestEnv, ITestProcessor } from "../type"; import { TestContext } from "./context"; const CONTEXT_MAP: Map< @@ -9,14 +9,18 @@ const CONTEXT_MAP: Map< export function getSimpleProcessorRunner( src: string, dist: string, - env: ITestEnv + options: { + env?: () => ITestEnv; + context?: (src: string, dist: string) => ITestContext; + } = {} ) { + const createEnv = options.env || (() => ({ it, beforeEach, afterEach })); + const createContext = + options.context || + ((src: string, dist: string) => new TestContext({ src, dist })); const key = `src: ${src}, dist: ${dist}`; if (!CONTEXT_MAP.has(key)) { - const context = new TestContext({ - src, - dist - }); + const context = createContext(src, dist); const runner = async function run(name: string, processor: ITestProcessor) { try { await processor.beforeAll?.(context); @@ -27,8 +31,8 @@ export function getSimpleProcessorRunner( } catch (e: unknown) { context.emitError(name, e as Error); } finally { - await processor.run?.(env, context); - await processor.check?.(env, context); + await processor.run?.(createEnv(), context); + await processor.check?.(createEnv(), context); await processor.after?.(context); await processor.afterAll?.(context); } diff --git a/packages/rspack-test-tools/tests/Hook.test.js b/packages/rspack-test-tools/tests/Hook.test.js new file mode 100644 index 00000000000..fa1e471451a --- /dev/null +++ b/packages/rspack-test-tools/tests/Hook.test.js @@ -0,0 +1,7 @@ +const path = require("path"); +const { createHookCase, describeByWalk } = require("../dist"); +const source = path.resolve(__dirname, "./fixtures"); + +describeByWalk(__filename, (name, src, dist) => { + createHookCase(name, src, dist, source); +}); diff --git a/packages/rspack-test-tools/tests/HookCases.test.js b/packages/rspack-test-tools/tests/HookCases.test.js deleted file mode 100644 index 3e10aeaeeb6..00000000000 --- a/packages/rspack-test-tools/tests/HookCases.test.js +++ /dev/null @@ -1,230 +0,0 @@ -const path = require("path"); -const fs = require("fs"); -const { Compiler, Compilation } = require("@rspack/core"); -const { getSerializers } = require("jest-snapshot"); -const pathSerializer = require("jest-serializer-path"); -const prettyFormat = require("pretty-format"); -const createLazyTestEnv = require("../src/helper/legacy/createLazyTestEnv"); -const { Source } = require("webpack-sources"); -const normalizePaths = pathSerializer.normalizePaths; -const srcDir = path.resolve(__dirname, "./fixtures"); -const distDir = path.resolve(__dirname, "./js/HookTestCases"); -const caseDir = path.resolve(__dirname, "./hookCases"); -const { - HookTaskProcessor, - TestContext, - ECompilerType, - isValidCaseDirectory, - isDirectory, - BasicRunnerFactory -} = require(".."); - -const sourceSerializer = { - test(val) { - return val instanceof Source; - }, - print(val) { - return val.source(); - } -}; - -const internalSerializer = { - test(val) { - return val instanceof Compiler || val instanceof Compilation; - }, - print(val) { - return JSON.stringify(`${val.constructor.name}(internal ignored)`); - } -}; - -const testPathSerializer = { - test(val) { - return typeof val === "string"; - }, - print(val) { - return JSON.stringify( - normalizePaths( - val - .replaceAll(srcDir, "") - .replaceAll(distDir, "") - ) - ); - } -}; - -const escapeRegex = true; -const printFunctionName = false; -const normalizeNewlines = string => string.replace(/\r\n|\r/g, "\n"); -const serialize = (val, indent = 2, formatOverrides = {}) => - normalizeNewlines( - prettyFormat.format(val, { - escapeRegex, - indent, - plugins: [ - ...getSerializers(), - sourceSerializer, - internalSerializer, - testPathSerializer - ], - printFunctionName, - ...formatOverrides - }) - ); - -class HookCasesContext extends TestContext { - constructor(name, testName, options) { - super(options); - this.snapshots = {}; - this.snapshotsList = []; - this.name = name; - this.testName = testName; - this.promises = []; - this.snapped = this.snapped.bind(this); - this.count = 0; - } - - /** - * Snapshot function arguments and return value. - * Generated snapshot is located in the same directory with the test source. - * @example - * compiler.hooks.compilation("name", context.snapped((...args) => { ... })) - */ - snapped(cb, prefix = "") { - let context = this; - return function SNAPPED_HOOK(...args) { - let group = prefix ? prefix : context.count++; - context._addSnapshot(args, "input", group); - let output = cb.apply(this, args); - if (output && typeof output.then === "function") { - let resolve; - context.promises.push(new Promise(r => (resolve = r))); - return output - .then(o => { - context._addSnapshot(o, "output (Promise resolved)", group); - return o; - }) - .catch(o => { - context._addSnapshot(o, "output (Promise rejected)", group); - return o; - }) - .finally(resolve); - } - context._addSnapshot(output, "output", group); - return output; - }; - } - - /** - * @internal - */ - _addSnapshot(content, name, group) { - content = Buffer.isBuffer(content) - ? content - : serialize(content, undefined, { - escapeString: true, - printBasicPrototype: true - }).replace(/\r\n/g, "\n"); - (this.snapshots[group] = this.snapshots[group] || []).push([content, name]); - if (!this.snapshotsList.includes(group)) { - this.snapshotsList.push(group); - } - } - - /** - * @internal - */ - async collectSnapshots( - options = { - diff: {} - } - ) { - await Promise.allSettled(this.promises); - if (!this.snapshotsList.length) return; - - let snapshots = this.snapshotsList.reduce((acc, group, index) => { - let block = this.snapshots[group || index].reduce( - (acc, [content, name]) => { - name = `## ${name || `test: ${index}`}\n\n`; - let block = "```javascript\n" + content + "\n```\n"; - return (acc += name + block + "\n"); - }, - "" - ); - group = Number.isInteger(group) ? `Group: ${index}` : group; - group = `# ${group}\n\n`; - return (acc += group + block); - }, ""); - - expect(snapshots).toMatchFileSnapshot( - path.join(path.dirname(this.name), "hooks.snap.txt"), - options - ); - } -} - -describe("Hook", () => { - const categories = fs - .readdirSync(caseDir) - .filter(isValidCaseDirectory) - .filter(folder => isDirectory(path.join(caseDir, folder))) - .map(cat => { - return { - name: cat, - tests: fs - .readdirSync(path.join(caseDir, cat)) - .map(i => { - if (isDirectory(path.join(caseDir, cat, i))) { - return i; - } - }) - .filter(Boolean) - .sort() - }; - }); - - for (let cat of categories) { - describe(cat.name, () => { - for (let name of cat.tests) { - async function run(_name, testName, processor) { - const context = new HookCasesContext(_name, testName, { - src: srcDir, - dist: path.join(distDir, cat.name, name), - runnerFactory: BasicRunnerFactory - }); - try { - await processor.before(context); - await processor.config(context); - await processor.compiler(context); - await processor.build(context); - await processor.run(env, context); - } catch (e) { - throw e; - } finally { - await context.collectSnapshots(); - await processor.check(null, context); - await processor.after(context); - } - } - - let file = path.join(caseDir, cat.name, name, "test.js"); - const caseConfig = require(file); - it(caseConfig.description, async () => { - await run( - file, - path.basename(name.slice(0, name.indexOf(path.extname(name)))), - new HookTaskProcessor({ - name: file, - compilerType: ECompilerType.Rspack, - findBundle: function (i, options) { - return ["main.js"]; - }, - snapshot: path.join(caseDir, cat.name, name, "output.snap.txt"), - ...caseConfig - }) - ); - }); - const env = createLazyTestEnv(1000); - } - }); - } -});