@moonwall/cli
Version:
Testing framework for the Moon family of projects
259 lines • 10.6 kB
JavaScript
import chalk from "chalk";
import path from "node:path";
import { startVitest } from "vitest/node";
import { createLogger } from "@moonwall/util";
import { clearNodeLogs } from "../internal/cmdFunctions/tempLogs";
import { commonChecks } from "../internal/launcherCommon";
import { cacheConfig, importAsyncConfig, loadEnvVars } from "../lib/configReader";
import { MoonwallContext, contextCreator, runNetworkOnly } from "../lib/globalContext";
import { shardManager } from "../lib/shardManager";
import { findTestFilesMatchingPattern } from "../internal/testIdParser";
const logger = createLogger({ name: "runner" });
/**
* Pre-filters test files by scanning for suite/test IDs matching the pattern.
* Uses ast-grep's parallel file search for efficient parsing.
* Returns matching file paths, or undefined if no pattern (let vitest handle all).
*/
async function filterTestFilesByPattern(testDirs, includePatterns, pattern) {
if (!pattern)
return undefined;
const patternRegex = new RegExp(pattern, "i");
const matches = await findTestFilesMatchingPattern(testDirs, includePatterns, patternRegex);
if (matches.length === 0) {
throw new Error(`No test files found matching pattern "${pattern}". ` +
`Check that the suite/test ID exists (e.g., D01, D01E01).`);
}
return matches;
}
export async function testCmd(envName, additionalArgs) {
await cacheConfig();
const globalConfig = await importAsyncConfig();
const env = globalConfig.environments.find(({ name }) => name === envName);
process.env.MOON_TEST_ENV = envName;
// Initialize sharding configuration
shardManager.initializeSharding(additionalArgs?.shard);
if (!env) {
const envList = globalConfig.environments
.map((env) => env.name)
.sort()
.join(", ");
throw new Error(`No environment found in config for: ${chalk.bgWhiteBright.blackBright(envName)}\n Environments defined in config are: ${envList}\n`);
}
loadEnvVars();
await commonChecks(env);
if ((env.foundation.type === "dev" && !env.foundation.launchSpec[0].retainAllLogs) ||
(env.foundation.type === "chopsticks" && !env.foundation.launchSpec[0].retainAllLogs)) {
clearNodeLogs();
}
if (env.foundation.type === "zombie") {
process.env.MOON_EXIT = "true";
}
const vitest = await executeTests(env, additionalArgs);
const failed = vitest.state
.getFiles()
.filter((file) => file.result && file.result.state === "fail");
if (failed.length === 0) {
logger.info("✅ All tests passed");
global.MOONWALL_TERMINATION_REASON = "tests finished";
return true;
}
logger.warn("❌ Some tests failed");
global.MOONWALL_TERMINATION_REASON = "tests failed";
return false;
}
export async function executeTests(env, testRunArgs) {
return new Promise(async (resolve, reject) => {
const globalConfig = await importAsyncConfig();
if (env.foundation.type === "read_only") {
try {
if (!process.env.MOON_TEST_ENV) {
throw new Error("MOON_TEST_ENV not set");
}
const ctx = await contextCreator();
const chainData = ctx.providers
.filter((provider) => provider.type === "polkadotJs" && provider.name.includes("para"))
.map((provider) => {
return {
[provider.name]: {
rtName: provider.greet().rtName,
rtVersion: provider.greet().rtVersion,
},
};
});
// TODO: Extend/develop this feature to respect para/relay chain specifications
if (chainData.length < 1) {
throw "Could not read runtime name or version \nTo fix: ensure moonwall config has a polkadotJs provider with a name containing 'para'";
}
const { rtVersion, rtName } = Object.values(chainData[0])[0];
process.env.MOON_RTVERSION = rtVersion;
process.env.MOON_RTNAME = rtName;
}
catch (e) {
logger.error(e);
}
finally {
await MoonwallContext.destroy();
}
}
const additionalArgs = { ...testRunArgs };
const vitestOptions = testRunArgs?.vitestPassthroughArgs?.reduce((acc, arg) => {
const [key, value] = arg.split("=");
return {
// biome-ignore lint/performance/noAccumulatingSpread: this is fine
...acc,
[key]: Number(value) || value,
};
}, {});
// transform in regexp pattern
if (env.skipTests && env.skipTests.length > 0) {
// the final pattern will look like this: "^((?!SO00T02|SM00T01|SM00T03).)*$"
additionalArgs.testNamePattern = `^((?!${env.skipTests?.map((test) => `${test.name}`).join("|")}).)*$`;
}
const options = new VitestOptionsBuilder()
.setReporters(env.reporters || ["default"])
.setOutputFile(env.reportFile)
.setName(env.name)
.setTimeout(env.timeout || globalConfig.defaultTestTimeout)
.setInclude(env.include || ["**/*{test,spec,test_,test-}*{ts,mts,cts}"])
.addThreadConfig(env.multiThreads)
.setCacheImports(env.cacheImports)
.addVitestPassthroughArgs(env.vitestArgs)
.build();
if (globalConfig.environments.find((env) => env.name === process.env.MOON_TEST_ENV)?.foundation
.type === "zombie") {
await runNetworkOnly();
process.env.MOON_RECYCLE = "true";
}
try {
const testFileDir = additionalArgs?.subDirectory !== undefined
? env.testFileDir.map((folder) => path.join(folder, additionalArgs.subDirectory || "error"))
: env.testFileDir;
const folders = testFileDir.map((folder) => path.join(".", folder, "/"));
const includePatterns = env.include || ["**/*{test,spec,test_,test-}*{ts,mts,cts}"];
// Pre-filter test files by scanning for suite IDs matching the pattern
// This avoids loading all files in vitest just to discover which ones match
const filteredFiles = await filterTestFilesByPattern(folders, includePatterns, additionalArgs?.testNamePattern);
const optionsToUse = {
...options,
...additionalArgs,
...vitestOptions,
...(filteredFiles ? { include: filteredFiles.map((f) => path.resolve(f)) } : {}),
};
if (env.printVitestOptions) {
logger.info(`Options to use: ${JSON.stringify(optionsToUse, null, 2)}`);
}
const foldersToUse = filteredFiles ? ["."] : folders;
resolve((await startVitest("test", foldersToUse, optionsToUse)));
}
catch (e) {
logger.error(e);
reject(e);
}
});
}
const filterList = ["<empty line>", "", "stdout | unknown test"];
class VitestOptionsBuilder {
options = {
watch: false,
globals: true,
reporters: ["default"],
passWithNoTests: false,
deps: {
optimizer: { ssr: { enabled: false }, web: { enabled: false } },
},
env: {
NODE_OPTIONS: "--no-warnings --no-deprecation",
},
include: ["**/*{test,spec,test_,test-}*{ts,mts,cts}"],
onConsoleLog(log) {
if (filterList.includes(log.trim()))
return false;
if (log.includes("has multiple versions, ensure that there is only one installed.")) {
return false;
}
},
};
setName(name) {
this.options.name = name;
return this;
}
setReporters(reporters) {
const modified = reporters.includes("basic")
? reporters.map((r) => r === "basic" ? ["default", { summary: false }] : r)
: reporters;
this.options.reporters = modified;
return this;
}
setOutputFile(file) {
if (!file) {
logger.info("No output file specified, skipping");
return this;
}
this.options.outputFile = file;
return this;
}
setTimeout(timeout) {
this.options.testTimeout = timeout;
this.options.hookTimeout = timeout;
return this;
}
setInclude(include) {
this.options.include = include;
return this;
}
addVitestPassthroughArgs(args) {
this.options = { ...this.options, ...args };
return this;
}
addThreadConfig(threads = false) {
this.options.fileParallelism = false;
this.options.pool = "forks";
this.options.poolOptions = {
forks: {
isolate: true,
minForks: 1,
maxForks: 3,
singleFork: false,
},
};
if (threads === true && process.env.MOON_RECYCLE !== "true") {
this.options.fileParallelism = true;
}
if (typeof threads === "number" && process.env.MOON_RECYCLE !== "true") {
this.options.fileParallelism = true;
if (this.options.poolOptions?.forks) {
this.options.poolOptions.forks.maxForks = threads;
this.options.poolOptions.forks.singleFork = false;
}
}
if (typeof threads === "object" && process.env.MOON_RECYCLE !== "true") {
const key = Object.keys(threads)[0];
if (["threads", "forks", "vmThreads", "typescript"].includes(key)) {
this.options.pool = key;
this.options.poolOptions = Object.values(threads)[0];
}
else {
throw new Error(`Invalid pool type: ${key}`);
}
}
return this;
}
setCacheImports(enabled) {
if (enabled) {
this.options.deps = {
optimizer: {
ssr: {
enabled: true,
include: ["viem", "ethers"],
},
web: { enabled: false },
},
};
}
return this;
}
build() {
return this.options;
}
}
//# sourceMappingURL=runTests.js.map