@moonsong-labs/moonwall-cli
Version:
Testing framework for the Moon family of projects
1,461 lines (1,434 loc) • 50.9 kB
JavaScript
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var __publicField = (obj, key, value) => {
__defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
return value;
};
// src/cmds/main.ts
var main_exports = {};
__export(main_exports, {
main: () => main
});
module.exports = __toCommonJS(main_exports);
var import_chalk6 = __toESM(require("chalk"), 1);
var import_inquirer3 = __toESM(require("inquirer"), 1);
var import_inquirer_press_to_continue3 = __toESM(require("inquirer-press-to-continue"), 1);
// src/lib/configReader.ts
var import_api_augment = require("@moonbeam-network/api-augment");
var import_promises = __toESM(require("fs/promises"), 1);
var import_path = __toESM(require("path"), 1);
async function importJsonConfig() {
const filePath = import_path.default.join(process.cwd(), "moonwall.config.json");
try {
const file = await import_promises.default.readFile(filePath, "utf8");
const json = JSON.parse(file);
return json;
} catch (e) {
console.error(e);
throw new Error(`Error import config at ${filePath}`);
}
}
// src/cmds/generateConfig.ts
var import_inquirer = __toESM(require("inquirer"), 1);
var import_inquirer_press_to_continue = __toESM(require("inquirer-press-to-continue"), 1);
var import_promises2 = __toESM(require("fs/promises"), 1);
import_inquirer.default.registerPrompt("press-to-continue", import_inquirer_press_to_continue.default);
async function generateConfig() {
while (true) {
if (await import_promises2.default.access("moonwall.config.json").catch(() => true)) {
const answers = await import_inquirer.default.prompt(generateQuestions);
const proceed = await import_inquirer.default.prompt(
questions.find(({ name }) => name === "Confirm")
);
if (proceed.Confirm === false) {
continue;
}
const JSONBlob = JSON.stringify(
createConfig({
label: answers.Label,
timeout: answers.Timeout,
environmentName: answers.EnvironmentName,
foundation: answers.EnvironmentFoundation,
testDir: answers.EnvironmentTestDir
}),
null,
3
);
await import_promises2.default.writeFile(
"moonwall.config.json",
JSONBlob,
"utf-8"
);
break;
} else {
console.log("\u2139\uFE0F Config file already exists at this location. Quitting.");
return;
}
}
console.log(`Goodbye! \u{1F44B}`);
}
var generateQuestions = [
{
name: "Label",
type: "input",
message: "Provide a label for the config file",
default: "moonwall_config"
},
{
name: "Timeout",
type: "number",
message: "Provide a global timeout value",
default: 3e4,
validate: (input) => {
const pass = /^\d+$/.test(input);
if (pass) {
return true;
}
return "Please enter a valid number \u274C";
}
},
{
name: "EnvironmentName",
type: "input",
message: "Provide a name for this environment",
default: "default_env"
},
{
name: "EnvironmentTestDir",
type: "input",
message: "Provide the path for where tests for this environment are kept",
default: "tests/"
},
{
name: "EnvironmentFoundation",
type: "list",
message: "What type of network foundation is this?",
choices: ["dev", "chopsticks", "read_only", "fork", "zombie"],
default: "tests/"
}
];
var questions = [
{
name: "Confirm",
type: "confirm",
message: "Would you like to generate this config? (no to restart from beginning)"
},
{
name: "Success",
type: "press-to-continue",
anyKey: true,
pressToContinueMessage: "\u{1F4C4} moonwall.config.ts has been generated. Press any key to exit \u2705\n"
},
{
name: "Failure",
type: "press-to-continue",
anyKey: true,
pressToContinueMessage: "Config has not been generated due to errors, Press any key to exit \u274C\n"
}
];
function createConfig(options) {
return {
$schema: "https://raw.githubusercontent.com/Moonsong-Labs/moonwall/main/packages/cli/config_schema.json",
label: options.label,
defaultTestTimeout: options.timeout,
environments: [
{
name: options.environmentName,
testFileDir: [options.testDir],
foundation: {
type: options.foundation
},
connections: [
{
name: "SAMPLE",
type: "ethers",
endpoints: ["wss://moonriver.api.onfinality.io/public-ws"]
}
]
}
]
};
}
// src/cmds/main.ts
var import_colors = __toESM(require("colors"), 1);
var import_clear2 = __toESM(require("clear"), 1);
// src/cmds/runNetwork.ts
var import_inquirer_press_to_continue2 = __toESM(require("inquirer-press-to-continue"), 1);
var import_inquirer2 = __toESM(require("inquirer"), 1);
// src/lib/globalContext.ts
var import_api_augment2 = require("@moonbeam-network/api-augment");
// src/internal/providers.ts
var import_moonbeam_types_bundle = require("moonbeam-types-bundle");
var import_api = require("@polkadot/api");
var import_web3 = require("web3");
var import_web3_providers_ws = require("web3-providers-ws");
var import_ethers = require("ethers");
var import_debug = __toESM(require("debug"), 1);
var import_chalk = __toESM(require("chalk"), 1);
var import_moonwall_util = require("@moonsong-labs/moonwall-util");
var debug = (0, import_debug.default)("global:providers");
function prepareProviders(providerConfigs) {
return providerConfigs.map(({ name, endpoints, type }) => {
const url = endpoints.includes("ENV_VAR") ? process.env.WSS_URL : endpoints[0];
switch (type) {
case "polkadotJs":
debug(`\u{1F7E2} PolkadotJs provider ${name} details prepared`);
return {
name,
type,
connect: async () => {
const api = await import_api.ApiPromise.create({
provider: new import_api.WsProvider(url),
initWasm: false,
noInitWarn: true
});
await api.isReady;
return api;
},
ws: () => new import_api.WsProvider(url)
};
case "moon":
debug(`\u{1F7E2} Moonbeam provider ${name} details prepared`);
return {
name,
type,
connect: async () => {
const moonApi = await import_api.ApiPromise.create({
provider: new import_api.WsProvider(url),
rpc: import_moonbeam_types_bundle.rpcDefinitions,
typesBundle: import_moonbeam_types_bundle.types,
noInitWarn: true
});
await moonApi.isReady;
return moonApi;
},
ws: () => new import_api.WsProvider(url)
};
case "web3":
debug(`\u{1F7E2} Web3 provider ${name} details prepared`);
return {
name,
type,
connect: () => {
const provider = new import_web3_providers_ws.WebSocketProvider(
url,
{},
{ delay: 50, autoReconnect: false, maxAttempts: 10 }
);
provider.on("error", () => {
throw new Error(
`Cannot connect to Web3 provider ${import_chalk.default.bgWhiteBright.blackBright(url)}`
);
});
return new import_web3.Web3(provider);
}
};
case "ethers":
debug(`\u{1F7E2} Ethers provider ${name} details prepared`);
return {
name,
type,
connect: () => {
const provider = new import_ethers.ethers.WebSocketProvider(url);
return new import_ethers.Wallet(import_moonwall_util.ALITH_PRIVATE_KEY, provider);
}
};
default:
return {
name,
type,
connect: () => console.log(`\u{1F6A7} provider ${name} not yet implemented`)
};
}
});
}
async function populateProviderInterface(name, type, connect) {
switch (type) {
case "polkadotJs":
const pjsApi = await connect();
return {
name,
api: pjsApi,
type,
greet: () => {
debug(
`\u{1F44B} Provider ${name} is connected to chain ${pjsApi.consts.system.version.specName.toString()} RT${pjsApi.consts.system.version.specVersion.toNumber()}`
);
return {
rtVersion: pjsApi.consts.system.version.specVersion.toNumber(),
rtName: pjsApi.consts.system.version.specName.toString()
};
},
disconnect: async () => pjsApi.disconnect()
};
case "moon":
const mbApi = await connect();
return {
name,
api: mbApi,
type,
greet: () => {
debug(
`\u{1F44B} Provider ${name} is connected to chain ${mbApi.consts.system.version.specName.toString()} RT${mbApi.consts.system.version.specVersion.toNumber()}`
);
return {
rtVersion: mbApi.consts.system.version.specVersion.toNumber(),
rtName: mbApi.consts.system.version.specName.toString()
};
},
disconnect: async () => mbApi.disconnect()
};
case "ethers":
const ethApi = await connect();
return {
name,
api: ethApi,
type,
greet: async () => debug(
`\u{1F44B} Provider ${name} is connected to chain ` + (await ethApi.provider.getNetwork()).chainId
),
disconnect: async () => {
ethApi.provider.destroy();
}
};
case "web3":
const web3Api = await connect();
return {
name,
api: web3Api,
type,
greet: async () => console.log(
`\u{1F44B} Provider ${name} is connected to chain ` + await web3Api.eth.getChainId()
),
disconnect: async () => {
web3Api.currentProvider.disconnect();
}
};
default:
throw new Error("UNKNOWN TYPE");
}
}
// src/internal/localNode.ts
var import_child_process = require("child_process");
var import_chalk2 = __toESM(require("chalk"), 1);
var import_debug2 = __toESM(require("debug"), 1);
var import_fs = require("fs");
var debugNode = (0, import_debug2.default)("global:node");
async function launchNode(cmd, args, name) {
if (cmd.includes("moonbeam") && !(0, import_fs.existsSync)(cmd)) {
throw new Error(
`No binary file found at location: ${cmd}
Are you sure your ${import_chalk2.default.bgWhiteBright.blackBright(
"moonwall.config.json"
)} file has the correct "binPath" in launchSpec?`
);
}
let runningNode;
const onProcessExit = () => {
runningNode && runningNode.kill();
};
const onProcessInterrupt = () => {
runningNode && runningNode.kill();
};
process.once("exit", onProcessExit);
process.once("SIGINT", onProcessInterrupt);
runningNode = (0, import_child_process.spawn)(cmd, args);
runningNode.once("exit", () => {
process.removeListener("exit", onProcessExit);
process.removeListener("SIGINT", onProcessInterrupt);
debugNode(`Exiting dev node: ${name}`);
});
runningNode.on("error", (err) => {
if (err.errno == "ENOENT") {
console.error(
`\x1B[31mMissing Moonbeam binary at(${cmd}).
Please compile the Moonbeam project\x1B[0m`
);
} else {
console.error(err);
}
process.exit(1);
});
const binaryLogs = [];
await new Promise((resolve, reject) => {
const timer = setTimeout(() => {
console.error(import_chalk2.default.redBright("Failed to start Moonbeam Test Node."));
console.error(`Command: ${cmd} ${args.join(" ")}`);
console.error(`Logs:`);
console.error(binaryLogs.map((chunk) => chunk.toString()).join("\n"));
reject("Failed to launch node");
}, 1e4);
const onData = async (chunk) => {
debugNode(chunk.toString());
binaryLogs.push(chunk);
if (chunk.toString().match(/Development Service Ready/) || chunk.toString().match(/ RPC listening on port/)) {
clearTimeout(timer);
runningNode.stderr.off("data", onData);
runningNode.stdout.off("data", onData);
resolve();
}
};
runningNode.stderr.on("data", onData);
runningNode.stdout.on("data", onData);
});
return runningNode;
}
// src/internal/foundations.ts
function parseRunCmd(launchSpec) {
const launch = !!!launchSpec.running ? true : launchSpec.running;
const cmd = launchSpec.binPath;
let args = launchSpec.options ? [...launchSpec.options] : [
"--no-hardware-benchmarks",
"--no-telemetry",
"--reserved-only",
"--rpc-cors=all",
"--no-grandpa",
"--sealing=manual",
"--force-authoring",
"--no-prometheus",
"--alice",
"--chain=moonbase-dev",
"--in-peers=0",
"--out-peers=0",
"--tmp"
];
`ws://127.0.0.1:${1e4 + Number(process.env.VITEST_POOL_ID) * 100}`;
if (launchSpec.ports) {
const ports = launchSpec.ports;
if (ports.p2pPort) {
args.push(`--port=${ports.p2pPort}`);
}
if (ports.wsPort) {
args.push(`--ws-port=${ports.wsPort}`);
}
if (ports.rpcPort) {
args.push(`--rpc-port=${ports.rpcPort}`);
}
} else {
args.push(
`--port=${1e4 + Number(process.env.VITEST_POOL_ID || 1) * 100 + 2}`
);
args.push(
`--ws-port=${1e4 + Number(process.env.VITEST_POOL_ID || 1) * 100}`
);
args.push(
`--rpc-port=${1e4 + (Number(process.env.VITEST_POOL_ID || 1) * 100 + 1)}`
);
}
return { cmd, args, launch };
}
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.js",
"dev",
`--config=${launchSpecs[0].configPath}`
];
const mode = launchSpecs[0].buildBlockMode ? launchSpecs[0].buildBlockMode : "manual";
const num = mode == "batch" ? 0 : mode == "instant" ? 1 : 2;
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}`);
}
return {
cmd: chopsticksCmd2,
args: chopsticksArgs2,
launch
};
}
const chopsticksCmd = "node";
const chopsticksArgs = [
"node_modules/@acala-network/chopsticks/chopsticks.js",
"xcm"
];
launchSpecs.forEach((spec) => {
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
// rtUpgradePath: launchSpecs[0].rtUpgradePath
// ? launchSpecs[0].rtUpgradePath
// : "",
};
}
// src/lib/globalContext.ts
var import_debug3 = __toESM(require("debug"), 1);
var debugSetup = (0, import_debug3.default)("global:context");
var _MoonwallContext = class {
environment;
providers;
nodes;
foundation;
_finalizedHead;
rtUpgradePath;
constructor(config) {
this.environment;
this.providers = [];
this.nodes = [];
const env = config.environments.find(({ name }) => name == process.env.MOON_TEST_ENV);
const blob = {
name: env.name,
context: {},
providers: [],
nodes: [],
foundationType: env.foundation.type
};
switch (env.foundation.type) {
case "read_only":
if (!env.connections) {
throw new Error(
`${env.name} env config is missing connections specification, required by foundation READ_ONLY`
);
} else {
blob.providers = prepareProviders(env.connections);
}
debugSetup(`\u{1F7E2} Foundation "${env.foundation.type}" parsed for environment: ${env.name}`);
break;
case "chopsticks":
blob.nodes.push(parseChopsticksRunCmd(env.foundation.launchSpec));
blob.providers.push(...prepareProviders(env.connections));
this.rtUpgradePath = env.foundation.rtUpgradePath;
debugSetup(`\u{1F7E2} Foundation "${env.foundation.type}" parsed for environment: ${env.name}`);
break;
case "dev":
const { cmd, args, launch } = parseRunCmd(env.foundation.launchSpec[0]);
blob.nodes.push({
name: env.foundation.launchSpec[0].name,
cmd,
args,
launch
});
blob.providers = env.connections ? prepareProviders(env.connections) : prepareProviders([
{
name: "w3",
type: "web3",
endpoints: [
`ws://127.0.0.1:${1e4 + Number(process.env.VITEST_POOL_ID || 1) * 100}`
]
},
{
name: "eth",
type: "ethers",
endpoints: [
`ws://127.0.0.1:${1e4 + Number(process.env.VITEST_POOL_ID || 1) * 100}`
]
},
{
name: "mb",
type: "moon",
endpoints: [
`ws://127.0.0.1:${1e4 + Number(process.env.VITEST_POOL_ID || 1) * 100}`
]
}
]);
debugSetup(`\u{1F7E2} Foundation "${env.foundation.type}" parsed for environment: ${env.name}`);
break;
default:
debugSetup(`\u{1F6A7} Foundation "${env.foundation.type}" unsupported, skipping`);
return;
}
this.environment = blob;
}
get genesis() {
if (this._finalizedHead) {
return this._finalizedHead;
} else {
return "";
}
}
set genesis(hash) {
if (hash.length !== 66) {
throw new Error("Cannot set genesis to invalid hash");
}
this._finalizedHead = hash;
}
async startNetwork() {
const activeNodes = this.nodes.filter((node) => !node.killed);
if (activeNodes.length > 0) {
console.log("Nodes already started! Skipping command");
return _MoonwallContext.getContext();
}
const nodes = _MoonwallContext.getContext().environment.nodes;
const promises = nodes.map(async ({ cmd, args, name, launch }) => {
return launch && this.nodes.push(await launchNode(cmd, args, name));
});
await Promise.all(promises);
}
async stopNetwork() {
if (this.nodes.length === 0) {
console.log("Nodes already stopped! Skipping command");
return _MoonwallContext.getContext();
}
this.nodes.forEach((node) => node.kill());
await this.wipeNodes();
}
async connectEnvironment(environmentName) {
if (this.providers.length > 0) {
console.log("Providers already connected! Skipping command");
return _MoonwallContext.getContext();
}
const globalConfig = await importJsonConfig();
const promises = this.environment.providers.map(
async ({ name, type, connect }) => new Promise(async (resolve) => {
this.providers.push(await populateProviderInterface(name, type, connect));
resolve("");
})
);
await Promise.all(promises);
this.foundation = globalConfig.environments.find(
({ name }) => name == environmentName
).foundation.type;
if (this.foundation == "dev") {
this.genesis = (await this.providers.find(({ type }) => type == "polkadotJs" || type == "moon").api.rpc.chain.getBlockHash(0)).toString();
}
if (this.foundation == "chopsticks") {
this.genesis = (await this.providers.find(({ type }) => type == "polkadotJs" || type == "moon").api.rpc.chain.getFinalizedHead()).toString();
}
}
async wipeNodes() {
this.nodes = [];
}
async disconnect(providerName) {
if (providerName) {
this.providers.find(({ name }) => name === providerName).disconnect();
this.providers.filter(({ name }) => name !== providerName);
} else {
await Promise.all(this.providers.map((prov) => prov.disconnect()));
this.providers = [];
}
}
static printStats() {
if (_MoonwallContext) {
console.dir(_MoonwallContext.getContext(), { depth: 1 });
} else {
console.log("Global context not created!");
}
}
static getContext(config, force = false) {
if (!_MoonwallContext.instance || force) {
if (global.moonInstance && !force) {
_MoonwallContext.instance = global.moonInstance;
return _MoonwallContext.instance;
}
if (!config) {
console.error("\u274C Config must be provided on Global Context instantiation");
return void 0;
}
_MoonwallContext.instance = new _MoonwallContext(config);
debugSetup(`\u{1F7E2} Moonwall context "${config.label}" created`);
}
return _MoonwallContext.instance;
}
static async destroy() {
const ctx = _MoonwallContext.getContext();
try {
ctx.disconnect();
} catch {
console.log("\u{1F6D1} All connections disconnected");
}
const promises = ctx.nodes.map((process2) => {
return new Promise((resolve) => {
process2.kill();
if (process2.killed) {
resolve(`process ${process2.pid} killed`);
}
});
});
await Promise.all(promises);
}
};
var MoonwallContext = _MoonwallContext;
__publicField(MoonwallContext, "instance");
var contextCreator = async (config, env) => {
const ctx = MoonwallContext.getContext(config);
await runNetworkOnly(config);
await ctx.connectEnvironment(env);
return ctx;
};
var runNetworkOnly = async (config) => {
const ctx = MoonwallContext.getContext(config);
await ctx.startNetwork();
};
// src/cmds/runNetwork.ts
var import_clear = __toESM(require("clear"), 1);
var import_chalk4 = __toESM(require("chalk"), 1);
// src/cmds/runTests.ts
var import_node = require("vitest/node");
var import_path2 = __toESM(require("path"), 1);
var import_chalk3 = __toESM(require("chalk"), 1);
async function testCmd(envName, additionalArgs) {
const globalConfig = await importJsonConfig();
const env = globalConfig.environments.find(({ name }) => name === envName);
if (!!!env) {
const envList = globalConfig.environments.map((env2) => env2.name);
throw new Error(
`No environment found in config for: ${import_chalk3.default.bgWhiteBright.blackBright(
envName
)}
Environments defined in config are: ${envList}
`
);
}
process.env.MOON_TEST_ENV = envName;
try {
await executeTests(env, additionalArgs);
} catch (e) {
console.error(e);
await MoonwallContext.destroy();
process.exit(1);
}
}
async function executeTests(env, additionalArgs) {
const globalConfig = await importJsonConfig();
if (env.foundation.type === "read_only") {
try {
const ctx = await contextCreator(globalConfig, process.env.MOON_TEST_ENV);
const chainData = ctx.providers.filter((provider) => provider.type == "moon" || provider.type == "polkadotJs").map((provider) => {
return {
[provider.name]: {
rtName: provider.greet().rtName,
rtVersion: provider.greet().rtVersion
}
};
});
const { rtVersion, rtName } = Object.values(chainData[0])[0];
process.env.MOON_RTVERSION = rtVersion;
process.env.MOON_RTNAME = rtName;
await ctx.disconnect();
} catch {
}
}
const options = {
watch: false,
globals: true,
reporters: env.html ? ["verbose", "html"] : ["verbose"],
testTimeout: 1e4,
hookTimeout: 5e5,
useAtomics: false,
passWithNoTests: false,
threads: true,
include: env.include ? env.include : ["**/{test,spec,test_,test-}*{ts,mts,cts}"]
};
if (!env.multiThreads || process.env.MOON_SINGLE_THREAD === "true") {
options.threads = false;
}
try {
const folders = env.testFileDir.map((folder) => import_path2.default.join("/", folder, "/"));
await (0, import_node.startVitest)("test", folders, { ...options, ...additionalArgs });
} catch (e) {
console.error(e);
process.exit(1);
}
}
// src/cmds/runNetwork.ts
var import_yaml = require("yaml");
var import_promises3 = __toESM(require("fs/promises"), 1);
import_inquirer2.default.registerPrompt("press-to-continue", import_inquirer_press_to_continue2.default);
async function runNetwork(args) {
process.env.MOON_TEST_ENV = args.envName;
const globalConfig = await importJsonConfig();
const testFileDirs = globalConfig.environments.find(
({ name }) => name == args.envName
).testFileDir;
const questions2 = [
{
type: "confirm",
name: "Quit",
message: "\u2139\uFE0F Are you sure you'd like to close network and quit? \n",
default: false
},
{
name: "Choice",
type: "list",
message: "What would you like todo now",
choices: ["Chill", "Info", "Test", "Quit"]
},
{
name: "MenuChoice",
type: "list",
message: `Environment : ${import_chalk4.default.bgGray.cyanBright(args.envName)}
Please select a choice: `,
default: 0,
pageSize: 10,
choices: [
{
name: "Tail: Print the logs of the current running node to this console",
value: 1,
short: "tail"
},
{
name: `Info: Display Information about this environment ${args.envName}`,
value: 2,
short: "info"
},
{
name: testFileDirs.length > 0 ? "Test: Execute tests registered for this environment (" + import_chalk4.default.bgGrey.cyanBright(testFileDirs) + ")" : import_chalk4.default.dim("Test: NO TESTS SPECIFIED"),
value: 3,
disabled: testFileDirs.length > 0 ? false : true,
short: "test"
},
{
name: testFileDirs.length > 0 ? "GrepTest: Execute individual test(s) based on grepping the name / ID (" + import_chalk4.default.bgGrey.cyanBright(testFileDirs) + ")" : import_chalk4.default.dim("Test: NO TESTS SPECIFIED"),
value: 4,
disabled: testFileDirs.length > 0 ? false : true,
short: "test"
},
{
name: "Quit: Close network and quit the application",
value: 5,
short: "quit"
}
],
filter(val) {
return val;
}
},
{
name: "NetworkStarted",
type: "press-to-continue",
anyKey: true,
pressToContinueMessage: "\u2705 Press any key to continue...\n"
}
];
await runNetworkOnly(globalConfig);
(0, import_clear.default)();
const portsList = await reportServicePorts();
portsList.forEach(
(ports) => console.log(` \u{1F5A5}\uFE0F https://polkadot.js.org/apps/?rpc=ws%3A%2F%2F127.0.0.1%3A${ports.wsPort}`)
);
await import_inquirer2.default.prompt(questions2.find(({ name }) => name == "NetworkStarted"));
mainloop:
while (true) {
const choice = await import_inquirer2.default.prompt(questions2.find(({ name }) => name == "MenuChoice"));
const env = globalConfig.environments.find(({ name }) => name === args.envName);
switch (choice.MenuChoice) {
case 1:
(0, import_clear.default)();
await resolveTailChoice();
(0, import_clear.default)();
break;
case 2:
resolveInfoChoice(env);
await reportServicePorts();
break;
case 3:
await resolveTestChoice(env);
break;
case 4:
await resolveGrepChoice(env);
case 5:
const quit = await import_inquirer2.default.prompt(questions2.find(({ name }) => name == "Quit"));
if (quit.Quit === true) {
break mainloop;
}
break;
default:
throw new Error("invalid value");
}
}
await MoonwallContext.destroy();
console.log(`Goodbye! \u{1F44B}`);
process.exit(0);
}
var reportServicePorts = async () => {
const ctx = MoonwallContext.getContext();
const portsList = [];
const globalConfig = await importJsonConfig();
const config = globalConfig.environments.find(({ name }) => name == process.env.MOON_TEST_ENV);
if (config.foundation.type == "dev") {
const ports = { wsPort: "", httpPort: "" };
ports.wsPort = ctx.environment.nodes[0].args.find((a) => a.includes("ws-port")).split("=")[1] || "9944";
ports.httpPort = ctx.environment.nodes[0].args.find((a) => a.includes("rpc-port")).split("=")[1] || "9933";
portsList.push(ports);
} else if (config.foundation.type == "chopsticks") {
portsList.push(
...await Promise.all(
config.foundation.launchSpec.map(async ({ configPath }) => {
const yaml = (0, import_yaml.parse)((await import_promises3.default.readFile(configPath)).toString());
return {
wsPort: yaml.port || "8000",
httpPort: "<\u{1F3D7}\uFE0F NOT YET IMPLEMENTED>"
};
})
)
);
}
portsList.forEach(
(ports) => console.log(
` \u{1F310} Node has started, listening on ports - Websocket: ${ports.wsPort} and HTTP: ${ports.httpPort}`
)
);
return portsList;
};
var resolveInfoChoice = async (env) => {
console.log(import_chalk4.default.bgWhite.blackBright("Node Launch args:"));
console.dir(MoonwallContext.getContext().environment, { depth: null });
console.log(import_chalk4.default.bgWhite.blackBright("Launch Spec in Config File:"));
console.dir(env, { depth: null });
};
var resolveGrepChoice = async (env) => {
const choice = await import_inquirer2.default.prompt({
name: "grep",
type: "input",
message: `What pattern would you like to filter for (ID/Title): `,
default: "D01T01"
});
return await executeTests(env, { testNamePattern: choice.grep });
};
var resolveTestChoice = async (env) => {
process.env.MOON_RECYCLE = "true";
return await executeTests(env);
};
var resolveTailChoice = async () => {
const ui = new import_inquirer2.default.ui.BottomBar();
await new Promise(async (resolve) => {
const runningNode = MoonwallContext.getContext().nodes[0];
const onData = (chunk) => ui.log.write(chunk.toString());
runningNode.stderr.on("data", onData);
runningNode.stdout.on("data", onData);
import_inquirer2.default.prompt({
name: "exitTail",
type: "press-to-continue",
anyKey: true,
pressToContinueMessage: " Press any key to stop tailing logs and go back \u21A9\uFE0F"
}).then(() => {
runningNode.stderr.off("data", onData);
runningNode.stdout.off("data", onData);
resolve("");
});
});
};
// src/cmds/fetchArtifact.ts
var import_promises4 = __toESM(require("fs/promises"), 1);
var import_path3 = __toESM(require("path"), 1);
var import_node_fetch2 = __toESM(require("node-fetch"), 1);
var import_chalk5 = __toESM(require("chalk"), 1);
// src/internal/runner.ts
var import_child_process2 = __toESM(require("child_process"), 1);
var import_node_util = require("util");
var import_debug4 = __toESM(require("debug"), 1);
var debug2 = (0, import_debug4.default)("actions:runner");
var execAsync = (0, import_node_util.promisify)(import_child_process2.default.exec);
async function runTask(cmd, { cwd, env } = {
cwd: process.cwd()
}, title) {
debug2(`${title ? `Title: ${title}
` : ""}Running task on directory ${cwd}: ${cmd}
`);
try {
const result = await execAsync(cmd, { cwd, env });
return result.stdout;
} catch (error) {
console.log(error);
debug2(`Caught exception in command execution. Error[${error.status}] ${error.message}
`);
throw error;
}
}
// src/internal/downloader.ts
var import_cli_progress = require("cli-progress");
var import_node_fetch = __toESM(require("node-fetch"), 1);
var import_node_fs = __toESM(require("fs"), 1);
var progressBar;
var onStart = (length) => {
progressBar = new import_cli_progress.SingleBar(
{
etaAsynchronousUpdate: true,
etaBuffer: 40,
format: "Downloading: [{bar}] {percentage}% | ETA: {eta_formatted} | {value}/{total}"
},
import_cli_progress.Presets.shades_classic
);
progressBar.start(length, 0);
};
var onProgress = (bytes) => {
progressBar.update(bytes);
};
var onComplete = () => {
progressBar.stop();
process.stdout.write(` \u{1F4BE} Saving binary artefact...`);
};
async function downloader(url, outputPath) {
const tempPath = outputPath + ".tmp";
const writeStream = import_node_fs.default.createWriteStream(tempPath);
let transferredBytes = 0;
const response = await (0, import_node_fetch.default)(url);
const readStream = response.body;
readStream.pipe(writeStream);
await new Promise((resolve, reject) => {
const contentLength = parseInt(response.headers.get("Content-Length") || "0");
onStart(contentLength);
readStream.on("data", (chunk) => {
transferredBytes += chunk.length;
onProgress(transferredBytes);
});
readStream.on("end", () => {
writeStream.end();
onComplete();
writeStream.close(() => resolve("Finished!"));
});
readStream.on("error", () => {
reject("Error!");
});
});
try {
import_node_fs.default.writeFileSync(outputPath, import_node_fs.default.readFileSync(tempPath));
import_node_fs.default.rmSync(tempPath);
} catch (e) {
throw new Error(e);
}
}
// src/cmds/fetchArtifact.ts
var supportedBinaries = [
"moonbeam",
"polkadot",
"moonbase-runtime",
"moonbeam-runtime",
"moonriver-runtime"
];
var repos = {
moonbeam: "https://api.github.com/repos/purestake/moonbeam/releases",
polkadot: "https://api.github.com/repos/paritytech/polkadot/releases"
};
async function fetchArtifact(args) {
if (!supportedBinaries.includes(args.artifact)) {
throw new Error(`Downloading ${args.artifact} unsupported`);
}
if (await import_promises4.default.access(args.path).catch(() => true)) {
console.log("Folder not exists, creating");
import_promises4.default.mkdir(args.path);
}
const binary = args.artifact;
const repoName = args.artifact.includes("-runtime") ? "moonbeam" : args.artifact;
const enteredPath = args.path ? args.path : "tmp/";
const binaryPath = import_path3.default.join("./", enteredPath, binary);
const releases = await (await (0, import_node_fetch2.default)(repos[repoName])).json();
const release = args.artifact.includes("-runtime") ? releases.find((release2) => {
if (args.binVersion === "latest") {
return release2.assets.find((asset2) => asset2.name.includes(binary));
} else {
return release2.assets.find((asset2) => asset2.name === `${binary}-${args.binVersion}.wasm`);
}
}) : args.binVersion === "latest" ? releases.find((release2) => release2.assets.find((asset2) => asset2.name === binary)) : releases.filter((release2) => release2.tag_name.includes("v" + args.binVersion)).find((release2) => release2.assets.find((asset2) => asset2.name === binary));
if (release == null) {
throw new Error(`Release not found for ${args.binVersion}`);
}
const asset = args.artifact.includes("-runtime") ? release.assets.find((asset2) => asset2.name.includes(binary) && asset2.name.includes("wasm")) : release.assets.find((asset2) => asset2.name === binary);
if (binary == "moonbeam" || binary == "polkadot") {
await downloader(asset.browser_download_url, binaryPath);
await import_promises4.default.chmod(binaryPath, "755");
const version = (await runTask(`./${binaryPath} --version`)).trim();
process.stdout.write(` ${import_chalk5.default.green(version.trim())} \u2713
`);
}
if (binary.includes("-runtime")) {
const binaryPath2 = import_path3.default.join("./", args.path, `${args.artifact}-${args.binVersion}.wasm`);
await downloader(asset.browser_download_url, binaryPath2);
await import_promises4.default.chmod(binaryPath2, "755");
process.stdout.write(` ${import_chalk5.default.green("done")} \u2713
`);
}
}
// package.json
var package_default = {
name: "@moonsong-labs/moonwall-cli",
type: "module",
version: "0.3.1",
description: "Testing framework for the Moon family of projects",
author: "timbrinded",
license: "ISC",
homepage: "https://github.com/Moonsong-Labs/moonwall#readme",
repository: {
type: "git",
url: "git+https://github.com/Moonsong-Labs/moonwall.git",
directory: "packages/moonwall"
},
bugs: {
url: "https://github.com/Moonsong-Labs/moonwall/issues"
},
keywords: [
"moonwall",
"moonbeam",
"moondance",
"polkadot",
"kusama",
"substrate"
],
exports: "./dist/index.js",
module: "./dist/index.mjs",
types: "./dist/index.d.ts",
bin: {
moonwall: "./moonwall.mjs"
},
engines: {
node: ">=14.16.0",
pnpm: ">=7"
},
files: [
"dist",
"bin",
"*.d.ts",
"*.d.cts",
"*.mjs",
"*.cjs"
],
scripts: {
build: "pnpm exec rimraf dist && tsup src --format cjs,esm --dts ",
watch: "tsup src --format cjs,esm --dts --watch",
typecheck: "pnpm exec tsc --noEmit",
prepublish: "pnpm run build",
prepare: "pnpm build",
schema: "typescript-json-schema ./src/types/config.ts MoonwallConfig > config_schema.json",
compile: "pnpm build ",
lint: "tsc"
},
dependencies: {
"@acala-network/chopsticks": "^0.5.10",
"@moonbeam-network/api-augment": "^0.2201.0",
"@moonsong-labs/moonwall-util": "workspace:*",
"@polkadot/api": "^10.2.1",
"@polkadot/api-augment": "^10.2.1",
"@polkadot/api-derive": "^10.2.1",
"@polkadot/keyring": "^11.1.2",
"@polkadot/types": "^10.2.1",
"@polkadot/types-codec": "^10.2.1",
"@polkadot/util": "^11.1.2",
"@types/cli-progress": "^3.11.0",
"@types/node": "^18.15.10",
"@types/yargs": "^17.0.23",
bottleneck: "^2.19.5",
chalk: "^5.2.0",
clear: "^0.1.0",
"cli-progress": "^3.12.0",
colors: "^1.4.0",
debug: "^4.3.4",
dotenv: "^16.0.3",
ethers: "^6.2.3",
inquirer: "^8.2.5",
"inquirer-press-to-continue": "^1.1.4",
"moonbeam-types-bundle": "^2.0.10",
"node-fetch": "^3.3.1",
prettier: "^2.8.7",
"pretty-bytes": "^6.1.0",
"request-progress": "^3.0.0",
semver: "^7.3.8",
superagent: "^8.0.9",
"ts-node": "^10.9.1",
tsup: "^6.7.0",
vitest: "^0.29.7",
web3: "4.0.1-rc.0",
"web3-providers-ws": "4.0.1-rc.0",
ws: "^8.13.0",
yaml: "^2.2.1",
yargs: "^17.7.1"
},
devDependencies: {
"@types/debug": "^4.1.7",
"@vitest/ui": "^0.28.5",
"regenerator-runtime": "^0.13.11",
typescript: "^4.9.5",
"typescript-json-schema": "^0.55.0"
},
pnpm: {
overrides: {
"@moonsong-labs/moonwall-util": "workspace:*",
"@polkadot/rpc-provider": "$@polkadot/rpc-provider",
"@polkadot/util": "$@polkadot/rpc-provider",
"@polkadot/keyring": "$@polkadot/rpc-provider",
"@polkadot/api": "$@polkadot/api",
"@polkadot/types": "$@polkadot/api"
}
},
publishConfig: {
access: "public"
}
};
// src/cmds/main.ts
var import_semver = require("semver");
var import_node_fetch3 = __toESM(require("node-fetch"), 1);
import_inquirer3.default.registerPrompt("press-to-continue", import_inquirer_press_to_continue3.default);
async function main() {
while (true) {
let globalConfig;
try {
globalConfig = await importJsonConfig();
} catch (e) {
console.log(e);
}
(0, import_clear2.default)();
await printIntro();
if (await mainMenu(globalConfig)) {
break;
} else {
continue;
}
}
process.stdout.write(`Goodbye! \u{1F44B}
`);
process.exit(0);
}
async function mainMenu(config) {
const configPresent = config !== void 0;
const questionList = {
name: "MenuChoice",
type: "list",
message: `Main Menu - Please select one of the following:`,
default: 0,
pageSize: 12,
choices: [
{
name: !configPresent ? "1) Initialise: Generate a new Moonwall Config File." : import_chalk6.default.dim("1) Initialise: \u2705 CONFIG ALREADY GENERATED"),
value: "init",
disabled: configPresent
},
{
name: configPresent ? `2) Network Launcher & Toolbox: Launch network, access tools: tail logs, interactive tests etc.` : import_chalk6.default.dim("2) Network Launcher & Toolbox NO CONFIG FOUND"),
value: "run",
disabled: !configPresent
},
{
name: configPresent ? "3) Test Suite Execution: Run automated tests, start network if needed." : import_chalk6.default.dim("3) Test Suite Execution: NO CONFIG FOUND"),
value: "test",
disabled: !configPresent
},
{
name: import_chalk6.default.dim("4) Batch-Run Tests: \u{1F3D7}\uFE0F NOT YET IMPLEMENTED "),
value: "batch",
disabled: true
},
{
name: "5) Artifact Downloader: Fetch artifacts from GitHub repos.",
value: "download",
disabled: false
},
{
name: `6) Quit Application`,
value: "quit"
}
],
filter(val) {
return val;
}
};
const answers = await import_inquirer3.default.prompt(questionList);
switch (answers.MenuChoice) {
case "init":
await generateConfig();
return false;
case "run":
const chosenRunEnv = await chooseRunEnv(config);
if (chosenRunEnv.envName !== "back") {
await runNetwork(chosenRunEnv);
}
return false;
case "test":
const chosenTestEnv = await chooseTestEnv(config);
if (chosenTestEnv.envName !== "back") {
await testCmd(chosenTestEnv.envName);
await import_inquirer3.default.prompt({
name: "test complete",
type: "press-to-continue",
anyKey: true,
pressToContinueMessage: `\u2139\uFE0F Test run for ${import_chalk6.default.bgWhiteBright.black(
chosenTestEnv.envName
)} has been completed. Press any key to continue...
`
});
}
return false;
case "download":
await resolveDownloadChoice();
return false;
case "quit":
return await resolveQuitChoice();
}
}
async function resolveDownloadChoice() {
while (true) {
const firstChoice = await import_inquirer3.default.prompt({
name: "artifact",
type: "list",
message: `Download - which artifact?`,
choices: [
"moonbeam",
"polkadot",
"moonbase-runtime",
"moonriver-runtime",
"moonbeam-runtime",
new import_inquirer3.default.Separator(),
"Back",
new import_inquirer3.default.Separator()
]
});
if (firstChoice.artifact === "Back") {
return;
}
const otherChoices = await import_inquirer3.default.prompt([
{
name: "binVersion",
type: "input",
default: "latest",
message: `Download - which version?`
},
{
name: "path",
type: "input",
message: `Download - where would you like it placed?`,
default: "./tmp"
}
]);
const result = await import_inquirer3.default.prompt({
name: "continue",
type: "confirm",
message: `You are about to download ${import_chalk6.default.bgWhite.blackBright(
firstChoice.artifact
)} v-${import_chalk6.default.bgWhite.blackBright(otherChoices.binVersion)} to: ${import_chalk6.default.bgWhite.blackBright(
otherChoices.path
)}.
Would you like to continue? `,
default: true
});
if (result.continue === false) {
continue;
}
await fetchArtifact({
artifact: firstChoice.artifact,
binVersion: otherChoices.binVersion,
path: otherChoices.path
});
await import_inquirer3.default.prompt({
name: "NetworkStarted",
type: "press-to-continue",
anyKey: true,
pressToContinueMessage: `\u2705 Artifact has been downloaded. Press any key to continue...
`
});
return;
}
}
var chooseTestEnv = async (config) => {
const envs = config.environments.map((a) => ({
name: `Env: ${a.name} (${a.foundation.type})`,
value: a.name,
disabled: false
})).sort((a, b) => a.name > b.name ? -1 : 1);
envs.push(
...[
new import_inquirer3.default.Separator(),
{ name: "Back", value: "back" },
new import_inquirer3.default.Separator()
]
);
const result = await import_inquirer3.default.prompt({
name: "envName",
message: "Select a environment to run",
type: "list",
pageSize: 12,
choices: envs
});
return result;
};
var chooseRunEnv = async (config) => {
const envs = config.environments.map((a) => {
const result2 = { name: "", value: a.name, disabled: false };
if (a.foundation.type === "dev" || a.foundation.type === "chopsticks") {
result2.name = `Env: ${a.name} (${a.foundation.type})`;
} else {
result2.name = import_chalk6.default.dim(`Env: ${a.name} (${a.foundation.type}) NO NETWORK TO RUN`);
result2.disabled = true;
}
return result2;
});
const choices = [
...envs.filter(({ disabled }) => disabled === false).sort((a, b) => a.name > b.name ? 1 : -1),
new import_inquirer3.default.Separator(),
...envs.filter(({ disabled }) => disabled === true).sort((a, b) => a.name > b.name ? 1 : -1),
new import_inquirer3.default.Separator(),
{ name: "Back", value: "back" },
new import_inquirer3.default.Separator()
];
const result = await import_inquirer3.default.prompt({
name: "envName",
message: "Select a environment to run",
type: "list",
pageSize: 12,
choices
});
return result;
};
var resolveQuitChoice = async () => {
const result = await import_inquirer3.default.prompt({
name: "Quit",
type: "confirm",
message: "Are you sure you want to Quit?",
default: false
});
return result.Quit;
};
var printIntro = async () => {
const currentVersion = new import_semver.SemVer(package_default.version);
const resp = await (0, import_node_fetch3.default)("https://registry.npmjs.org/@moonsong-labs/moonwall-cli/latest");
const json = await resp.json();
const npmVersion = new import_semver.SemVer(json["version"]);
const logo = import_chalk6.default.cyan(`
####################
############################
###################################
########################################
###########################################
##############################################
################################################
.#################################################
##################################################
##################################################
`) + import_chalk6.default.magenta(`
*** ************************************************************
**** *********************************************
*** ******************************************************
** *********************** *********************************************
** *********************** ********************************************
*** ******************************
**** *****************************
`);
process.stdout.write(logo);
p