hardhat
Version:
Hardhat is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.
220 lines (190 loc) • 6.78 kB
text/typescript
import type { RunOptions } from "./runner.js";
import type { TestEvent } from "./types.js";
import type { BuildOptions } from "../../../types/solidity/build-system.js";
import type { NewTaskActionFunction } from "../../../types/tasks.js";
import type {
ObservabilityConfig,
SolidityTestRunnerConfigArgs,
TracingConfigWithBuffers,
} from "@nomicfoundation/edr";
import { finished } from "node:stream/promises";
import {
assertHardhatInvariant,
HardhatError,
} from "@nomicfoundation/hardhat-errors";
import { getAllFilesMatching } from "@nomicfoundation/hardhat-utils/fs";
import { resolveFromRoot } from "@nomicfoundation/hardhat-utils/path";
import { createNonClosingWriter } from "@nomicfoundation/hardhat-utils/stream";
import { HardhatRuntimeEnvironmentImplementation } from "../../core/hre.js";
import { isSupportedChainType } from "../../edr/chain-type.js";
import {
markTestRunDone,
markTestRunStart,
markTestWorkerDone,
} from "../coverage/helpers.js";
import { throwIfSolidityBuildFailed } from "../solidity/build-results.js";
import { getEdrArtifacts, getBuildInfos } from "./edr-artifacts.js";
import {
isTestSuiteArtifact,
solidityTestConfigToRunOptions,
solidityTestConfigToSolidityTestRunnerConfigArgs,
} from "./helpers.js";
import { testReporter } from "./reporter.js";
import { run } from "./runner.js";
interface TestActionArguments {
testFiles: string[];
chainType: string;
grep?: string;
noCompile: boolean;
verbosity: number;
}
const runSolidityTests: NewTaskActionFunction<TestActionArguments> = async (
{ testFiles, chainType, grep, noCompile, verbosity },
hre,
) => {
let rootFilePaths: string[];
if (!isSupportedChainType(chainType)) {
throw new HardhatError(
HardhatError.ERRORS.CORE.ARGUMENTS.INVALID_VALUE_FOR_TYPE,
{
value: chainType,
type: "ChainType",
name: "chainType",
},
);
}
// NOTE: We run the compile task first to ensure all the artifacts for them are generated
// Then, we compile just the test sources. We don't do it in one go because the user
// is likely to use different compilation options for the tests and the sources.
if (noCompile === false) {
await hre.tasks.getTask("compile").run();
}
if (testFiles.length > 0) {
rootFilePaths = testFiles.map((f) =>
resolveFromRoot(hre.config.paths.root, f),
);
} else {
// NOTE: A test file is either a file with a `.sol` extension in the `tests.solidity`
// directory or a file with a `.t.sol` extension in the `sources.solidity` directory
rootFilePaths = (
await Promise.all([
getAllFilesMatching(hre.config.paths.tests.solidity, (f) =>
f.endsWith(".sol"),
),
...hre.config.paths.sources.solidity.map(async (dir) => {
return getAllFilesMatching(dir, (f) => f.endsWith(".t.sol"));
}),
])
).flat(1);
}
// NOTE: We remove duplicates in case there is an intersection between
// the tests.solidity paths and the sources paths
rootFilePaths = Array.from(new Set(rootFilePaths));
// NOTE: We are not skipping the test compilation even if the noCompile flag is set
// because the user cannot run test compilation outside of the test task yet.
// TODO: Allow users to run test compilation outside of the test task.
const buildOptions: BuildOptions = {
force: false,
buildProfile: hre.globalOptions.buildProfile ?? "default",
quiet: true,
};
const results = await hre.solidity.build(rootFilePaths, buildOptions);
throwIfSolidityBuildFailed(results);
const buildInfos = await getBuildInfos(hre.artifacts);
const edrArtifacts = await getEdrArtifacts(hre.artifacts);
const testSuiteIds = edrArtifacts
.filter(({ userSourceName }) =>
rootFilePaths.includes(
resolveFromRoot(hre.config.paths.root, userSourceName),
),
)
.filter(({ edrAtifact }) => isTestSuiteArtifact(edrAtifact))
.map(({ edrAtifact }) => edrAtifact.id);
console.log("Running Solidity tests");
console.log();
let includesFailures = false;
let includesErrors = false;
const solidityTestConfig = hre.config.test.solidity;
let observabilityConfig: ObservabilityConfig | undefined;
if (hre.globalOptions.coverage) {
assertHardhatInvariant(
hre instanceof HardhatRuntimeEnvironmentImplementation,
"Expected HRE to be an instance of HardhatRuntimeEnvironmentImplementation",
);
observabilityConfig = {
codeCoverage: {
onCollectedCoverageCallback: async (coverageData: Uint8Array[]) => {
const tags = coverageData.map((tag) =>
Buffer.from(tag).toString("hex"),
);
await hre._coverage.addData(tags);
},
},
};
}
const config: SolidityTestRunnerConfigArgs =
solidityTestConfigToSolidityTestRunnerConfigArgs(
chainType,
hre.config.paths.root,
solidityTestConfig,
verbosity,
observabilityConfig,
grep,
);
const tracingConfig: TracingConfigWithBuffers = {
buildInfos,
ignoreContracts: false,
};
const options: RunOptions =
solidityTestConfigToRunOptions(solidityTestConfig);
const sourceNameToUserSourceName = new Map(
edrArtifacts.map(({ userSourceName, edrAtifact }) => [
edrAtifact.id.source,
userSourceName,
]),
);
await markTestRunStart("solidity");
const runStream = run(
chainType,
edrArtifacts.map(({ edrAtifact }) => edrAtifact),
testSuiteIds,
config,
tracingConfig,
sourceNameToUserSourceName,
options,
);
const testReporterStream = runStream
.on("data", (event: TestEvent) => {
if (event.type === "suite:result") {
if (event.data.testResults.some(({ status }) => status === "Failure")) {
includesFailures = true;
}
}
})
.compose((source) =>
testReporter(source, sourceNameToUserSourceName, verbosity),
);
const outputStream = testReporterStream.pipe(
createNonClosingWriter(process.stdout),
);
try {
// NOTE: We're awaiting the original run stream to finish to catch any
// errors produced by the runner.
await finished(runStream);
// We also await the output stream to finish, as we want to wait for it
// to avoid returning before the whole output was generated.
await finished(outputStream);
} catch (error) {
console.error(error);
includesErrors = true;
}
// NOTE: We collect coverage data for solidity tests in the main process.
await markTestWorkerDone("solidity");
// NOTE: This might print a coverage report.
await markTestRunDone("solidity");
if (includesFailures || includesErrors) {
process.exitCode = 1;
}
console.log();
};
export default runSolidityTests;