UNPKG

@moonwall/cli

Version:

Testing framework for the Moon family of projects

306 lines 12.4 kB
import { createLogger } from "@moonwall/util"; import path from "node:path"; import net from "node:net"; import { Effect } from "effect"; import { standardRepos } from "../lib/repoDefinitions"; import { shardManager } from "../lib/shardManager"; import invariant from "tiny-invariant"; import { StartupCacheService, StartupCacheServiceLive } from "./effect/StartupCacheService.js"; const logger = createLogger({ name: "commandParsers" }); export function parseZombieCmd(launchSpec) { if (launchSpec) { return { cmd: launchSpec.configPath }; } throw new Error("No ZombieSpec found in config. Are you sure your moonwall.config.json file has the correct 'configPath' in zombieSpec?"); } function fetchDefaultArgs(binName, additionalRepos = []) { let defaultArgs; const repos = [...standardRepos(), ...additionalRepos]; for (const repo of repos) { const foundBin = repo.binaries.find((bin) => bin.name === binName); if (foundBin) { defaultArgs = foundBin.defaultArgs; break; } } if (!defaultArgs) { defaultArgs = ["--dev"]; } return defaultArgs; } export class LaunchCommandParser { args; cmd; launch; launchSpec; launchOverrides; constructor(options) { const { launchSpec, additionalRepos, launchOverrides } = options; this.launchSpec = launchSpec; this.launchOverrides = launchOverrides; this.launch = !launchSpec.running ? true : launchSpec.running; this.cmd = launchSpec.binPath; this.args = launchSpec.options ? [...launchSpec.options] : fetchDefaultArgs(path.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); } } async withPorts() { // In RECYCLE mode, the node is already running if (process.env.MOON_RECYCLE === "true") { const existingPort = process.env.MOONWALL_RPC_PORT; if (existingPort) { this.overrideArg(`--rpc-port=${existingPort}`); } return this; } 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 = (await getFreePort()).toString(); // Always pin the rpc port so the provider endpoint matches exactly the spawned node. process.env.MOONWALL_RPC_PORT = freePort; this.overrideArg(`--rpc-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() { logger.debug(`Command to run: ${this.cmd}`); logger.debug(`Arguments: ${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"); } } /** * Cache startup artifacts if enabled in launchSpec. * This uses an Effect-based service that caches artifacts by binary hash. * * When cacheStartupArtifacts is enabled, this generates: * 1. Precompiled WASM for the runtime * 2. Raw chain spec to skip genesis WASM compilation * * This reduces startup from ~3s to ~200ms (~10x improvement). */ async withStartupCache() { if (!this.launchSpec.cacheStartupArtifacts) { return this; } // Skip for Docker images if (this.launchSpec.useDocker) { logger.warn("Startup caching is not supported for Docker images, skipping"); return this; } // Extract chain argument from existing args (e.g., "--chain=moonbase-dev") const chainArg = this.args.find((arg) => arg.startsWith("--chain")); // Check if using --dev flag const hasDevFlag = this.args.includes("--dev"); // Extract chain name from --chain=XXX or --chain XXX const existingChainName = chainArg?.match(/--chain[=\s]?(\S+)/)?.[1]; // We can generate raw chain spec for both --dev mode and explicit --chain=XXX const canGenerateRawSpec = hasDevFlag || !!existingChainName; const cacheDir = this.launchSpec.startupCacheDir || path.join(process.cwd(), "tmp", "startup-cache"); const program = StartupCacheService.pipe(Effect.flatMap((service) => service.getCachedArtifacts({ binPath: this.launchSpec.binPath, chainArg, cacheDir, // Generate raw chain spec for faster startup (works for both --dev and --chain=XXX) generateRawChainSpec: canGenerateRawSpec, // Pass dev mode flag for proper chain name detection isDevMode: hasDevFlag, })), Effect.provide(StartupCacheServiceLive)); try { const result = await Effect.runPromise(program); // --wasmtime-precompiled expects a DIRECTORY, not a file path // Get the directory containing the precompiled wasm const precompiledDir = path.dirname(result.precompiledPath); this.overrideArg(`--wasmtime-precompiled=${precompiledDir}`); // If we have a raw chain spec, use it for ~10x faster startup if (result.rawChainSpecPath) { if (hasDevFlag) { // Remove --dev flag and add equivalent flags this.args = this.args.filter((arg) => arg !== "--dev"); this.overrideArg(`--chain=${result.rawChainSpecPath}`); // Add flags that --dev would normally set this.overrideArg("--alice"); this.overrideArg("--force-authoring"); this.overrideArg("--rpc-cors=all"); // Use a deterministic node key for consistency this.overrideArg("--node-key=0000000000000000000000000000000000000000000000000000000000000001"); } else if (existingChainName) { // Replace original --chain=XXX with --chain=<raw-spec-path> this.overrideArg(`--chain=${result.rawChainSpecPath}`); } logger.debug(`Using raw chain spec for ~10x faster startup: ${result.rawChainSpecPath}`); } // Set cache directory env var for metadata caching in provider factories process.env.MOONWALL_CACHE_DIR = precompiledDir; logger.debug(result.fromCache ? `Using cached precompiled WASM: ${result.precompiledPath}` : `Precompiled WASM created: ${result.precompiledPath}`); } catch (error) { // Log warning but continue without precompilation logger.warn(`WASM precompilation failed, continuing without: ${error}`); } return this; } build() { return { cmd: this.cmd, args: this.args, launch: this.launch, }; } static async create(options) { const parser = new LaunchCommandParser(options); const parsed = await parser .withPorts() .then((p) => p.withDefaultForkConfig().withLaunchOverrides()) .then((p) => p.withStartupCache()); if (options.verbose) { parsed.print(); } return parsed.build(); } } export function parseChopsticksRunCmd(launchSpecs) { const launch = !launchSpecs[0].running ? true : launchSpecs[0].running; if (launchSpecs.length === 1) { const chopsticksCmd = "node"; const chopsticksArgs = [ "node_modules/@acala-network/chopsticks/chopsticks.cjs", `--config=${launchSpecs[0].configPath}`, `--host=${launchSpecs[0].address ?? "127.0.0.1"}`, ]; const mode = launchSpecs[0].buildBlockMode ? launchSpecs[0].buildBlockMode : "manual"; const num = mode === "batch" ? "Batch" : mode === "instant" ? "Instant" : "Manual"; chopsticksArgs.push(`--build-block-mode=${num}`); if (launchSpecs[0].wsPort) { chopsticksArgs.push(`--port=${launchSpecs[0].wsPort}`); } if (launchSpecs[0].wasmOverride) { chopsticksArgs.push(`--wasm-override=${launchSpecs[0].wasmOverride}`); } if (launchSpecs[0].allowUnresolvedImports) { chopsticksArgs.push("--allow-unresolved-imports"); } return { cmd: chopsticksCmd, args: chopsticksArgs, 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, }; } /** * Check if a port is available for use */ const isPortAvailable = async (port) => { return new Promise((resolve) => { const server = net.createServer(); server.listen(port, () => { server.once("close", () => resolve(true)); server.close(); }); server.on("error", () => resolve(false)); }); }; /** * Get the next available port starting from a given port */ const getNextAvailablePort = async (startPort) => { let port = startPort; while (port <= 65535) { if (await isPortAvailable(port)) { return port; } port++; } throw new Error(`No available ports found starting from ${startPort}`); }; /** * Get a free port with availability checking * Uses async port allocation for better collision avoidance */ export const getFreePort = async () => { // Get shard information from centralized manager const shardIndex = shardManager.getShardIndex(); const totalShards = shardManager.getTotalShards(); // Use VITEST_POOL_ID as additional offset if available const poolId = parseInt(process.env.VITEST_POOL_ID || "0", 10); // Calculate port with better isolation between shards // Base port 10000 + (shard * 1000) + (pool * 100) + deterministic offset const basePort = 10000; const shardOffset = shardIndex * 1000; const poolOffset = poolId * 100; // Use a deterministic but unique offset based on environment const processOffset = process.pid % 50; const calculatedPort = basePort + shardOffset + poolOffset + processOffset; // Ensure we stay within a reasonable port range const startPort = Math.min(calculatedPort, 60000 + shardIndex * 100 + poolId); logger.debug(`Port calculation: shard=${shardIndex + 1}/${totalShards}, pool=${poolId}, final=${startPort}`); return getNextAvailablePort(startPort); }; //# sourceMappingURL=commandParsers.js.map