From 6310f0d3fc83f386df4c42ced37a41bb66fe251f Mon Sep 17 00:00:00 2001 From: Facundo Farall <37149322+ffarall@users.noreply.github.com> Date: Tue, 15 Apr 2025 11:01:24 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=A7=91=E2=80=8D=F0=9F=92=BB=20Tur?= =?UTF-8?q?n=20scripts=20into=20an=20interactive=20CLI=20(#41)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WARNING: This PR changes the kurtosis package to use the one from upstream, not our fork, as it was not working at the moment. This should be changed when fixed. In this PR: 1. Turn `launch-kurtosis` script into a CLI, which parses parameters and can interactively run multiple steps. 2. Separate steps of such CLI into their own scripts. 1. New script created `launch-kurtosis`, which detects if an enclave is running and prompts to relaunch it if it is. 2. New script created `deploy-contracts` to deploy all contracts. It can optionally verify them as well. 3. Each step can be interactively opted in/out, or choose whether to run it via CLI params. 4. The CLI offers a help command as well. 5. Cleanup logs of CLI. Logs of internal commands ran can be printed with LOG_LEVEL=debug. In case of error, they are always printed out. --- test/bun.lock | 3 + test/package.json | 8 +- test/scripts/deploy-contracts.ts | 145 +++++++++++++++++++++++ test/scripts/e2e-cli.ts | 190 +++++++++++++++++++++++++++++++ test/scripts/launch-kurtosis.ts | 95 ++++++++++++++++ test/scripts/start-kurtosis.ts | 139 ---------------------- test/utils/docker.ts | 4 +- test/utils/index.ts | 1 + test/utils/input.ts | 51 +++++++++ test/utils/logger.ts | 42 ++++++- 10 files changed, 532 insertions(+), 146 deletions(-) create mode 100644 test/scripts/deploy-contracts.ts create mode 100644 test/scripts/e2e-cli.ts create mode 100644 test/scripts/launch-kurtosis.ts delete mode 100644 test/scripts/start-kurtosis.ts create mode 100644 test/utils/input.ts diff --git a/test/bun.lock b/test/bun.lock index 5b0848fd..e23384d7 100644 --- a/test/bun.lock +++ b/test/bun.lock @@ -7,6 +7,7 @@ "@biomejs/biome": "^1.9.4", "@dotenvx/dotenvx": "^1.39.0", "@types/dockerode": "^3.3.37", + "chalk": "^5.4.1", "dockerode": "^4.0.5", "dotenv": "^16.4.7", "pino": "^9.6.0", @@ -121,6 +122,8 @@ "bun-types": ["bun-types@1.2.7", "", { "dependencies": { "@types/node": "*", "@types/ws": "*" } }, "sha512-P4hHhk7kjF99acXqKvltyuMQ2kf/rzIw3ylEDpCxDS9Xa0X0Yp/gJu/vDCucmWpiur5qJ0lwB2bWzOXa2GlHqA=="], + "chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], + "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], diff --git a/test/package.json b/test/package.json index bc8fe97d..20d3ff1a 100644 --- a/test/package.json +++ b/test/package.json @@ -6,12 +6,11 @@ "scripts": { "fmt": "biome check .", "fmt:fix": "biome check --write .", - "demo": "bun run scripts/placeholder.ts", - "start:e2e:verified": "bun run scripts/start-kurtosis.ts --verified", - "start:e2e:minimal": "bun run scripts/start-kurtosis.ts", + "start:e2e:verified": "bun run scripts/e2e-cli.ts --verified", + "start:e2e:minimal": "bun run scripts/e2e-cli.ts", "stop:e2e": "kurtosis enclave stop datahaven-ethereum && kurtosis clean && kurtosis engine stop && docker container prune -f", "stop:e2e:verified": "bun stop:e2e", - "stop:e2e:minimal": "bun stop:e2e", + "stop:e2e:minimal": "bun stop:e2e", "stop:kurtosis-engine": "kurtosis engine stop && docker container prune -f", "test:e2e": "bun test suites/e2e" }, @@ -25,6 +24,7 @@ "@biomejs/biome": "^1.9.4", "@dotenvx/dotenvx": "^1.39.0", "@types/dockerode": "^3.3.37", + "chalk": "^5.4.1", "dockerode": "^4.0.5", "dotenv": "^16.4.7", "pino": "^9.6.0", diff --git a/test/scripts/deploy-contracts.ts b/test/scripts/deploy-contracts.ts new file mode 100644 index 00000000..7d81f44f --- /dev/null +++ b/test/scripts/deploy-contracts.ts @@ -0,0 +1,145 @@ +import { $ } from "bun"; +import invariant from "tiny-invariant"; +import { logger, printHeader, promptWithTimeout } from "utils"; + +interface DeployContractsOptions { + rpcUrl: string; + verified?: boolean; + blockscoutBackendUrl?: string; + deployContracts?: boolean; +} + +/** + * Deploys smart contracts to the specified RPC URL + * + * @param options - Configuration options for deployment + * @param options.rpcUrl - The RPC URL to deploy to + * @param options.verified - Whether to verify contracts (requires blockscoutBackendUrl) + * @param options.blockscoutBackendUrl - URL for the Blockscout API (required if verified is true) + * @param options.deployContracts - Flag to control deployment (if undefined, will prompt) + * @returns Promise resolving to true if contracts were deployed successfully, false if skipped + */ +export const deployContracts = async (options: DeployContractsOptions): Promise => { + const { rpcUrl, verified = false, blockscoutBackendUrl, deployContracts } = options; + + // Check if deployContracts option was set via flags, or prompt if not + let shouldDeployContracts = deployContracts; + if (shouldDeployContracts === undefined) { + shouldDeployContracts = await promptWithTimeout( + "Do you want to deploy the smart contracts?", + true, + 10 + ); + } else { + logger.info( + `Using flag option: ${shouldDeployContracts ? "will deploy" : "will not deploy"} smart contracts` + ); + } + + if (!shouldDeployContracts) { + logger.info("Skipping contract deployment. Done!"); + return false; + } + + // Check if required parameters are provided + invariant(rpcUrl, "โŒ RPC URL is required"); + if (verified) { + invariant(blockscoutBackendUrl, "โŒ Blockscout backend URL is required for verification"); + } + + printHeader("Deploying Smart Contracts"); + + // Build contracts + logger.info("๐Ÿ›ณ๏ธ Building contracts..."); + const { + exitCode: buildExitCode, + stderr: buildStderr, + stdout: buildStdout + } = await $`forge build`.cwd("../contracts").nothrow().quiet(); + + if (buildExitCode !== 0) { + logger.error(buildStderr.toString()); + throw Error("โŒ Contracts have failed to build properly."); + } + logger.debug(buildStdout.toString()); + + // Get forge path + const { stdout: forgePath } = await $`which forge`.quiet(); + const forgeExecutable = forgePath.toString().trim(); + + // Prepare deployment command + let deployCommand = `${forgeExecutable} script script/deploy/DeployLocal.s.sol --rpc-url ${rpcUrl} --color never -vv --no-rpc-rate-limit --non-interactive --broadcast`; + + if (verified && blockscoutBackendUrl) { + deployCommand += ` --verify --verifier blockscout --verifier-url ${blockscoutBackendUrl}/api/ --delay 0`; + logger.info("๐Ÿ” Contract verification enabled"); + } + + logger.info("โณ Deploying contracts (this might take a few minutes)..."); + + const { exitCode: deployExitCode, stderr: deployStderr } = await $`sh -c ${deployCommand}` + .cwd("../contracts") + .nothrow(); + + if (deployExitCode !== 0) { + logger.error(deployStderr.toString()); + throw Error("โŒ Contracts have failed to deploy properly."); + } + + logger.success("Contracts deployed successfully"); + return true; +}; + +// Allow script to be run directly with CLI arguments +if (import.meta.main) { + const args = process.argv.slice(2); + const options: { + rpcUrl?: string; + verified: boolean; + blockscoutBackendUrl?: string; + deployContracts?: boolean; + } = { + verified: args.includes("--verified"), + deployContracts: args.includes("--deploy-contracts") + ? true + : args.includes("--no-deploy-contracts") + ? false + : undefined + }; + + // Extract RPC URL + const rpcUrlIndex = args.indexOf("--rpc-url"); + if (rpcUrlIndex !== -1 && rpcUrlIndex + 1 < args.length) { + options.rpcUrl = args[rpcUrlIndex + 1]; + } + + // Extract Blockscout URL if verification is enabled + if (options.verified) { + const blockscoutUrlIndex = args.indexOf("--blockscout-url"); + if (blockscoutUrlIndex !== -1 && blockscoutUrlIndex + 1 < args.length) { + options.blockscoutBackendUrl = args[blockscoutUrlIndex + 1]; + } + } + + // Check required parameters + if (!options.rpcUrl) { + console.error("Error: --rpc-url parameter is required"); + process.exit(1); + } + + if (options.verified && !options.blockscoutBackendUrl) { + console.error("Error: --blockscout-url parameter is required when using --verified"); + process.exit(1); + } + + // Run deployment + deployContracts({ + rpcUrl: options.rpcUrl, + verified: options.verified, + blockscoutBackendUrl: options.blockscoutBackendUrl, + deployContracts: options.deployContracts + }).catch((error) => { + console.error("Deployment failed:", error); + process.exit(1); + }); +} diff --git a/test/scripts/e2e-cli.ts b/test/scripts/e2e-cli.ts new file mode 100644 index 00000000..1cd3c285 --- /dev/null +++ b/test/scripts/e2e-cli.ts @@ -0,0 +1,190 @@ +import { $ } from "bun"; +import invariant from "tiny-invariant"; +import chalk from "chalk"; +import { logger, printDivider, printHeader } from "utils"; +import sendTxn from "./send-txn"; +import { launchKurtosis } from "./launch-kurtosis"; +import { deployContracts } from "./deploy-contracts"; + +interface ScriptOptions { + verified: boolean; + launchKurtosis?: boolean; + deployContracts?: boolean; + help?: boolean; +} + +async function main() { + const args = process.argv.slice(2); + + // Parse command-line arguments + const options: ScriptOptions = { + verified: args.includes("--verified"), + launchKurtosis: parseFlag(args, "launchKurtosis"), + deployContracts: parseFlag(args, "deploy-contracts"), + help: args.includes("--help") || args.includes("-h") + }; + + // Show help menu if requested + if (options.help) { + printHelp(); + return; + } + + logger.info(`Running with options: ${getOptionsString(options)}`); + + const timeStart = performance.now(); + + printHeader("Environment Checks"); + + await checkDependencies(); + + // Clean up and launch Kurtosis enclave + const { services } = await launchKurtosis({ + launchKurtosis: options.launchKurtosis + }); + + // Send test transaction + printHeader("Setting Up Blockchain"); + const privateKey = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; // Anvil test acc1 + const networkRpcUrl = services.find((s) => s.service === "reth-1-rpc")?.url; + invariant(networkRpcUrl, "โŒ Network RPC URL not found"); + + logger.info("๐Ÿ’ธ Sending test transaction..."); + await sendTxn(privateKey, networkRpcUrl); + + printDivider(); + + // Display service information in a clean table + printHeader("Service Endpoints"); + + console.table( + services + .filter((s) => ["reth-1-rpc", "reth-2-rpc", "blockscout-backend", "dora"].includes(s.service)) + .concat([ + { service: "blockscout", port: "3000", url: "http://127.0.0.1:3000" }, + { service: "kurtosis-web", port: "9711", url: "http://127.0.0.1:9711" } + ]) + ); + + printDivider(); + + // Show completion information + const timeEnd = performance.now(); + const minutes = ((timeEnd - timeStart) / (1000 * 60)).toFixed(1); + + logger.success(`Kurtosis network started successfully in ${minutes} minutes`); + + printDivider(); + + // Deploy contracts using the extracted function + const blockscoutBackendUrl = services.find((s) => s.service === "blockscout-backend")?.url; + await deployContracts({ + rpcUrl: networkRpcUrl, + verified: options.verified, + blockscoutBackendUrl, + deployContracts: options.deployContracts + }); +} + +// Helper function to check all dependencies at once +const checkDependencies = async (): Promise => { + if (!(await checkKurtosisInstalled())) { + logger.error("Kurtosis CLI is required to be installed: https://docs.kurtosis.com/install"); + throw Error("โŒ Kurtosis CLI application not found."); + } + + logger.success("Kurtosis CLI found"); + + if (!(await checkDockerRunning())) { + logger.error("Is Docker Running? Unable to make connection to docker daemon"); + throw Error("โŒ Error connecting to Docker"); + } + + logger.success("Docker is running"); + + if (!(await checkForgeInstalled())) { + logger.error("Is foundry installed? https://book.getfoundry.sh/getting-started/installation"); + throw Error("โŒ forge binary not found in PATH"); + } + + logger.success("Forge is installed"); +}; + +const checkKurtosisInstalled = async (): Promise => { + const { exitCode, stderr, stdout } = await $`kurtosis version`.nothrow().quiet(); + if (exitCode !== 0) { + logger.error(stderr.toString()); + return false; + } + logger.debug(stdout.toString()); + return true; +}; + +const checkDockerRunning = async (): Promise => { + const { exitCode, stderr, stdout } = await $`docker system info`.nothrow().quiet(); + if (exitCode !== 0) { + logger.error(stderr.toString()); + return false; + } + logger.debug(stdout.toString()); + return true; +}; + +const checkForgeInstalled = async (): Promise => { + const { exitCode, stderr, stdout } = await $`forge --version`.nothrow().quiet(); + if (exitCode !== 0) { + logger.error(stderr.toString()); + return false; + } + logger.debug(stdout.toString()); + return true; +}; + +// Helper function to format options as a string +function getOptionsString(options: ScriptOptions): string { + const optionStrings: string[] = []; + if (options.verified) optionStrings.push("verified"); + if (options.launchKurtosis !== undefined) + optionStrings.push(`launchKurtosis=${options.launchKurtosis}`); + if (options.deployContracts !== undefined) + optionStrings.push(`deployContracts=${options.deployContracts}`); + return optionStrings.length ? optionStrings.join(", ") : "no options"; +} + +// Print help menu +function printHelp(): void { + console.log(chalk.bold.cyan("\nDatahaven Kurtosis Startup Script")); + console.log(chalk.gray("=".repeat(40))); + console.log(` +${chalk.yellow("Available Options:")} + +${chalk.green("--verified")} Use contract verification via Blockscout +${chalk.green("--launchKurtosis")} Clean and launch Kurtosis enclave if already running +${chalk.green("--no-launchKurtosis")} Keep existing Kurtosis enclave if already running +${chalk.green("--deploy-contracts")} Deploy smart contracts after Kurtosis starts +${chalk.green("--no-deploy-contracts")} Skip smart contract deployment +${chalk.green("--help, -h")} Show this help menu + +${chalk.yellow("Examples:")} + ${chalk.gray("# Start with interactive prompts")} + bun run start-kurtosis + + ${chalk.gray("# Start with verification and automatic redeploy")} + bun run start-kurtosis --verified --redeploy + + ${chalk.gray("# Start without deploying contracts")} + bun run start-kurtosis --no-deploy-contracts +`); +} + +// Parse and handle boolean flags with negations +function parseFlag(args: string[], flagName: string): boolean | undefined { + const positiveFlag = `--${flagName}`; + const negativeFlag = `--no-${flagName}`; + + if (args.includes(positiveFlag)) return true; + if (args.includes(negativeFlag)) return false; + return undefined; +} + +main(); diff --git a/test/scripts/launch-kurtosis.ts b/test/scripts/launch-kurtosis.ts new file mode 100644 index 00000000..57d26717 --- /dev/null +++ b/test/scripts/launch-kurtosis.ts @@ -0,0 +1,95 @@ +import { $ } from "bun"; +import { getServicesFromDocker, logger, printDivider, printHeader, promptWithTimeout } from "utils"; + +/** + * Launches a Kurtosis Ethereum network enclave for testing. + * + * This function checks if a Kurtosis network is already running. If it is: + * - With `launchKurtosis: false` - keeps the existing enclave + * - With `launchKurtosis: true` - cleans and relaunches the enclave + * - With `launchKurtosis: undefined` - prompts the user to decide whether to relaunch + * + * If no network is running, it launches a new one. + * + * @param options - Configuration options + * @param options.launchKurtosis - Whether to forcibly launch Kurtosis (true), keep existing (false), or prompt user (undefined) + * @returns Object containing success status and Docker services information + */ +export const launchKurtosis = async (options: { launchKurtosis?: boolean } = {}) => { + if (await checkKurtosisRunning()) { + logger.info("โ„น๏ธ Kurtosis network is already running."); + + // Check if launchKurtosis option was set via flags + if (options.launchKurtosis === false) { + logger.info("Keeping existing Kurtosis enclave. Exiting..."); + return { success: true, services: await getServicesFromDocker() }; + } + + if (options.launchKurtosis === true) { + logger.info("Proceeding to clean and relaunch the Kurtosis enclave..."); + } else { + // Use promptWithTimeout if launchKurtosis is undefined + const shouldRelaunch = await promptWithTimeout( + "Do you want to clean and relaunch the Kurtosis enclave?", + true, + 10 + ); + + if (!shouldRelaunch) { + logger.info("Keeping existing Kurtosis enclave. Exiting..."); + return { success: true, services: await getServicesFromDocker() }; + } + + logger.info("Proceeding to clean and relaunch the Kurtosis enclave..."); + } + } + + // Start Kurtosis network + printHeader("Starting Kurtosis Network"); + + // Clean up Docker and Kurtosis + logger.info("๐Ÿงน Cleaning up Docker and Kurtosis environments..."); + logger.debug(await $`kurtosis enclave stop datahaven-ethereum`.nothrow().text()); + logger.debug(await $`kurtosis clean`.text()); + logger.debug(await $`kurtosis engine stop`.text()); + logger.debug(await $`docker system prune -f`.nothrow().text()); + + // Pull necessary Docker images + if (process.platform === "darwin") { + logger.debug("Detected macOS, pulling container images with linux/amd64 platform..."); + logger.debug( + await $`docker pull ghcr.io/blockscout/smart-contract-verifier:latest --platform linux/amd64`.text() + ); + } + + // Run Kurtosis + logger.info("๐Ÿš€ Starting Kurtosis enclave..."); + const { stderr, stdout, exitCode } = + await $`kurtosis run github.com/ethpandaops/ethereum-package --args-file configs/minimal.yaml --enclave datahaven-ethereum` + .nothrow() + .quiet(); + + if (exitCode !== 0) { + logger.error(stderr.toString()); + throw Error("โŒ Kurtosis network has failed to start properly."); + } + logger.debug(stdout.toString()); + + // Get service information from Docker + logger.info("๐Ÿ” Detecting Docker container ports..."); + const services = await getServicesFromDocker(); + + printDivider(); + + return { success: true, services }; +}; + +/** + * Checks if a Kurtosis enclave named "datahaven-ethereum" is currently running. + * + * @returns True if the enclave is running, false otherwise + */ +const checkKurtosisRunning = async (): Promise => { + const text = await $`kurtosis enclave ls | grep "datahaven-ethereum" | grep RUNNING`.text(); + return text.length > 0; +}; diff --git a/test/scripts/start-kurtosis.ts b/test/scripts/start-kurtosis.ts deleted file mode 100644 index ea1c3cea..00000000 --- a/test/scripts/start-kurtosis.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { $ } from "bun"; -import invariant from "tiny-invariant"; -import { getServicesFromDocker, logger } from "utils"; -import sendTxn from "./send-txn"; - -async function main() { - const args = process.argv.slice(2); - const isVerified = args.includes("--verified"); - logger.info(`Running with --verified: ${isVerified}`); - - const timeStart = performance.now(); - logger.debug(`Running on ${process.platform}`); - - if (!(await checkKurtosisInstalled())) { - logger.error("Kurtosis CLI is required to be installed: https://docs.kurtosis.com/install"); - throw Error("โŒ Kurtosis CLI application not found."); - } - - if (!(await checkDockerRunning())) { - logger.error("Is Docker Running? Unable to make connection to docker daemon"); - throw Error("โŒ Error connecting to Docker"); - } - - if (!(await checkForgeInstalled())) { - logger.error("Is foundry installed? https://book.getfoundry.sh/getting-started/installation"); - throw Error("โŒ forge binary not found in PATH"); - } - - logger.info("๐Ÿช” Starting Kurtosis network..."); - - if (await checkKurtosisRunning()) { - logger.info("โ„น๏ธ Kurtosis network is already running. Quitting..."); - return; - } - - await $`docker system prune -f`.nothrow(); - await $`kurtosis clean`; - - if (process.platform === "darwin") { - logger.debug("Detected macOS, pulling container images with linux/amd64 platform..."); - await $`docker pull ghcr.io/blockscout/smart-contract-verifier:latest --platform linux/amd64`; - } - - const { stderr, stdout, exitCode } = - await $`kurtosis run github.com/Moonsong-Labs/ethereum-package --args-file configs/minimal.yaml --enclave datahaven-ethereum`.nothrow(); - - if (exitCode !== 0) { - logger.error(stderr.toString()); - throw Error("โŒ Kurtosis network has failed to start properly."); - } - - // Get service information from Docker instead of parsing stdout - logger.info("๐Ÿ” Detecting Docker container ports..."); - const services = await getServicesFromDocker(); - - logger.info("================================================"); - - const privateKey = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; // Anvil test acc1 - const networkRpcUrl = services.find((s) => s.service === "reth-1-rpc")?.url; - invariant(networkRpcUrl, "โŒ Network RPC URL not found"); - await sendTxn(privateKey, networkRpcUrl); - - // Deploy all the contracts - logger.info("๐Ÿ›ณ๏ธ Deploying contracts..."); - const { exitCode: buildExitCode, stderr: buildStderr } = await $`forge build` - .cwd("../contracts") - .nothrow(); - - if (buildExitCode !== 0) { - logger.error(buildStderr.toString()); - throw Error("โŒ Contracts have failed to build properly."); - } - - const { stdout: forgePath } = await $`which forge`.quiet(); - const forgeExecutable = forgePath.toString().trim(); - logger.info(`Using forge at: ${forgeExecutable}`); - - const blockscoutBackendUrl = services.find((s) => s.service === "blockscout-backend")?.url; - invariant(blockscoutBackendUrl, "โŒ Blockscout backend URL not found"); - - let deployCommand = `${forgeExecutable} script script/deploy/DeployLocal.s.sol --rpc-url ${networkRpcUrl} --color never -vv --no-rpc-rate-limit --non-interactive --broadcast`; - - if (isVerified) { - deployCommand += ` --verify --verifier blockscout --verifier-url ${blockscoutBackendUrl}/api/ --delay 0`; - } - - console.log(`Running command: ${deployCommand}`); - - const { exitCode: deployExitCode, stderr: deployStderr } = await $`sh -c ${deployCommand}` - .cwd("../contracts") - .nothrow(); - - if (deployExitCode !== 0) { - logger.error(deployStderr.toString()); - throw Error("โŒ Contracts have failed to deploy properly."); - } - - logger.info("================================================"); - - console.table([ - ...services, - { service: "blockscout", port: "3000", url: "http://127.0.0.1:3000" }, - { - service: "kurtosis-web", - port: "9711", - url: "http://127.0.0.1:9711" - } - ]); - - logger.info("================================================"); - - const timeEnd = performance.now(); - - logger.info( - `๐Ÿ’š Kurtosis network has started successfully in ${((timeEnd - timeStart) / (1000 * 60)).toFixed(1)} minutes` - ); -} - -const checkKurtosisInstalled = async (): Promise => { - const { exitCode } = await $`kurtosis version`.nothrow().quiet(); - return exitCode === 0; -}; - -const checkKurtosisRunning = async (): Promise => { - const text = await $`kurtosis enclave ls | grep RUNNING`.text(); - return text.length > 0; -}; - -const checkDockerRunning = async (): Promise => { - const { exitCode } = await $`docker system info`.nothrow(); - return exitCode === 0; -}; - -const checkForgeInstalled = async (): Promise => { - const { exitCode } = await $`forge --version`.nothrow(); - return exitCode === 0; -}; - -main(); diff --git a/test/utils/docker.ts b/test/utils/docker.ts index 60fa4d97..00b4f7b0 100644 --- a/test/utils/docker.ts +++ b/test/utils/docker.ts @@ -42,7 +42,7 @@ const serviceMappings: ServiceMapping[] = [ } ]; -export async function getServicesFromDocker(): Promise { +export const getServicesFromDocker = async (): Promise => { const docker = new Docker(); const containers = await docker.listContainers(); @@ -103,7 +103,7 @@ export async function getServicesFromDocker(): Promise { } return services; -} +}; export const getPublicPort = async ( containerName: string, diff --git a/test/utils/index.ts b/test/utils/index.ts index 99236803..196aa70b 100644 --- a/test/utils/index.ts +++ b/test/utils/index.ts @@ -1,6 +1,7 @@ export * from "./blockscout"; export * from "./constants"; export * from "./docker"; +export * from "./input"; export * from "./logger"; export * from "./rpc"; export * from "./viem"; diff --git a/test/utils/input.ts b/test/utils/input.ts new file mode 100644 index 00000000..6a9fa348 --- /dev/null +++ b/test/utils/input.ts @@ -0,0 +1,51 @@ +import chalk from "chalk"; +import readline from "node:readline"; +// Helper function to create an interactive prompt with timeout +export const promptWithTimeout = async ( + question: string, + defaultValue: boolean, + timeoutSeconds: number +): Promise => { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + + return new Promise((resolve) => { + const defaultText = defaultValue ? "Y/n" : "y/N"; + + // Create a visually striking prompt + const border = chalk.yellow("=".repeat(question.length + 40)); + console.log("\n"); + console.log(border); + console.log(chalk.yellow("โ–ถ ") + chalk.bold.cyan(question)); + console.log( + chalk.magenta( + `โฑ Will default to ${chalk.bold(defaultValue ? "YES" : "NO")} in ${chalk.bold(timeoutSeconds)} seconds` + ) + ); + console.log(border); + const fullQuestion = chalk.green(`\nโžค Please enter your choice [${chalk.bold(defaultText)}]: `); + + const timer = setTimeout(() => { + console.log( + `\n${chalk.yellow("โฑ")} ${chalk.bold("Timeout reached, using default:")} ${chalk.green(defaultValue ? "YES" : "NO")}\n` + ); + rl.close(); + resolve(defaultValue); + }, timeoutSeconds * 1000); + + rl.question(fullQuestion, (answer) => { + clearTimeout(timer); + rl.close(); + + if (answer.trim() === "") { + resolve(defaultValue); + } else { + const normalizedAnswer = answer.trim().toLowerCase(); + console.log(""); + resolve(normalizedAnswer === "y" || normalizedAnswer === "yes"); + } + }); + }); +}; diff --git a/test/utils/logger.ts b/test/utils/logger.ts index bbfc1075..0b9c3e15 100644 --- a/test/utils/logger.ts +++ b/test/utils/logger.ts @@ -1,9 +1,49 @@ import pino from "pino"; import pinoPretty from "pino-pretty"; +import chalk from "chalk"; const logLevel = process.env.LOG_LEVEL || "info"; const stream = pinoPretty({ colorize: true }); -export const logger = pino({ level: logLevel }, stream); + +// Custom logger type with success method +interface CustomLogger extends pino.Logger { + success(msg: string, ...args: any[]): void; +} + +// Create the base logger with proper configuration +export const logger: CustomLogger = pino( + { + level: logLevel + }, + stream +) as CustomLogger; + +// Add custom success method to the logger +logger.success = function (message: string) { + this.info(`โœ… ${message}`); +}; + +// Simple progress bar function +export const printProgress = (percent: number) => { + const width = 30; + const completed = Math.floor(width * (percent / 100)); + const remaining = width - completed; + + const bar = chalk.green("โ–ˆ".repeat(completed)) + chalk.gray("โ–‘".repeat(remaining)); + + console.log(`\n${chalk.bold("Progress:")} ${bar} ${percent}%\n`); +}; + +// Print a section header +export const printHeader = (title: string) => { + console.log(`\n${chalk.bold.cyan(`โ–ถ ${title}`)}`); + console.log(chalk.gray("โ”€".repeat(title.length + 3))); +}; + +// Print a divider +export const printDivider = () => { + console.log(chalk.gray(`\n${"โ”€".repeat(50)}\n`)); +};