UNPKG

@moonwall/cli

Version:

Testing framework for the Moon family of projects

1,524 lines (1,498 loc) 101 kB
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