@moonwall/cli
Version:
Testing framework for the Moon family of projects
1,524 lines (1,498 loc) • 101 kB
JavaScript
var __defProp = Object.defineProperty;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __esm = (fn, res) => function __init() {
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
};
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
// src/lib/configReader.ts
var configReader_exports = {};
__export(configReader_exports, {
cacheConfig: () => cacheConfig,
configExists: () => configExists,
configSetup: () => configSetup,
getEnvironmentFromConfig: () => getEnvironmentFromConfig,
importAsyncConfig: () => importAsyncConfig,
importConfig: () => importConfig,
importJsonConfig: () => importJsonConfig,
isEthereumDevConfig: () => isEthereumDevConfig,
isEthereumZombieConfig: () => isEthereumZombieConfig,
isOptionSet: () => isOptionSet,
loadEnvVars: () => loadEnvVars,
parseZombieConfigForBins: () => parseZombieConfigForBins
});
import "@moonbeam-network/api-augment";
import { readFile, access } from "fs/promises";
import { readFileSync, existsSync, constants } from "fs";
import JSONC from "jsonc-parser";
import path, { extname } from "path";
async function configExists() {
try {
await access(process.env.MOON_CONFIG_PATH || "", constants.R_OK);
return true;
} catch {
return false;
}
}
function configSetup(args) {
if (args.includes("--configFile") || process.argv.includes("-c")) {
const index = process.argv.indexOf("--configFile") !== -1 ? process.argv.indexOf("--configFile") : process.argv.indexOf("-c") !== -1 ? process.argv.indexOf("-c") : 0;
if (index === 0) {
throw new Error("Invalid configFile argument");
}
const configFile = process.argv[index + 1];
if (!existsSync(configFile)) {
throw new Error(`Config file not found at "${configFile}"`);
}
process.env.MOON_CONFIG_PATH = configFile;
}
if (!process.env.MOON_CONFIG_PATH) {
process.env.MOON_CONFIG_PATH = "moonwall.config.json";
}
}
async function parseConfig(filePath) {
let result;
const file = await readFile(filePath, "utf8");
switch (extname(filePath)) {
case ".json":
result = JSON.parse(file);
break;
case ".config":
result = JSONC.parse(file);
break;
default:
result = void 0;
break;
}
return result;
}
function parseConfigSync(filePath) {
let result;
const file = readFileSync(filePath, "utf8");
switch (extname(filePath)) {
case ".json":
result = JSON.parse(file);
break;
case ".config":
result = JSONC.parse(file);
break;
default:
result = void 0;
break;
}
return result;
}
async function importConfig(configPath) {
return await import(configPath);
}
function isOptionSet(option) {
const env = getEnvironmentFromConfig();
const optionValue = traverseConfig(env, option);
return optionValue !== void 0;
}
function isEthereumZombieConfig() {
const config = importJsonConfig();
const env = getEnvironmentFromConfig();
return env.foundation.type === "zombie" && !env.foundation.zombieSpec.disableDefaultEthProviders;
}
function isEthereumDevConfig() {
const config = importJsonConfig();
const env = getEnvironmentFromConfig();
return env.foundation.type === "dev" && !env.foundation.launchSpec[0].disableDefaultEthProviders;
}
async function cacheConfig() {
const configPath = process.env.MOON_CONFIG_PATH;
if (!configPath) {
throw new Error(`Environment ${process.env.MOON_TEST_ENV} not found in config`);
}
const filePath = path.isAbsolute(configPath) ? configPath : path.join(process.cwd(), configPath);
try {
const config = parseConfigSync(filePath);
const replacedConfig = replaceEnvVars(config);
cachedConfig = replacedConfig;
} catch (e) {
console.error(e);
throw new Error(`Error import config at ${filePath}`);
}
}
function getEnvironmentFromConfig() {
const globalConfig = importJsonConfig();
const config = globalConfig.environments.find(({ name }) => name === process.env.MOON_TEST_ENV);
if (!config) {
throw new Error(`Environment ${process.env.MOON_TEST_ENV} not found in config`);
}
return config;
}
function importJsonConfig() {
if (cachedConfig) {
return cachedConfig;
}
const configPath = process.env.MOON_CONFIG_PATH;
if (!configPath) {
throw new Error("No moonwall config path set. This is a defect, please raise it.");
}
const filePath = path.isAbsolute(configPath) ? configPath : path.join(process.cwd(), configPath);
try {
const config = parseConfigSync(filePath);
const replacedConfig = replaceEnvVars(config);
cachedConfig = replacedConfig;
return cachedConfig;
} catch (e) {
console.error(e);
throw new Error(`Error import config at ${filePath}`);
}
}
async function importAsyncConfig() {
if (cachedConfig) {
return cachedConfig;
}
const configPath = process.env.MOON_CONFIG_PATH;
if (!configPath) {
throw new Error("No moonwall config path set. This is a defect, please raise it.");
}
const filePath = path.isAbsolute(configPath) ? configPath : path.join(process.cwd(), configPath);
try {
const config = await parseConfig(filePath);
const replacedConfig = replaceEnvVars(config);
cachedConfig = replacedConfig;
return cachedConfig;
} catch (e) {
console.error(e);
throw new Error(`Error import config at ${filePath}`);
}
}
function loadEnvVars() {
const env = getEnvironmentFromConfig();
for (const envVar of env.envVars || []) {
const [key, value] = envVar.split("=");
process.env[key] = value;
}
}
function replaceEnvVars(value) {
if (typeof value === "string") {
return value.replace(/\$\{([^}]+)\}/g, (match, group) => {
const envVarValue = process.env[group];
return envVarValue || match;
});
}
if (Array.isArray(value)) {
return value.map(replaceEnvVars);
}
if (typeof value === "object" && value !== null) {
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, replaceEnvVars(v)]));
}
return value;
}
function traverseConfig(configObj, option) {
if (typeof configObj !== "object" || !configObj) return void 0;
if (Object.prototype.hasOwnProperty.call(configObj, option)) {
return configObj[option];
}
for (const key in configObj) {
const result = traverseConfig(configObj[key], option);
if (result !== void 0) {
return result;
}
}
return void 0;
}
function parseZombieConfigForBins(zombieConfigPath) {
const config = JSON.parse(readFileSync(zombieConfigPath, "utf8"));
const commands = [];
if (config.relaychain?.default_command) {
commands.push(path.basename(config.relaychain.default_command));
}
if (config.parachains) {
for (const parachain of config.parachains) {
if (parachain.collator?.command) {
commands.push(path.basename(parachain.collator.command));
}
}
}
return [...new Set(commands)].sort();
}
var cachedConfig;
var init_configReader = __esm({
"src/lib/configReader.ts"() {
"use strict";
}
});
// src/internal/logging.ts
var originalWrite = process.stderr.write.bind(process.stderr);
var blockList = [
"has multiple versions, ensure that there is only one installed",
"Unable to map [u8; 32] to a lookup index"
];
process.stderr.write = (chunk, encodingOrCallback, callback) => {
let shouldWrite = true;
if (typeof chunk === "string") {
shouldWrite = !blockList.some((phrase) => chunk.includes(phrase));
}
if (shouldWrite) {
if (typeof encodingOrCallback === "function") {
return originalWrite.call(process.stderr, chunk, void 0, encodingOrCallback);
}
return originalWrite.call(process.stderr, chunk, encodingOrCallback, callback);
}
const cb = typeof encodingOrCallback === "function" ? encodingOrCallback : callback;
if (cb) cb(null);
return true;
};
// src/internal/cmdFunctions/downloader.ts
import { SingleBar, Presets } from "cli-progress";
import fs from "fs";
import { Readable } from "stream";
async function downloader(url, outputPath) {
const tempPath = `${outputPath}.tmp`;
const writeStream = fs.createWriteStream(tempPath);
let transferredBytes = 0;
if (url.startsWith("ws")) {
console.log("You've passed a WebSocket URL to fetch. Is this intended?");
}
const headers = {};
if (process.env.GITHUB_TOKEN) {
headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
}
const response = await fetch(url, { headers });
if (!response.body) {
throw new Error("No response body");
}
const readStream = Readable.fromWeb(response.body);
const contentLength = Number.parseInt(response.headers.get("Content-Length") || "0");
const progressBar = initializeProgressBar();
progressBar.start(contentLength, 0);
readStream.pipe(writeStream);
await new Promise((resolve, reject) => {
readStream.on("data", (chunk) => {
transferredBytes += chunk.length;
progressBar.update(transferredBytes);
});
readStream.on("end", () => {
writeStream.end();
progressBar.stop();
process.stdout.write(" \u{1F4BE} Saving binary artifact...");
writeStream.close(() => resolve());
});
readStream.on("error", (error) => {
reject(error);
});
});
fs.writeFileSync(outputPath, fs.readFileSync(tempPath));
fs.rmSync(tempPath);
}
function initializeProgressBar() {
const options = {
etaAsynchronousUpdate: true,
etaBuffer: 40,
format: "Downloading: [{bar}] {percentage}% | ETA: {eta_formatted} | {value}/{total}"
};
return new SingleBar(options, Presets.shades_classic);
}
// src/internal/cmdFunctions/fetchArtifact.ts
import fs2 from "fs/promises";
import path2 from "path";
import semver from "semver";
import chalk from "chalk";
// src/internal/processHelpers.ts
import child_process from "child_process";
import { promisify } from "util";
import { createLogger } from "@moonwall/util";
var logger = createLogger({ name: "actions:runner" });
var debug = logger.debug.bind(logger);
var execAsync = promisify(child_process.exec);
var withTimeout = (promise, ms) => {
return Promise.race([
promise,
new Promise(
(_, reject) => setTimeout(() => reject(new Error("Operation timed out")), ms)
)
]);
};
async function runTask(cmd, { cwd, env } = {
cwd: process.cwd()
}, title) {
debug(`${title ? `Title: ${title}
` : ""}Running task on directory ${cwd}: ${cmd}
`);
try {
const result = await execAsync(cmd, { cwd, env });
return result.stdout;
} catch (error) {
const status = error.status ? `[${error.status}]` : "[Unknown Status]";
const message = error.message ? `${error.message}` : "No Error Message";
debug(`Caught exception in command execution. Error[${status}] ${message}`);
throw error;
}
}
async function spawnTask(cmd, { cwd, env } = {
cwd: process.cwd()
}, title) {
debug(`${title ? `Title: ${title}
` : ""}Running task on directory ${process.cwd()}: ${cmd}
`);
try {
const process2 = child_process.spawn(
cmd.split(" ")[0],
cmd.split(" ").slice(1).filter((a) => a.length > 0),
{
cwd,
env
}
);
return process2;
} catch (error) {
const status = error.status ? `[${error.status}]` : "[Unknown Status]";
const message = error.message ? `${error.message}` : "No Error Message";
debug(`Caught exception in command execution. Error[${status}] ${message}
`);
throw error;
}
}
// src/internal/cmdFunctions/fetchArtifact.ts
import { minimatch } from "minimatch";
// src/lib/repoDefinitions/moonbeam.ts
var repo = {
name: "moonbeam",
binaries: [
{
name: "moonbeam",
defaultArgs: [
"--no-hardware-benchmarks",
"--no-telemetry",
"--reserved-only",
"--rpc-cors=all",
"--unsafe-rpc-external",
"--unsafe-force-node-key-generation",
"--no-grandpa",
"--sealing=manual",
"--force-authoring",
"--no-prometheus",
"--alice",
"--chain=moonbase-dev",
"--tmp"
]
},
{ name: "moonbase-runtime" },
{ name: "moonbeam-runtime" },
{ name: "moonriver-runtime" }
],
ghAuthor: "moonbeam-foundation",
ghRepo: "moonbeam"
};
var moonbeam_default = repo;
// src/lib/repoDefinitions/polkadot.ts
var repo2 = {
name: "polkadot",
binaries: [
{ name: "polkadot" },
{ name: "polkadot-prepare-worker" },
{ name: "polkadot-execute-worker" }
],
ghAuthor: "paritytech",
ghRepo: "polkadot-sdk"
};
var polkadot_default = repo2;
// src/lib/repoDefinitions/tanssi.ts
var repo3 = {
name: "tanssi",
binaries: [
{
name: "tanssi-node",
defaultArgs: ["--dev", "--sealing=manual", "--no-hardware-benchmarks"]
},
{ name: "container-chain-template-simple-node" },
{ name: "container-chain-template-frontier-node" }
],
ghAuthor: "moondance-labs",
ghRepo: "tanssi"
};
var tanssi_default = repo3;
// src/lib/repoDefinitions/index.ts
init_configReader();
async function allReposAsync() {
const defaultRepos = [moonbeam_default, polkadot_default, tanssi_default];
const globalConfig = await importAsyncConfig();
const importedRepos = globalConfig.additionalRepos || [];
return [...defaultRepos, ...importedRepos];
}
function standardRepos() {
const defaultRepos = [moonbeam_default, polkadot_default, tanssi_default];
return [...defaultRepos];
}
// src/internal/cmdFunctions/fetchArtifact.ts
init_configReader();
import { execSync } from "child_process";
import { Octokit } from "@octokit/rest";
import { confirm } from "@inquirer/prompts";
var octokit = new Octokit({
baseUrl: "https://api.github.com",
log: {
debug: () => {
},
info: () => {
},
warn: console.warn,
error: console.error
}
});
async function fetchArtifact(args) {
if (args.path && await fs2.access(args.path).catch(() => true)) {
console.log("Folder not exists, creating");
fs2.mkdir(args.path);
}
const checkOverwrite = async (path10) => {
try {
await fs2.access(path10, fs2.constants.R_OK);
if (args.overwrite) {
console.log("File exists, overwriting ...");
} else {
const cont = await confirm({
message: "File exists, do you want to overwrite?"
});
if (!cont) {
return false;
}
}
} catch {
console.log("File does not exist, creating ...");
}
return true;
};
const binary = args.bin;
const repos = await configExists() ? await allReposAsync() : standardRepos();
const repo4 = repos.find((network) => network.binaries.find((bin) => bin.name === binary));
if (!repo4) {
throw new Error(`Downloading ${binary} unsupported`);
}
const enteredPath = args.path ? args.path : "tmp/";
const releases = await octokit.rest.repos.listReleases({
owner: repo4.ghAuthor,
repo: repo4.ghRepo
});
if (releases.status !== 200 || releases.data.length === 0) {
throw new Error(`No releases found for ${repo4.ghAuthor}.${repo4.ghRepo}, try again later.`);
}
const release = binary.includes("-runtime") ? releases.data.find((release2) => {
if (args.ver === "latest") {
return release2.assets.find((asset2) => asset2.name.includes(binary));
}
return release2.assets.find((asset2) => asset2.name === `${binary}-${args.ver}.wasm`);
}) : args.ver === "latest" ? releases.data.find((release2) => release2.assets.find((asset2) => asset2.name === binary)) : releases.data.filter((release2) => release2.tag_name.includes(args.ver || "")).find((release2) => release2.assets.find((asset2) => minimatch(asset2.name, binary)));
if (!release) {
throw new Error(`Release not found for ${args.ver}`);
}
const asset = binary.includes("-runtime") ? release.assets.find((asset2) => asset2.name.includes(binary) && asset2.name.includes("wasm")) : release.assets.find((asset2) => minimatch(asset2.name, binary));
if (!asset) {
throw new Error(`Asset not found for ${binary}`);
}
if (!binary.includes("-runtime")) {
const url = asset.browser_download_url;
const filename = path2.basename(url);
const binPath = args.outputName ? args.outputName : path2.join("./", enteredPath, filename);
if (await checkOverwrite(binPath) === false) {
console.log("User chose not to overwrite existing file, exiting.");
return;
}
await downloader(url, binPath);
await fs2.chmod(binPath, "755");
if (filename.endsWith(".tar.gz")) {
const outputBuffer = execSync(`tar -xzvf ${binPath}`);
const cleaned = outputBuffer.toString().split("\n")[0].split("/")[0];
const version2 = (await runTask(`./${cleaned} --version`)).trim();
process.stdout.write(` ${chalk.green(version2.trim())} \u2713
`);
return;
}
const version = (await runTask(`./${binPath} --version`)).trim();
process.stdout.write(`${path2.basename(binPath)} ${chalk.green(version.trim())} \u2713
`);
return;
}
const binaryPath = args.outputName ? args.outputName : path2.join("./", args.path || "", `${args.bin}-${args.ver}.wasm`);
if (await checkOverwrite(binaryPath) === false) {
console.log("User chose not to overwrite existing file, exiting.");
return;
}
await downloader(asset.browser_download_url, binaryPath);
await fs2.chmod(binaryPath, "755");
process.stdout.write(` ${chalk.green("done")} \u2713
`);
return;
}
async function getVersions(name, runtime = false) {
const repos = await configExists() ? await allReposAsync() : standardRepos();
const repo4 = repos.find((network) => network.binaries.find((bin) => bin.name === name));
if (!repo4) {
throw new Error(`Network not found for ${name}`);
}
const releases = await octokit.rest.repos.listReleases({
owner: repo4.ghAuthor,
repo: repo4.ghRepo
});
if (releases.status !== 200 || releases.data.length === 0) {
throw new Error(`No releases found for ${repo4.ghAuthor}.${repo4.ghRepo}, try again later.`);
}
const versions = releases.data.map((release) => {
let tag = release.tag_name;
if (release.tag_name.includes("v")) {
tag = tag.split("v")[1];
}
if (tag.includes("-rc")) {
tag = tag.split("-rc")[0];
}
return tag;
}).filter(
(version) => runtime && version.includes("runtime") || !runtime && !version.includes("runtime")
).map((version) => version.replace("runtime-", ""));
const set = new Set(versions);
return runtime ? [...set] : [...set].sort(
(a, b) => semver.valid(a) && semver.valid(b) ? semver.rcompare(a, b) : a
);
}
// src/internal/cmdFunctions/initialisation.ts
import fs3 from "fs/promises";
import { input, number, confirm as confirm2 } from "@inquirer/prompts";
async function createFolders() {
await fs3.mkdir("scripts").catch(() => "scripts folder already exists, skipping");
await fs3.mkdir("tests").catch(() => "tests folder already exists, skipping");
await fs3.mkdir("tmp").catch(() => "tmp folder already exists, skipping");
}
async function generateConfig(argv) {
let answers;
try {
await fs3.access("moonwall.config.json");
console.log("\u2139\uFE0F Config file already exists at this location. Quitting.");
return;
} catch (_) {
}
if (argv.acceptAllDefaults) {
answers = {
label: "moonwall_config",
timeout: 3e4,
environmentName: "default_env",
foundation: "dev",
testDir: "tests/default/"
};
} else {
while (true) {
answers = {
label: await input({
message: "Provide a label for the config file",
default: "moonwall_config"
}),
timeout: await number({
message: "Provide a global timeout value",
default: 3e4
}) ?? 3e4,
environmentName: await input({
message: "Provide a name for this environment",
default: "default_env"
}),
foundation: "dev",
testDir: await input({
message: "Provide the path for where tests for this environment are kept",
default: "tests/default/"
})
};
const proceed = await confirm2({
message: "Would you like to generate this config? (no to restart from beginning)"
});
if (proceed) {
break;
}
console.log("Restarting the configuration process...");
}
}
const config = createSampleConfig({
label: answers.label,
timeout: answers.timeout,
environmentName: answers.environmentName,
foundation: answers.foundation,
testDir: answers.testDir
});
const JSONBlob = JSON.stringify(config, null, 3);
await fs3.writeFile("moonwall.config.json", JSONBlob, "utf-8");
process.env.MOON_CONFIG_PATH = "./moonwall.config.json";
await createSampleTest(answers.testDir);
console.log("Test directory created at: ", answers.testDir);
console.log(
`You can now add tests to this directory and run them with 'pnpm moonwall test ${answers.environmentName}'`
);
console.log("Goodbye! \u{1F44B}");
}
function createConfig(options) {
return {
label: options.label,
defaultTestTimeout: options.timeout,
environments: [
{
name: options.environmentName,
testFileDir: [options.testDir],
foundation: {
type: options.foundation
}
}
]
};
}
function createSampleConfig(options) {
return {
$schema: "https://raw.githubusercontent.com/Moonsong-Labs/moonwall/main/packages/types/config_schema.json",
label: options.label,
defaultTestTimeout: options.timeout,
environments: [
{
name: options.environmentName,
testFileDir: [options.testDir],
multiThreads: false,
foundation: {
type: "dev",
launchSpec: [
{
name: "moonbeam",
useDocker: true,
newRpcBehaviour: true,
binPath: "moonbeamfoundation/moonbeam"
}
]
}
}
]
};
}
async function createSampleTest(directory) {
await fs3.mkdir(directory, { recursive: true });
await fs3.writeFile(`${directory}/sample.test.ts`, sampleTest, "utf-8");
}
var sampleTest = `import { describeSuite, expect } from "@moonwall/cli";
describeSuite({
id: "B01",
title: "Sample test suite for moonbeam network",
foundationMethods: "dev",
testCases: ({ context, it }) => {
const ALITH_ADDRESS = "0xf24FF3a9CF04c71Dbc94D0b566f7A27B94566cac"
it({
id: "T01",
title: "Test that API is connected correctly",
test: async () => {
const chainName = context.pjsApi.consts.system.version.specName.toString();
const specVersion = context.pjsApi.consts.system.version.specVersion.toNumber();
expect(chainName.length).toBeGreaterThan(0)
expect(chainName).toBe("moonbase")
expect(specVersion).toBeGreaterThan(0)
},
});
it({
id: "T02",
title: "Test that chain queries can be made",
test: async () => {
const balance = (await context.pjsApi.query.system.account(ALITH_ADDRESS)).data.free
expect(balance.toBigInt()).toBeGreaterThan(0n)
},
});
},
});
`;
// src/internal/cmdFunctions/tempLogs.ts
import path3 from "path";
import fs4 from "fs";
function clearNodeLogs(silent = true) {
const dirPath = path3.join(process.cwd(), "tmp", "node_logs");
if (!fs4.existsSync(dirPath)) {
fs4.mkdirSync(dirPath, { recursive: true });
}
const files = fs4.readdirSync(dirPath);
for (const file of files) {
!silent && console.log(`Deleting log: ${file}`);
if (file.endsWith(".log")) {
fs4.unlinkSync(path3.join(dirPath, file));
}
}
}
function reportLogLocation(silent = false) {
const dirPath = path3.join(process.cwd(), "tmp", "node_logs");
if (!fs4.existsSync(dirPath)) {
fs4.mkdirSync(dirPath, { recursive: true });
}
const result = fs4.readdirSync(dirPath);
let consoleMessage = "";
let filePath = "";
try {
filePath = process.env.MOON_ZOMBIE_DIR ? process.env.MOON_ZOMBIE_DIR : process.env.MOON_LOG_LOCATION ? process.env.MOON_LOG_LOCATION : path3.join(
dirPath,
result.find((file) => path3.extname(file) === ".log") || "no_logs_found"
);
consoleMessage = ` \u{1FAB5} Log location: ${filePath}`;
} catch (e) {
console.error(e);
}
!silent && console.log(consoleMessage);
return filePath.trim();
}
// src/internal/commandParsers.ts
import chalk2 from "chalk";
import path4 from "path";
import invariant from "tiny-invariant";
function parseZombieCmd(launchSpec) {
if (launchSpec) {
return { cmd: launchSpec.configPath };
}
throw new Error(
`No ZombieSpec found in config.
Are you sure your ${chalk2.bgWhiteBright.blackBright(
"moonwall.config.json"
)} file has the correct "configPath" in zombieSpec?`
);
}
function fetchDefaultArgs(binName, additionalRepos = []) {
let defaultArgs;
const repos = [...standardRepos(), ...additionalRepos];
for (const repo4 of repos) {
const foundBin = repo4.binaries.find((bin) => bin.name === binName);
if (foundBin) {
defaultArgs = foundBin.defaultArgs;
break;
}
}
if (!defaultArgs) {
defaultArgs = ["--dev"];
}
return defaultArgs;
}
var LaunchCommandParser = class _LaunchCommandParser {
args;
cmd;
launch;
launchSpec;
additionalRepos;
launchOverrides;
constructor(options) {
const { launchSpec, additionalRepos, launchOverrides } = options;
this.launchSpec = launchSpec;
this.additionalRepos = additionalRepos;
this.launchOverrides = launchOverrides;
this.launch = !launchSpec.running ? true : launchSpec.running;
this.cmd = launchSpec.binPath;
this.args = launchSpec.options ? [...launchSpec.options] : fetchDefaultArgs(path4.basename(launchSpec.binPath), additionalRepos);
}
overrideArg(newArg) {
const newArgKey = newArg.split("=")[0];
const existingIndex = this.args.findIndex((arg) => arg.startsWith(`${newArgKey}=`));
if (existingIndex !== -1) {
this.args[existingIndex] = newArg;
} else {
this.args.push(newArg);
}
}
withPorts() {
if (this.launchSpec.ports) {
const ports = this.launchSpec.ports;
if (ports.p2pPort) {
this.overrideArg(`--port=${ports.p2pPort}`);
}
if (ports.wsPort) {
this.overrideArg(`--ws-port=${ports.wsPort}`);
}
if (ports.rpcPort) {
this.overrideArg(`--rpc-port=${ports.rpcPort}`);
}
} else {
const freePort = getFreePort().toString();
process.env.MOONWALL_RPC_PORT = freePort;
if (this.launchSpec.newRpcBehaviour) {
this.overrideArg(`--rpc-port=${freePort}`);
} else {
this.overrideArg(`--ws-port=${freePort}`);
}
}
return this;
}
withDefaultForkConfig() {
const forkOptions = this.launchSpec.defaultForkConfig;
if (forkOptions) {
this.applyForkOptions(forkOptions);
}
return this;
}
withLaunchOverrides() {
if (this.launchOverrides?.forkConfig) {
this.applyForkOptions(this.launchOverrides.forkConfig);
}
return this;
}
print() {
console.log(chalk2.cyan(`Command to run is: ${chalk2.bold(this.cmd)}`));
console.log(chalk2.cyan(`Arguments are: ${chalk2.bold(this.args.join(" "))}`));
return this;
}
applyForkOptions(forkOptions) {
if (forkOptions.url) {
invariant(forkOptions.url.startsWith("http"), "Fork URL must start with http:// or https://");
this.overrideArg(`--fork-chain-from-rpc=${forkOptions.url}`);
}
if (forkOptions.blockHash) {
this.overrideArg(`--block=${forkOptions.blockHash}`);
}
if (forkOptions.stateOverridePath) {
this.overrideArg(`--fork-state-overrides=${forkOptions.stateOverridePath}`);
}
if (forkOptions.verbose) {
this.overrideArg("-llazy-loading=trace");
}
}
build() {
return {
cmd: this.cmd,
args: this.args,
launch: this.launch
};
}
static create(options) {
const parser = new _LaunchCommandParser(options);
const parsed = parser.withPorts().withDefaultForkConfig().withLaunchOverrides();
if (options.verbose) {
parsed.print();
}
return parsed.build();
}
};
function parseChopsticksRunCmd(launchSpecs) {
const launch = !launchSpecs[0].running ? true : launchSpecs[0].running;
if (launchSpecs.length === 1) {
const chopsticksCmd2 = "node";
const chopsticksArgs2 = [
"node_modules/@acala-network/chopsticks/chopsticks.cjs",
`--config=${launchSpecs[0].configPath}`,
`--addr=${launchSpecs[0].address ?? "127.0.0.1"}`
// use old behaviour by default
];
const mode = launchSpecs[0].buildBlockMode ? launchSpecs[0].buildBlockMode : "manual";
const num = mode === "batch" ? "Batch" : mode === "instant" ? "Instant" : "Manual";
chopsticksArgs2.push(`--build-block-mode=${num}`);
if (launchSpecs[0].wsPort) {
chopsticksArgs2.push(`--port=${launchSpecs[0].wsPort}`);
}
if (launchSpecs[0].wasmOverride) {
chopsticksArgs2.push(`--wasm-override=${launchSpecs[0].wasmOverride}`);
}
if (launchSpecs[0].allowUnresolvedImports) {
chopsticksArgs2.push("--allow-unresolved-imports");
}
return {
cmd: chopsticksCmd2,
args: chopsticksArgs2,
launch
};
}
const chopsticksCmd = "node";
const chopsticksArgs = ["node_modules/@acala-network/chopsticks/chopsticks.cjs", "xcm"];
for (const spec of launchSpecs) {
const type = spec.type ? spec.type : "parachain";
switch (type) {
case "parachain":
chopsticksArgs.push(`--parachain=${spec.configPath}`);
break;
case "relaychain":
chopsticksArgs.push(`--relaychain=${spec.configPath}`);
}
}
return {
cmd: chopsticksCmd,
args: chopsticksArgs,
launch
};
}
var getFreePort = () => {
const notionalPort = 1e4 + Number(process.env.VITEST_POOL_ID || 1) * 100;
return notionalPort;
};
// src/internal/deriveTestIds.ts
import chalk3 from "chalk";
import fs5 from "fs";
import { confirm as confirm3 } from "@inquirer/prompts";
import path5 from "path";
async function deriveTestIds(params) {
const usedPrefixes = /* @__PURE__ */ new Set();
const { rootDir, singlePrefix } = params;
try {
await fs5.promises.access(rootDir, fs5.constants.R_OK);
} catch (error) {
console.error(
`\u{1F534} Error accessing directory ${chalk3.bold(`/${rootDir}`)}, please sure this exists`
);
process.exitCode = 1;
return;
}
console.log(`\u{1F7E2} Processing ${rootDir} ...`);
const topLevelDirs = getTopLevelDirs(rootDir);
const foldersToRename = [];
if (singlePrefix) {
const prefix = generatePrefix(rootDir, usedPrefixes, params.prefixPhrase);
foldersToRename.push({ prefix, dir: "." });
} else {
for (const dir of topLevelDirs) {
const prefix = generatePrefix(dir, usedPrefixes, params.prefixPhrase);
foldersToRename.push({ prefix, dir });
}
}
const result = await confirm3({
message: `This will rename ${foldersToRename.length} suites IDs in ${rootDir}, continue?`
});
if (!result) {
console.log("\u{1F534} Aborted");
return;
}
for (const folder of foldersToRename) {
const { prefix, dir } = folder;
process.stdout.write(
`\u{1F7E2} Changing suite ${dir} to use prefix ${chalk3.bold(`(${prefix})`)} ....`
);
generateId(path5.join(rootDir, dir), rootDir, prefix);
process.stdout.write(" Done \u2705\n");
}
console.log(`\u{1F3C1} Finished renaming rootdir ${chalk3.bold(`/${rootDir}`)}`);
}
function getTopLevelDirs(rootDir) {
return fs5.readdirSync(rootDir).filter((dir) => fs5.statSync(path5.join(rootDir, dir)).isDirectory());
}
function generatePrefix(directory, usedPrefixes, rootPrefix) {
const sanitizedDir = directory.replace(/[-_ ]/g, "").toUpperCase();
let prefix = rootPrefix ?? sanitizedDir[0];
let additionalIndex = 1;
while (usedPrefixes.has(prefix) && additionalIndex < sanitizedDir.length) {
prefix += rootPrefix?.[additionalIndex] ?? sanitizedDir[additionalIndex];
additionalIndex++;
}
let numericSuffix = 0;
while (usedPrefixes.has(prefix)) {
if (numericSuffix < 10) {
numericSuffix++;
prefix = sanitizedDir[0] + numericSuffix.toString();
} else {
let lastChar = prefix.slice(-1).charCodeAt(0);
if (lastChar >= 90) {
lastChar = 65;
} else {
lastChar++;
}
prefix = sanitizedDir[0] + String.fromCharCode(lastChar);
}
}
usedPrefixes.add(prefix);
return prefix;
}
function generateId(directory, rootDir, prefix) {
const contents = fs5.readdirSync(directory);
contents.sort((a, b) => {
const aIsDir = fs5.statSync(path5.join(directory, a)).isDirectory();
const bIsDir = fs5.statSync(path5.join(directory, b)).isDirectory();
if (aIsDir && !bIsDir) return -1;
if (!aIsDir && bIsDir) return 1;
return customFileSort(a, b);
});
let fileCount = 1;
let subDirCount = 1;
for (const item of contents) {
const fullPath = path5.join(directory, item);
if (fs5.statSync(fullPath).isDirectory()) {
const subDirPrefix = `0${subDirCount}`.slice(-2);
generateId(fullPath, rootDir, prefix + subDirPrefix);
subDirCount++;
} else {
const fileContent = fs5.readFileSync(fullPath, "utf-8");
if (fileContent.includes("describeSuite")) {
const newId = prefix + `0${fileCount}`.slice(-2);
const updatedContent = fileContent.replace(
/(describeSuite\s*?\(\s*?\{\s*?id\s*?:\s*?['"])[^'"]+(['"])/,
`$1${newId}$2`
);
fs5.writeFileSync(fullPath, updatedContent);
}
fileCount++;
}
}
}
function hasSpecialCharacters(filename) {
return /[ \t!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]+/.test(filename);
}
function customFileSort(a, b) {
const aHasSpecialChars = hasSpecialCharacters(a);
const bHasSpecialChars = hasSpecialCharacters(b);
if (aHasSpecialChars && !bHasSpecialChars) return -1;
if (!aHasSpecialChars && bHasSpecialChars) return 1;
return a.localeCompare(b, void 0, { sensitivity: "accent" });
}
// src/internal/fileCheckers.ts
import fs6 from "fs";
import { execSync as execSync2 } from "child_process";
import chalk4 from "chalk";
import os from "os";
import path6 from "path";
import { select as select2 } from "@inquirer/prompts";
async function checkExists(path10) {
const binPath = path10.split(" ")[0];
const fsResult = fs6.existsSync(binPath);
if (!fsResult) {
throw new Error(
`No binary file found at location: ${binPath}
Are you sure your ${chalk4.bgWhiteBright.blackBright(
"moonwall.config.json"
)} file has the correct "binPath" in launchSpec?`
);
}
const binArch = await getBinaryArchitecture(binPath);
const currentArch = os.arch();
if (binArch !== currentArch && binArch !== "unknown") {
throw new Error(
`The binary architecture ${chalk4.bgWhiteBright.blackBright(
binArch
)} does not match this system's architecture ${chalk4.bgWhiteBright.blackBright(
currentArch
)}
Download or compile a new binary executable for ${chalk4.bgWhiteBright.blackBright(
currentArch
)} `
);
}
return true;
}
async function downloadBinsIfMissing(binPath) {
const binName = path6.basename(binPath);
const binDir = path6.dirname(binPath);
const binPathExists = fs6.existsSync(binPath);
if (!binPathExists && process.arch === "x64") {
const download = await select2({
message: `The binary ${chalk4.bgBlack.greenBright(
binName
)} is missing from ${chalk4.bgBlack.greenBright(
path6.join(process.cwd(), binDir)
)}.
Would you like to download it now?`,
default: 0,
choices: [
{ name: `Yes, download ${binName}`, value: true },
{ name: "No, quit program", value: false }
]
});
if (!download) {
process.exit(0);
} else {
execSync2(`mkdir -p ${binDir}`);
execSync2(`pnpm moonwall download ${binName} latest ${binDir}`, {
stdio: "inherit"
});
}
} else if (!binPathExists) {
console.log(
`The binary: ${chalk4.bgBlack.greenBright(
binName
)} is missing from: ${chalk4.bgBlack.greenBright(path6.join(process.cwd(), binDir))}`
);
console.log(
`Given you are running ${chalk4.bgBlack.yellowBright(
process.arch
)} architecture, you will need to build it manually from source \u{1F6E0}\uFE0F`
);
throw new Error("Executable binary not available");
}
}
function checkListeningPorts(processId) {
try {
const stdOut = execSync2(`lsof -p ${processId} | grep LISTEN`, {
encoding: "utf-8"
});
const binName = stdOut.split("\n")[0].split(" ")[0];
const ports = stdOut.split("\n").filter(Boolean).map((line) => {
const port = line.split(":")[1];
return port.split(" ")[0];
});
const filtered = new Set(ports);
return { binName, processId, ports: [...filtered].sort() };
} catch (e) {
const binName = execSync2(`ps -p ${processId} -o comm=`).toString().trim();
console.log(
`Process ${processId} is running which for binary ${binName}, however it is unresponsive.`
);
console.log(
"Running Moonwall with this in the background may cause unexpected behaviour. Please manually kill the process and try running Moonwall again."
);
console.log(`N.B. You can kill it with: sudo kill -9 ${processId}`);
throw new Error(e);
}
}
function checkAlreadyRunning(binaryName) {
try {
console.log(`Checking if ${chalk4.bgWhiteBright.blackBright(binaryName)} is already running...`);
const stdout = execSync2(`pgrep ${[binaryName.slice(0, 14)]}`, {
encoding: "utf8",
timeout: 2e3
});
const pIdStrings = stdout.split("\n").filter(Boolean);
return pIdStrings.map((pId) => Number.parseInt(pId, 10));
} catch (error) {
if (error.status === 1) {
return [];
}
throw error;
}
}
async function promptAlreadyRunning(pids) {
const alreadyRunning = await select2({
message: `The following processes are already running:
${pids.map((pid) => {
const { binName, ports } = checkListeningPorts(pid);
return `${binName} - pid: ${pid}, listenPorts: [${ports.join(", ")}]`;
}).join("\n")}`,
default: 1,
choices: [
{ name: "\u{1FA93} Kill processes and continue", value: "kill" },
{ name: "\u27A1\uFE0F Continue (and let processes live)", value: "continue" },
{ name: "\u{1F6D1} Abort (and let processes live)", value: "abort" }
]
});
switch (alreadyRunning) {
case "kill":
for (const pid of pids) {
execSync2(`kill ${pid}`);
}
break;
case "continue":
break;
case "abort":
throw new Error("Abort Signal Picked");
}
}
function checkAccess(path10) {
const binPath = path10.split(" ")[0];
try {
fs6.accessSync(binPath, fs6.constants.X_OK);
} catch (err) {
console.error(`The file ${binPath} is not executable`);
throw new Error(`The file at ${binPath} , lacks execute permissions.`);
}
}
async function getBinaryArchitecture(filePath) {
return new Promise((resolve, reject) => {
const architectureMap = {
0: "unknown",
3: "x86",
62: "x64",
183: "arm64"
};
fs6.open(filePath, "r", (err, fd) => {
if (err) {
reject(err);
return;
}
const buffer = Buffer.alloc(20);
fs6.read(fd, buffer, 0, 20, 0, (err2, bytesRead, buffer2) => {
if (err2) {
reject(err2);
return;
}
const e_machine = buffer2.readUInt16LE(18);
const architecture = architectureMap[e_machine] || "unknown";
resolve(architecture);
});
});
});
}
// src/internal/foundations/chopsticksHelpers.ts
import "@moonbeam-network/api-augment";
import chalk6 from "chalk";
import { setTimeout as setTimeout2 } from "timers/promises";
// src/lib/globalContext.ts
import "@moonbeam-network/api-augment";
import zombie from "@zombienet/orchestrator";
import { createLogger as createLogger4 } from "@moonwall/util";
import fs9 from "fs";
import net2 from "net";
import readline from "readline";
import { setTimeout as timer3 } from "timers/promises";
import path8 from "path";
// src/internal/foundations/zombieHelpers.ts
import chalk5 from "chalk";
import fs7 from "fs";
import invariant2 from "tiny-invariant";
import { setTimeout as timer } from "timers/promises";
import net from "net";
async function checkZombieBins(config) {
const relayBinPath = config.relaychain.default_command;
if (!relayBinPath) {
throw new Error("No relayBinPath '[relaychain.default_command]' specified in zombie config");
}
await checkExists(relayBinPath);
checkAccess(relayBinPath);
if (config.parachains) {
const promises = config.parachains.map((para) => {
if (para.collator) {
if (!para.collator.command) {
throw new Error(
"No command found for collator, please check your zombienet config file for collator command"
);
}
checkExists(para.collator.command);
checkAccess(para.collator.command);
}
if (para.collators) {
for (const coll of para.collators) {
if (!coll.command) {
throw new Error(
"No command found for collators, please check your zombienet config file for collators command"
);
}
checkExists(coll.command);
checkAccess(coll.command);
}
}
});
await Promise.all(promises);
}
}
function getZombieConfig(path10) {
const fsResult = fs7.existsSync(path10);
if (!fsResult) {
throw new Error(
`No ZombieConfig file found at location: ${path10}
Are you sure your ${chalk5.bgWhiteBright.blackBright(
"moonwall.config.json"
)} file has the correct "configPath" in zombieSpec?`
);
}
const buffer = fs7.readFileSync(path10, "utf-8");
return JSON.parse(buffer);
}
async function sendIpcMessage(message) {
return new Promise(async (resolve, reject) => {
let response;
const ipcPath = process.env.MOON_IPC_SOCKET;
invariant2(ipcPath, "No IPC path found. This is a bug, please report it.");
const client = net.createConnection({ path: ipcPath }, () => {
console.log("\u{1F4E8} Successfully connected to IPC server");
});
client.on("error", (err) => {
console.error("\u{1F4E8} IPC client connection error:", err);
});
client.on("data", async (data) => {
response = JSON.parse(data.toString());
if (response.status === "success") {
client.end();
for (let i = 0; ; i++) {
if (client.closed) {
break;
}
if (i > 100) {
reject(new Error("Closing IPC connection failed"));
}
await timer(200);
}
resolve(response);
}
if (response.status === "failure") {
reject(new Error(JSON.stringify(response)));
}
});
for (let i = 0; ; i++) {
if (!client.connecting) {
break;
}
if (i > 100) {
reject(new Error(`Connection to ${ipcPath} failed`));
}
await timer(200);
}
await new Promise((resolve2) => {
client.write(JSON.stringify(message), () => resolve2("Sent!"));
});
});
}
// src/internal/localNode.ts
import { exec, spawn, spawnSync } from "child_process";
import fs8 from "fs";
import path7 from "path";
import WebSocket from "ws";
import { createLogger as createLogger2 } from "@moonwall/util";
import { setTimeout as timer2 } from "timers/promises";
import util from "util";
import Docker from "dockerode";
import invariant3 from "tiny-invariant";
var execAsync2 = util.promisify(exec);
var logger2 = createLogger2({ name: "localNode" });
var debug2 = logger2.debug.bind(logger2);
async function launchDockerContainer(imageName, args, name, dockerConfig) {
const docker = new Docker();
const port = args.find((a) => a.includes("port"))?.split("=")[1];
debug2(`\x1B[36mStarting Docker container ${imageName} on port ${port}...\x1B[0m`);
const dirPath = path7.join(process.cwd(), "tmp", "node_logs");
const logLocation = path7.join(dirPath, `${name}_docker_${Date.now()}.log`);
const fsStream = fs8.createWriteStream(logLocation);
process.env.MOON_LOG_LOCATION = logLocation;
const portBindings = dockerConfig?.exposePorts?.reduce(
(acc, { hostPort, internalPort }) => {
acc[`${internalPort}/tcp`] = [{ HostPort: hostPort.toString() }];
return acc;
},
{}
);
const rpcPort = args.find((a) => a.includes("rpc-port"))?.split("=")[1];
invariant3(rpcPort, "RPC port not found, this is a bug");
const containerOptions = {
Image: imageName,
platform: "linux/amd64",
Cmd: args,
name: dockerConfig?.containerName || `moonwall_${name}_${Date.now()}`,
ExposedPorts: {
...Object.fromEntries(
dockerConfig?.exposePorts?.map(({ internalPort }) => [`${internalPort}/tcp`, {}]) || []
),
[`${rpcPort}/tcp`]: {}
},
HostConfig: {
PortBindings: {
...portBindings,
[`${rpcPort}/tcp`]: [{ HostPort: rpcPort }]
}
},
Env: dockerConfig?.runArgs?.filter((arg) => arg.startsWith("env:")).map((arg) => arg.slice(4))
};
try {
await pullImage(imageName, docker);
const container = await docker.createContainer(containerOptions);
await container.start();
const containerInfo = await container.inspect();
if (!containerInfo.State.Running) {
const errorMessage = `Container failed to start: ${containerInfo.State.Error}`;
console.error(errorMessage);
fs8.appendFileSync(logLocation, `${errorMessage}
`);
throw new Error(errorMessage);
}
for (let i = 0; i < 300; i++) {
if (await checkWebSocketJSONRPC(Number.parseInt(rpcPort))) {
break;
}
await timer2(100);
}
return { runningNode: container, fsStream };
} catch (error) {
if (error instanceof Error) {
console.error(`Docker container launch failed: ${error.message}`);
fs8.appendFileSync(logLocation, `Docker launch error: ${error.message}
`);
}
throw error;
}
}
async function launchNode(options) {
const { command: cmd, args, name, launchSpec: config } = options;
if (config?.useDocker) {
return launchDockerContainer(cmd, args, name, config.dockerConfig);
}
if (cmd.includes("moonbeam")) {
await checkExists(cmd);
checkAccess(cmd);
}
const port = args.find((a) => a.includes("port"))?.split("=")[1];
debug2(`\x1B[36mStarting ${name} node on port ${port}...\x1B[0m`);
const dirPath = path7.join(process.cwd(), "tmp", "node_logs");
const runningNode = spawn(cmd, args);
const logLocation = path7.join(
dirPath,
`${path7.basename(cmd)}_node_${args.find((a) => a.includes("port"))?.split("=")[1]}_${runningNode.pid}.log`
).replaceAll("node_node_undefined", "chopsticks");
process.env.MOON_LOG_LOCATION = logLocation;
const fsStream = fs8.createWriteStream(logLocation);
runningNode.on("error", (err) => {
if (err.errno === "ENOENT") {
console.error(`\x1B[31mMissing Local binary at(${cmd}).
Please compile the project\x1B[0m`);
}
throw new Error(err.message);
});
const logHandler = (chunk) => {
if (fsStream.writable) {
fsStream.write(chunk, (err) => {
if (err) console.error(err);
else fsStream.emit("drain");
});
}
};
runningNode.stderr?.on("data", logHandler);
runningNode.stdout?.on("data", logHandler);
runningNode.once("exit", (code, signal) => {
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
let message;
const moonwallNode = runningNode;
if (moonwallNode.isMoonwallTerminating) {
message = `${timestamp} [moonwall] process killed. reason: ${moonwallNode.moonwallTerminationReason || "unknown"}`;
} else if (code !== null) {
message = `${timestamp} [moonwall] process exited with status code ${code}`;
} else if (signal !== null) {
message = `${timestamp} [moonwall] process terminated by signal ${signal}`;
} else {
message = `${timestamp} [moonwall] process terminated unexpectedly`;
}
if (fsStream.writable) {
fsStream.write(`${message}
`, (err) => {
if (err) console.error(`Failed to write exit message to log: ${err}`);
fsStream.end();
});
} else {
try {
fs8.appendFileSync(logLocation, `${message}
`);
} catch (err) {
console.error(`Failed to append exit message to log file: ${err}`);
}
fsStream.end();
}
runningNode.stderr?.removeListener("data", logHandler);
runningNode.stdout?.removeListener("data", logHandler);
});
if (!runningNode.pid) {
const errorMessage = "Failed to start child process";
console.error(errorMessage);
fs8.appendFileSync(logLocation, `${errorMessage}
`);
throw new Error(errorMessage);
}
if (runningNode.exitCode !== null) {
const errorMessage = `Child process exited immediately with code ${runningNode.exitCode}`;
console.error(errorMessage);
fs8.appendFileSync(logLocation, `${errorMessage}
`);
throw new Error(errorMessage);
}
const isRunning = await isPidRunning(runningNode.pid);
if (!isRunning) {
const errorMessage = `Process with PID ${runningNode.pid} is not running`;
spawnSync(cmd, args, { stdio: "inherit" });
throw new Error(errorMessage);
}
probe: for (let i = 0; ; i++) {
try {
const ports = await findPortsByPid(runningNode.pid);
if (ports) {
for (const port2 of ports) {
try {
await checkWebSocketJSONRPC(port2);
break probe;
} catch {
}
}
}
} catch {
if (i === 300) {
throw new Error("Could not find ports for node after 30 seconds");
}
await timer2(100);
continue;
}
await timer2(100);
}
return { runningNode, fsStream };
}
function i