import type { HardhatConfig } from "hardhat/types/config"; import type { NewTaskActionFunction } from "hardhat/types/tasks"; import type { TestRunResult, TestSummary } from "hardhat/types/test"; import type { LastParameter, Result } from "hardhat/types/utils"; import { pipeline } from "node:stream/promises"; import { run } from "node:test"; import { hardhatTestReporter } from "@nomicfoundation/hardhat-node-test-reporter"; import { setGlobalOptionsAsEnvVariables } from "@nomicfoundation/hardhat-utils/env"; import { getAllFilesMatching } from "@nomicfoundation/hardhat-utils/fs"; import { createNonClosingWriter } from "@nomicfoundation/hardhat-utils/stream"; import { errorResult, successfulResult } from "hardhat/utils/result"; interface TestActionArguments { testFiles: string[]; only: boolean; grep?: string; noCompile: boolean; testSummaryIndex: number; } function isTypescriptFile(path: string): boolean { return /\.(ts|cts|mts)$/i.test(path); } function isJavascriptFile(path: string): boolean { return /\.(js|cjs|mjs)$/i.test(path); } function isSubtestFailedError(error: Error): boolean { return ( "code" in error && "failureType" in error && error.code === "ERR_TEST_FAILURE" && error.failureType === "subtestsFailed" ); } async function getTestFiles( testFiles: string[], config: HardhatConfig, ): Promise { if (testFiles.length !== 0) { return testFiles; } return await getAllFilesMatching( config.paths.tests.nodejs, (f) => isJavascriptFile(f) || isTypescriptFile(f), ); } /** * Note that we are testing this manually for now as you can't run a node:test within a node:test */ const testWithHardhat: NewTaskActionFunction = async ( { testFiles, only, grep, noCompile, testSummaryIndex }, hre, ): Promise> => { // Set an environment variable that plugins can use to detect when a process is running tests process.env.HH_TEST = "true"; // Sets the NODE_ENV environment variable to "test" so the code can detect that tests are running // This is done by other JS/TS test frameworks like vitest process.env.NODE_ENV ??= "test"; setGlobalOptionsAsEnvVariables(hre.globalOptions); if (!noCompile) { const noTests = hre.config.solidity.splitTestsCompilation; await hre.tasks.getTask("build").run({ noTests }); console.log(); } const files = await getTestFiles(testFiles, hre.config); if (files.length === 0) { return successfulResult({ summary: { passed: 0, failed: 0, skipped: 0, todo: 0, }, }); } async function runTests(): Promise { const nodeTestOptions: LastParameter = { files, only, concurrency: false, isolation: "none", }; if (grep !== undefined && grep !== "") { nodeTestOptions.testNamePatterns = grep; } const testOnlyMessage = "'only' and 'runOnly' require the --only command-line option."; const customReporter = hardhatTestReporter(nodeTestOptions, { testOnlyMessage, testSummaryIndex, }); console.log("Running node:test tests"); console.log(); let failed = 0; let passed = 0; let skipped = 0; let todo = 0; let failureOutput = ""; const reporterStream = run(nodeTestOptions) .on("test:fail", (event) => { if (event.details.type === "suite") { // If a suite failed only because a subtest failed, we don't want to // count it as a failure since the subtest failure will be reported as well if (isSubtestFailedError(event.details.error)) { return; } } failed++; }) .on("test:summary", ({ counts }) => { passed = counts.passed; skipped = counts.skipped; todo = counts.todo; }) .compose(async function* (source) { const reporter = customReporter(source); for await (const value of reporter) { if (typeof value === "string") { yield value; } else { failed = value.failed; passed = value.passed; skipped = value.skipped; todo = value.todo; failureOutput = value.failureOutput; } } }); const outputStream = createNonClosingWriter(process.stdout); await pipeline(reporterStream, outputStream); return { failed, passed, skipped, todo, failureOutput, }; } await hre.hooks.runHandlerChain( "test", "onTestRunStart", ["nodejs"], async () => {}, ); const testResults = await runTests(); await hre.hooks.runHandlerChain( "test", "onTestWorkerDone", ["nodejs"], async () => {}, ); await hre.hooks.runHandlerChain( "test", "onTestRunDone", ["nodejs"], async () => {}, ); console.log(); const result: TestRunResult = { summary: testResults }; return testResults.failed > 0 ? errorResult(result) : successfulResult(result); }; export default testWithHardhat;