diff --git a/.github/actions/run-tests/action.yaml b/.github/actions/run-tests/action.yaml index 2077550ed..7c7f855e3 100644 --- a/.github/actions/run-tests/action.yaml +++ b/.github/actions/run-tests/action.yaml @@ -22,22 +22,6 @@ runs: - run: pnpm install --frozen-lockfile shell: bash - # Install the CLI. - - run: pnpm install -g @aptos-labs/aptos-cli - shell: bash - - # Run a local testnet in the background. - - run: aptos node run-local-testnet --force-restart --assume-yes --with-indexer-api --log-to-stdout >& ${{ runner.temp }}/local-testnet-logs.txt & - shell: bash - - # Wait for the local testnet to be ready by hitting the readiness endpoint. - # We give it a while because the CLI will have to download some images before - # actually running the local testnet, which can take a while. - - run: pnpm install -g wait-on - shell: bash - - run: wait-on --verbose --interval 1500 --timeout 120000 --httpTimeout 120000 http-get://127.0.0.1:8070 - shell: bash - # Run the TS SDK tests. - uses: nick-fields/retry@7f8f3d9f0f62fe5925341be21c2e8314fd4f7c7c # pin@v2 name: sdk-pnpm-test diff --git a/CHANGELOG.md b/CHANGELOG.md index f61742ebb..103a7a78a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ All notable changes to the Aptos TypeScript SDK will be captured in this file. T - [`Breaking`] Capitalize `TransactionPayloadMultiSig` type - Add support to Array value in digital asset property map - [`Breaking`] Change `maxGasAmount, gasUnitPrice and expireTimestamp` properties in `InputGenerateTransactionOptions` type to `number` type +- Add `@aptos-labs/aptos-cli` npm package as a dev dependency +- Implement a `LocalNode` module to run a local testnet with in the SDK environment +- Use `LocalNode` module to spin up a local testnet pre running SDK tests # 1.2.0 (2023-12-14) diff --git a/README.md b/README.md index 93f9c95a4..52d805ddb 100644 --- a/README.md +++ b/README.md @@ -179,9 +179,10 @@ const pendingTransaction = await aptos.signAndSubmitTransaction({ signer: alice, To run the SDK tests, simply run from the root of this repository: -> Note: make sure aptos local node is up and running. Take a look at the [local development network guide](https://aptos.dev/guides/local-development-network/) for more details. +> Note: for a better experience, make sure there is no aptos local node process up and running (can check if there is a process running on port 8080). ```bash +pnpm i pnpm test ``` diff --git a/jest.config.js b/jest.config.js index a91482de1..7d3ee29f8 100644 --- a/jest.config.js +++ b/jest.config.js @@ -23,4 +23,6 @@ module.exports = { }, // To help avoid exhausting all the available fds. maxWorkers: 4, + globalSetup: "./tests/preTest.js", + globalTeardown: "./tests/postTest.js", }; diff --git a/package.json b/package.json index c5df3924e..64a0c2431 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "tweetnacl": "^1.0.3" }, "devDependencies": { + "@aptos-labs/aptos-cli": "^0.1.2", "@babel/traverse": "^7.23.6", "@graphql-codegen/cli": "^5.0.0", "@graphql-codegen/import-types-preset": "^3.0.0", @@ -74,6 +75,7 @@ "graphql-request": "^6.1.0", "jest": "^29.7.0", "prettier": "^3.1.1", + "tree-kill": "^1.2.2", "ts-jest": "^29.1.1", "ts-loader": "^9.5.1", "ts-node": "^10.9.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8078be661..a4fcc2621 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -28,6 +28,9 @@ dependencies: version: 1.0.3 devDependencies: + '@aptos-labs/aptos-cli': + specifier: ^0.1.2 + version: 0.1.2 '@babel/traverse': specifier: ^7.23.6 version: 7.23.6 @@ -88,6 +91,9 @@ devDependencies: prettier: specifier: ^3.1.1 version: 3.1.1 + tree-kill: + specifier: ^1.2.2 + version: 1.2.2 ts-jest: specifier: ^29.1.1 version: 29.1.1(@babel/core@7.22.5)(esbuild@0.19.9)(jest@29.7.0)(typescript@5.3.3) @@ -122,6 +128,11 @@ packages: '@jridgewell/trace-mapping': 0.3.18 dev: true + /@aptos-labs/aptos-cli@0.1.2: + resolution: {integrity: sha512-MCi+9xPDG/Fx6c6b9ACqyQBYJLHqKJQKB8lay/1sUNJ2mNBB2OU1Lkmd5pmjUG1JbZ5cg2Fmgid5iMKdTnphhA==} + hasBin: true + dev: true + /@aptos-labs/aptos-client@0.1.0: resolution: {integrity: sha512-q3s6pPq8H2buGp+tPuIRInWsYOuhSEwuNJPwd2YnsiID3YSLihn2ug39ktDJAcSOprUcp7Nid8WK7hKqnUmSdA==} engines: {node: '>=15.10.0'} diff --git a/src/cli/index.ts b/src/cli/index.ts new file mode 100644 index 000000000..1ef31d008 --- /dev/null +++ b/src/cli/index.ts @@ -0,0 +1 @@ +export * from "./localNode"; diff --git a/src/cli/localNode.ts b/src/cli/localNode.ts new file mode 100644 index 000000000..57230ca65 --- /dev/null +++ b/src/cli/localNode.ts @@ -0,0 +1,105 @@ +import { ChildProcessWithoutNullStreams, spawn } from "child_process"; +import kill from "tree-kill"; +import { sleep } from "../utils/helpers"; + +export class LocalNode { + readonly MAXIMUM_WAIT_TIME_SEC = 30; + + readonly READINESS_ENDPOINT = "http://127.0.0.1:8070/"; + + process: ChildProcessWithoutNullStreams | null = null; + + /** + * kills all the descendent processes + * of the node process, including the node process itself + */ + stop() { + if (!this.process?.pid) return; + kill(this.process.pid); + } + + /** + * Runs a local testnet and waits for process to be up. + * + * If local node process is already up it returns and does + * not start the process + */ + async run() { + const nodeIsUp = await this.checkIfProcessIsUp(); + if (nodeIsUp) { + return; + } + this.start(); + await this.waitUntilProcessIsUp(); + } + + /** + * Starts the local testnet by running the aptos node run-local-testnet command + */ + start() { + const cliCommand = "npx"; + const cliArgs = ["aptos", "node", "run-local-testnet", "--force-restart", "--assume-yes", "--with-indexer-api"]; + + const childProcess = spawn(cliCommand, cliArgs); + this.process = childProcess; + + childProcess.stderr?.on("data", (data: any) => { + const str = data.toString(); + // Print local node output log + // eslint-disable-next-line no-console + console.log(str); + }); + + childProcess.stdout?.on("data", (data: any) => { + const str = data.toString(); + // Print local node output log + // eslint-disable-next-line no-console + console.log(str); + }); + } + + /** + * Waits for the local testnet process to be up + * + * @returns Promise + */ + async waitUntilProcessIsUp(): Promise { + let operational = await this.checkIfProcessIsUp(); + const start = Date.now() / 1000; + let last = start; + + while (!operational && start + this.MAXIMUM_WAIT_TIME_SEC > last) { + // eslint-disable-next-line no-await-in-loop + await sleep(1000); + // eslint-disable-next-line no-await-in-loop + operational = await this.checkIfProcessIsUp(); + last = Date.now() / 1000; + } + + // If we are here it means something blocks the process to start. + // Might worth checking if another process is running on port 8080 + if (!operational) { + throw new Error("Process failed to start"); + } + + return true; + } + + /** + * Checks if the local testnet is up + * + * @returns Promise + */ + async checkIfProcessIsUp(): Promise { + try { + // Query readiness endpoint + const data = await fetch(this.READINESS_ENDPOINT); + if (data.status === 200) { + return true; + } + return false; + } catch (err: any) { + return false; + } + } +} diff --git a/tests/postTest.js b/tests/postTest.js new file mode 100644 index 000000000..e237b3e8e --- /dev/null +++ b/tests/postTest.js @@ -0,0 +1,11 @@ +module.exports = async function () { + // Check if the current local node process is + // from within the sdk node environment + if (globalThis.__LOCAL_NODE__.process) { + const aptosNode = globalThis.__LOCAL_NODE__; + // Local node runs multiple procceses, to avoid asynchronous operations + // that weren't stopped in our tests, we kill all the descendent processes + // of the node process, including the node process itself + aptosNode.stop(); + } +}; diff --git a/tests/preTest.js b/tests/preTest.js new file mode 100644 index 000000000..e3553caca --- /dev/null +++ b/tests/preTest.js @@ -0,0 +1,7 @@ +const { LocalNode } = require("../src/cli"); + +module.exports = async function setup() { + const localNode = new LocalNode(); + globalThis.__LOCAL_NODE__ = localNode; + await localNode.run(); +};