From a6c5d8d22dfcb739d84405715680ba20f5404681 Mon Sep 17 00:00:00 2001 From: nedsalk Date: Tue, 18 Jun 2024 16:15:52 +0200 Subject: [PATCH 01/13] feat: add browser testing support in `launchTestNode` utility --- .changeset/serious-dogs-wash.md | 2 + packages/account/src/test-utils/launchNode.ts | 3 +- .../setup-test-provider-and-wallets.ts | 22 +++- .../fuel-gauge/src/call-test-contract.test.ts | 47 ++++---- .../src/setup-launch-node-server.test.ts | 68 +++++++++++ .../fuels/src/setup-launch-node-server.ts | 109 ++++++++++++++++++ scripts/tests-ci.sh | 3 +- vitest.browser.config.mts | 8 ++ vitest.global-browser-setup.ts | 24 ++++ 9 files changed, 260 insertions(+), 26 deletions(-) create mode 100644 .changeset/serious-dogs-wash.md create mode 100644 packages/fuels/src/setup-launch-node-server.test.ts create mode 100644 packages/fuels/src/setup-launch-node-server.ts create mode 100644 vitest.global-browser-setup.ts diff --git a/.changeset/serious-dogs-wash.md b/.changeset/serious-dogs-wash.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/serious-dogs-wash.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/packages/account/src/test-utils/launchNode.ts b/packages/account/src/test-utils/launchNode.ts index a2bf58f1dcb..4f89ab09041 100644 --- a/packages/account/src/test-utils/launchNode.ts +++ b/packages/account/src/test-utils/launchNode.ts @@ -3,7 +3,6 @@ import { randomBytes } from '@fuel-ts/crypto'; import type { SnapshotConfigs } from '@fuel-ts/utils'; import { defaultConsensusKey, hexlify, defaultSnapshotConfigs } from '@fuel-ts/utils'; import type { ChildProcessWithoutNullStreams } from 'child_process'; -import { spawn } from 'child_process'; import { randomUUID } from 'crypto'; import { existsSync, mkdirSync, rmSync, writeFileSync } from 'fs'; import os from 'os'; @@ -217,6 +216,8 @@ export const launchNode = async ({ snapshotDirToUse = tempDir; } + const { spawn } = await import('child_process'); + const child = spawn( command, [ diff --git a/packages/account/src/test-utils/setup-test-provider-and-wallets.ts b/packages/account/src/test-utils/setup-test-provider-and-wallets.ts index 2e03d5fa8d0..6ff99aca921 100644 --- a/packages/account/src/test-utils/setup-test-provider-and-wallets.ts +++ b/packages/account/src/test-utils/setup-test-provider-and-wallets.ts @@ -64,7 +64,7 @@ export async function setupTestProviderAndWallets({ } ); - const { cleanup, url } = await launchNode({ + const launchNodeOptions = { loggingEnabled: false, ...nodeOptions, snapshotConfig: mergeDeepRight( @@ -72,7 +72,25 @@ export async function setupTestProviderAndWallets({ walletsConfig.apply(nodeOptions?.snapshotConfig) ), port: '0', - }); + }; + + let cleanup: () => void; + let url: string; + if (process.env.LAUNCH_NODE_SERVER_PORT) { + const serverUrl = `http://localhost:${process.env.LAUNCH_NODE_SERVER_PORT}`; + url = await ( + await fetch(serverUrl, { method: 'POST', body: JSON.stringify(launchNodeOptions) }) + ).text(); + + cleanup = () => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + fetch(`${serverUrl}/cleanup/${url}`); + }; + } else { + const settings = await launchNode(launchNodeOptions); + url = settings.url; + cleanup = settings.cleanup; + } let provider: Provider; diff --git a/packages/fuel-gauge/src/call-test-contract.test.ts b/packages/fuel-gauge/src/call-test-contract.test.ts index 53eb58ab080..e47cdfe0540 100644 --- a/packages/fuel-gauge/src/call-test-contract.test.ts +++ b/packages/fuel-gauge/src/call-test-contract.test.ts @@ -1,27 +1,30 @@ import { ASSET_A } from '@fuel-ts/utils/test-utils'; import type { Contract } from 'fuels'; import { BN, bn, toHex } from 'fuels'; +import { launchTestNode } from 'fuels/test-utils'; -import type { CallTestContractAbi } from '../test/typegen/contracts'; import { CallTestContractAbi__factory } from '../test/typegen/contracts'; -import binHexlified from '../test/typegen/contracts/CallTestContractAbi.hex'; - -import { createSetupConfig } from './utils'; - -const setupContract = createSetupConfig({ - contractBytecode: binHexlified, - abi: CallTestContractAbi__factory.abi, - cache: true, -}); +import bytecode from '../test/typegen/contracts/CallTestContractAbi.hex'; + +const setupContract = async () => { + const { + contracts: [contract], + cleanup, + } = await launchTestNode({ + contractsConfigs: [{ deployer: CallTestContractAbi__factory, bytecode }], + }); + return Object.assign(contract, { [Symbol.dispose]: cleanup }); +}; const U64_MAX = bn(2).pow(64).sub(1); /** * @group node + * @group browser */ describe('CallTestContract', () => { it.each([0, 1337, U64_MAX.sub(1)])('can call a contract with u64 (%p)', async (num) => { - const contract = await setupContract(); + using contract = await setupContract(); const { value } = await contract.functions.foo(num).call(); expect(value.toHex()).toEqual(bn(num).add(1).toHex()); }); @@ -34,14 +37,14 @@ describe('CallTestContract', () => { [{ a: false, b: U64_MAX.sub(1) }], [{ a: true, b: U64_MAX.sub(1) }], ])('can call a contract with structs (%p)', async (struct) => { - const contract = await setupContract(); + using contract = await setupContract(); const { value } = await contract.functions.boo(struct).call(); expect(value.a).toEqual(!struct.a); expect(value.b.toHex()).toEqual(bn(struct.b).add(1).toHex()); }); it('can call a function with empty arguments', async () => { - const contract = await setupContract(); + using contract = await setupContract(); const { value: empty } = await contract.functions.empty().call(); expect(empty.toHex()).toEqual(toHex(63)); @@ -59,7 +62,7 @@ describe('CallTestContract', () => { }); it('function with empty return should resolve undefined', async () => { - const contract = await setupContract(); + using contract = await setupContract(); // Call method with no params but with no result and no value on config const { value } = await contract.functions.return_void().call(); @@ -136,9 +139,9 @@ describe('CallTestContract', () => { async (method, { values, expected }) => { // Type cast to Contract because of the dynamic nature of the test // But the function names are type-constrained to correct Contract's type - const contract = (await setupContract()) as Contract; + using contract = await setupContract(); - const { value } = await contract.functions[method](...values).call(); + const { value } = await (contract as Contract).functions[method](...values).call(); if (BN.isBN(value)) { expect(toHex(value)).toBe(toHex(expected)); @@ -149,7 +152,7 @@ describe('CallTestContract', () => { ); it('Forward amount value on contract call', async () => { - const contract = await setupContract(); + using contract = await setupContract(); const baseAssetId = contract.provider.getBaseAssetId(); const { value } = await contract.functions .return_context_amount() @@ -161,7 +164,7 @@ describe('CallTestContract', () => { }); it('Forward asset_id on contract call', async () => { - const contract = await setupContract(); + using contract = await setupContract(); const assetId = ASSET_A; const { value } = await contract.functions @@ -174,7 +177,7 @@ describe('CallTestContract', () => { }); it('Forward asset_id on contract simulate call', async () => { - const contract = await setupContract(); + using contract = await setupContract(); const assetId = ASSET_A; const { value } = await contract.functions @@ -187,7 +190,7 @@ describe('CallTestContract', () => { }); it('can make multiple calls', async () => { - const contract = await setupContract(); + using contract = await setupContract(); const num = 1337; const numC = 10; @@ -222,14 +225,14 @@ describe('CallTestContract', () => { }); it('Calling a simple contract function does only one dry run', async () => { - const contract = await setupContract(); + using contract = await setupContract(); const dryRunSpy = vi.spyOn(contract.provider.operations, 'dryRun'); await contract.functions.no_params().call(); expect(dryRunSpy).toHaveBeenCalledOnce(); }); it('Simulating a simple contract function does two dry runs', async () => { - const contract = await setupContract(); + using contract = await setupContract(); const dryRunSpy = vi.spyOn(contract.provider.operations, 'dryRun'); await contract.functions.no_params().simulate(); diff --git a/packages/fuels/src/setup-launch-node-server.test.ts b/packages/fuels/src/setup-launch-node-server.test.ts new file mode 100644 index 00000000000..9e612cc92b1 --- /dev/null +++ b/packages/fuels/src/setup-launch-node-server.test.ts @@ -0,0 +1,68 @@ +import { Provider } from '@fuel-ts/account'; +import { waitUntilUnreachable } from '@fuel-ts/utils/test-utils'; +import { spawn } from 'node:child_process'; + +function startServer(): Promise<{ serverUrl: string; killServer: () => void } & Disposable> { + return new Promise((resolve, reject) => { + const cp = spawn('pnpm tsx packages/fuels/src/setup-launch-node-server.ts 0', { + detached: true, + shell: 'sh', + }); + + const killServer = () => { + // https://github.com/nodejs/node/issues/2098#issuecomment-169549789 + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + process.kill(-cp.pid!); + }; + + cp.stdout?.on('data', (chunk) => { + // first message is server url + const message = chunk.toString(); + const serverUrl = message.startsWith('http://') ? message : ''; + + // teardown + resolve({ + serverUrl, + killServer, + [Symbol.dispose]: killServer, + }); + }); + + cp.on('error', (err) => { + reject(err); + }); + }); +} + +describe('setup-launch-node-server', () => { + test('returns a valid fuel-core node url on request', async () => { + using launched = await startServer(); + + const url = await (await fetch(launched.serverUrl)).text(); + // fetches node-related data + // would fail if fuel-core node is not running on url + await Provider.create(url); + }); + + test('the /cleanup endpoint kills the node', async () => { + using launched = await startServer(); + const url = await (await fetch(launched.serverUrl)).text(); + + await fetch(`${launched.serverUrl}/cleanup/${url}`); + + // if the node remained live then the test would time out + await waitUntilUnreachable(url); + }); + + test('kills all nodes when the server is shut down', async () => { + const { serverUrl, killServer } = await startServer(); + const url1 = await (await fetch(serverUrl)).text(); + const url2 = await (await fetch(serverUrl)).text(); + + killServer(); + + // if the nodes remained live then the test would time out + await waitUntilUnreachable(url1); + await waitUntilUnreachable(url2); + }); +}); diff --git a/packages/fuels/src/setup-launch-node-server.ts b/packages/fuels/src/setup-launch-node-server.ts new file mode 100644 index 00000000000..16a9724b6e6 --- /dev/null +++ b/packages/fuels/src/setup-launch-node-server.ts @@ -0,0 +1,109 @@ +/* eslint-disable no-console */ +import type { LaunchNodeOptions, LaunchNodeResult } from '@fuel-ts/account/test-utils'; +import { launchNode } from '@fuel-ts/account/test-utils'; +import http from 'http'; +import type { AddressInfo } from 'net'; + +process.setMaxListeners(Infinity); + +async function parseBody(req: http.IncomingMessage) { + return new Promise((resolve, reject) => { + const body: Buffer[] = []; + req.on('data', (chunk) => { + body.push(chunk); + }); + req.on('end', () => { + resolve(JSON.parse(body.length === 0 ? '{}' : Buffer.concat(body).toString())); + }); + req.on('error', reject); + }); +} + +const cleanupFns: Map['cleanup']> = new Map(); + +function cleanupAllNodes() { + cleanupFns.forEach((fn) => fn()); +} + +const server = http.createServer(async (req, res) => { + res.setHeader('Access-Control-Allow-Origin', '*'); + + if (req.url === '/') { + const body = (await parseBody(req)) as LaunchNodeOptions; + + const node = await launchNode({ + port: '0', + ...body, + fuelCorePath: 'fuels-core', + }); + cleanupFns.set(node.url, () => { + node.cleanup(); + cleanupFns.delete(node.url); + }); + res.write(node.url); + res.end(); + return; + } + + if (req.url?.startsWith('/cleanup')) { + const nodeUrl = req.url?.match(/\/cleanup\/(.+)/)?.[1]; + if (nodeUrl) { + const cleanupFn = cleanupFns.get(nodeUrl); + cleanupFn?.(); + res.end(); + } + } +}); + +const port = process.argv[2] ? parseInt(process.argv[2], 10) : 49342; + +server.listen(port); + +server.on('listening', () => { + const usedPort = (server.address() as AddressInfo).port; + const serverUrl = `http://localhost:${usedPort}`; + console.log(serverUrl); + console.log(`Server is listening on: ${serverUrl}`); + console.log("To launch a new fuel-core node and get its url, make a POST request to '/'."); + console.log( + "To kill the node, make a POST request to '/cleanup/' where is the url of the node you want to kill." + ); + console.log('All nodes will be killed when the server is killed.'); +}); + +server.on('close', () => { + console.log('close'); + cleanupAllNodes(); +}); + +process.on('exit', () => { + console.log('exit'); + cleanupAllNodes(); +}); +process.on('SIGINT', () => { + console.log('sigint'); + cleanupAllNodes(); +}); +process.on('SIGUSR1', () => { + console.log('SIGUSR1'); + cleanupAllNodes(); +}); +process.on('SIGUSR2', () => { + console.log('SIGUSR2'); + cleanupAllNodes(); +}); +process.on('uncaughtException', (e) => { + console.log('uncaughtException'); + console.log(e); + cleanupAllNodes(); +}); +process.on('unhandledRejection', (reason) => { + console.log('unhandledRejection'); + console.log(reason); + + cleanupAllNodes(); +}); +process.on('beforeExit', () => { + console.log('beforeExit'); + cleanupAllNodes(); +}); diff --git a/scripts/tests-ci.sh b/scripts/tests-ci.sh index 1ecfac1a748..930fa37cf58 100755 --- a/scripts/tests-ci.sh +++ b/scripts/tests-ci.sh @@ -4,11 +4,12 @@ pkill fuel-core pnpm node:clean -pnpm node:run > /dev/null 2>&1 & +pnpm node:run >/dev/null 2>&1 & echo "Started Fuel-Core node in background." if [[ "$*" == *"--browser"* ]]; then + pnpm pretest pnpm test:browser TEST_RESULT=$? elif [[ "$*" == *"--node"* ]]; then diff --git a/vitest.browser.config.mts b/vitest.browser.config.mts index 8c7b3b0132f..97126db6ba6 100644 --- a/vitest.browser.config.mts +++ b/vitest.browser.config.mts @@ -20,6 +20,9 @@ const config: UserConfig = { "timers/promises", "util", "stream", + "path", + "fs", + "os", ], overrides: { fs: "memfs", @@ -31,7 +34,12 @@ const config: UserConfig = { include: ["events", "timers/promises"], }, test: { + env: { + LAUNCH_NODE_SERVER_PORT: "49342", + }, + globalSetup: ["./vitest.global-browser-setup.ts"], coverage: { + enabled: true, reportsDirectory: "coverage/environments/browser", }, browser: { diff --git a/vitest.global-browser-setup.ts b/vitest.global-browser-setup.ts new file mode 100644 index 00000000000..37c76751bc3 --- /dev/null +++ b/vitest.global-browser-setup.ts @@ -0,0 +1,24 @@ +import { spawn } from 'node:child_process'; + +export default async function setup() { + return new Promise((resolve, reject) => { + const cp = spawn('pnpm tsx packages/fuels/src/setup-launch-node-server.ts', { + detached: true, + shell: 'sh', + }); + + cp.stdout?.on('data', () => { + // return teardown function to be called when tests finish + // it will kill the server + resolve(() => { + // https://github.com/nodejs/node/issues/2098#issuecomment-169549789 + process.kill(-cp.pid!); + }); + }); + + cp.on('error', (err) => { + console.log('failed to start launchNode server', err); + reject(err); + }); + }); +} From f3724b6cc27d4d18ad795ca13e27f38bc96d75be Mon Sep 17 00:00:00 2001 From: nedsalk Date: Tue, 18 Jun 2024 16:18:08 +0200 Subject: [PATCH 02/13] chore: changeset update --- .changeset/serious-dogs-wash.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.changeset/serious-dogs-wash.md b/.changeset/serious-dogs-wash.md index a845151cc84..98709abc3cf 100644 --- a/.changeset/serious-dogs-wash.md +++ b/.changeset/serious-dogs-wash.md @@ -1,2 +1,5 @@ --- +"@fuel-ts/account": patch --- + +chore: add browser testing infrastructure From f642a315077b7f2c85c54d3c6845c3b84a5911a1 Mon Sep 17 00:00:00 2001 From: nedsalk Date: Tue, 18 Jun 2024 16:30:19 +0200 Subject: [PATCH 03/13] add missing group to test --- packages/fuels/src/setup-launch-node-server.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/fuels/src/setup-launch-node-server.test.ts b/packages/fuels/src/setup-launch-node-server.test.ts index 9e612cc92b1..9aa50777495 100644 --- a/packages/fuels/src/setup-launch-node-server.test.ts +++ b/packages/fuels/src/setup-launch-node-server.test.ts @@ -34,6 +34,9 @@ function startServer(): Promise<{ serverUrl: string; killServer: () => void } & }); } +/** + * @group node + */ describe('setup-launch-node-server', () => { test('returns a valid fuel-core node url on request', async () => { using launched = await startServer(); From 99a8aed5f5f09178dad3193e163879557f22769a Mon Sep 17 00:00:00 2001 From: nedsalk Date: Tue, 18 Jun 2024 16:33:50 +0200 Subject: [PATCH 04/13] remove unnecessary addition --- vitest.browser.config.mts | 1 - 1 file changed, 1 deletion(-) diff --git a/vitest.browser.config.mts b/vitest.browser.config.mts index 97126db6ba6..48852149d53 100644 --- a/vitest.browser.config.mts +++ b/vitest.browser.config.mts @@ -39,7 +39,6 @@ const config: UserConfig = { }, globalSetup: ["./vitest.global-browser-setup.ts"], coverage: { - enabled: true, reportsDirectory: "coverage/environments/browser", }, browser: { From 0abbed7eab82ca7d260614c35479ae2e9e47dd80 Mon Sep 17 00:00:00 2001 From: nedsalk Date: Tue, 18 Jun 2024 16:38:24 +0200 Subject: [PATCH 05/13] test: add test for specific port --- .../fuels/src/setup-launch-node-server.test.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/fuels/src/setup-launch-node-server.test.ts b/packages/fuels/src/setup-launch-node-server.test.ts index 9aa50777495..898ccd776ed 100644 --- a/packages/fuels/src/setup-launch-node-server.test.ts +++ b/packages/fuels/src/setup-launch-node-server.test.ts @@ -2,9 +2,11 @@ import { Provider } from '@fuel-ts/account'; import { waitUntilUnreachable } from '@fuel-ts/utils/test-utils'; import { spawn } from 'node:child_process'; -function startServer(): Promise<{ serverUrl: string; killServer: () => void } & Disposable> { +function startServer( + port: number = 0 +): Promise<{ serverUrl: string; killServer: () => void } & Disposable> { return new Promise((resolve, reject) => { - const cp = spawn('pnpm tsx packages/fuels/src/setup-launch-node-server.ts 0', { + const cp = spawn(`pnpm tsx packages/fuels/src/setup-launch-node-server.ts ${port}`, { detached: true, shell: 'sh', }); @@ -17,9 +19,8 @@ function startServer(): Promise<{ serverUrl: string; killServer: () => void } & cp.stdout?.on('data', (chunk) => { // first message is server url - const message = chunk.toString(); - const serverUrl = message.startsWith('http://') ? message : ''; - + const message: string[] = chunk.toString().split('\n'); + const serverUrl = message[0].startsWith('http://') ? message[0] : ''; // teardown resolve({ serverUrl, @@ -38,6 +39,11 @@ function startServer(): Promise<{ serverUrl: string; killServer: () => void } & * @group node */ describe('setup-launch-node-server', () => { + test('can start server on specific port', async () => { + using launched = await startServer(9876); + expect(launched.serverUrl).toEqual('http://localhost:9876'); + }); + test('returns a valid fuel-core node url on request', async () => { using launched = await startServer(); From 562cbf5b8b2433bde3511fa615c2a33c741ed706 Mon Sep 17 00:00:00 2001 From: nedsalk Date: Tue, 18 Jun 2024 17:23:49 +0200 Subject: [PATCH 06/13] fix: linting --- vitest.global-browser-setup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vitest.global-browser-setup.ts b/vitest.global-browser-setup.ts index 37c76751bc3..1a2cd0ca467 100644 --- a/vitest.global-browser-setup.ts +++ b/vitest.global-browser-setup.ts @@ -12,12 +12,12 @@ export default async function setup() { // it will kill the server resolve(() => { // https://github.com/nodejs/node/issues/2098#issuecomment-169549789 + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion process.kill(-cp.pid!); }); }); cp.on('error', (err) => { - console.log('failed to start launchNode server', err); reject(err); }); }); From 7045dcf0598d20922c1fad917a2ba9d52a4f89c4 Mon Sep 17 00:00:00 2001 From: nedsalk Date: Tue, 18 Jun 2024 17:50:34 +0200 Subject: [PATCH 07/13] Add longer timeouts --- .../src/setup-launch-node-server.test.ts | 70 ++++++++++++------- 1 file changed, 43 insertions(+), 27 deletions(-) diff --git a/packages/fuels/src/setup-launch-node-server.test.ts b/packages/fuels/src/setup-launch-node-server.test.ts index 898ccd776ed..01961f18d3e 100644 --- a/packages/fuels/src/setup-launch-node-server.test.ts +++ b/packages/fuels/src/setup-launch-node-server.test.ts @@ -39,39 +39,55 @@ function startServer( * @group node */ describe('setup-launch-node-server', () => { - test('can start server on specific port', async () => { - using launched = await startServer(9876); - expect(launched.serverUrl).toEqual('http://localhost:9876'); - }); + test( + 'can start server on specific port', + async () => { + using launched = await startServer(9876); + expect(launched.serverUrl).toEqual('http://localhost:9876'); + }, + { timeout: 10000 } + ); - test('returns a valid fuel-core node url on request', async () => { - using launched = await startServer(); + test( + 'returns a valid fuel-core node url on request', + async () => { + using launched = await startServer(); - const url = await (await fetch(launched.serverUrl)).text(); - // fetches node-related data - // would fail if fuel-core node is not running on url - await Provider.create(url); - }); + const url = await (await fetch(launched.serverUrl)).text(); + // fetches node-related data + // would fail if fuel-core node is not running on url + await Provider.create(url); + }, + { timeout: 10000 } + ); - test('the /cleanup endpoint kills the node', async () => { - using launched = await startServer(); - const url = await (await fetch(launched.serverUrl)).text(); + test( + 'the /cleanup endpoint kills the node', + async () => { + using launched = await startServer(); + const url = await (await fetch(launched.serverUrl)).text(); - await fetch(`${launched.serverUrl}/cleanup/${url}`); + await fetch(`${launched.serverUrl}/cleanup/${url}`); - // if the node remained live then the test would time out - await waitUntilUnreachable(url); - }); + // if the node remained live then the test would time out + await waitUntilUnreachable(url); + }, + { timeout: 10000 } + ); - test('kills all nodes when the server is shut down', async () => { - const { serverUrl, killServer } = await startServer(); - const url1 = await (await fetch(serverUrl)).text(); - const url2 = await (await fetch(serverUrl)).text(); + test( + 'kills all nodes when the server is shut down', + async () => { + const { serverUrl, killServer } = await startServer(); + const url1 = await (await fetch(serverUrl)).text(); + const url2 = await (await fetch(serverUrl)).text(); - killServer(); + killServer(); - // if the nodes remained live then the test would time out - await waitUntilUnreachable(url1); - await waitUntilUnreachable(url2); - }); + // if the nodes remained live then the test would time out + await waitUntilUnreachable(url1); + await waitUntilUnreachable(url2); + }, + { timeout: 10000 } + ); }); From 7a1ebbfe1a2944f5aebf3de9e75bd6003ba8b497 Mon Sep 17 00:00:00 2001 From: nedsalk Date: Thu, 20 Jun 2024 09:30:34 +0200 Subject: [PATCH 08/13] apply change requests --- .../src/setup-launch-node-server.test.ts | 23 +++++++---- .../fuels/src/setup-launch-node-server.ts | 39 ++++++++++++------- vitest.global-browser-setup.ts | 25 ++++++++---- 3 files changed, 58 insertions(+), 29 deletions(-) diff --git a/packages/fuels/src/setup-launch-node-server.test.ts b/packages/fuels/src/setup-launch-node-server.test.ts index 01961f18d3e..1304e4718b8 100644 --- a/packages/fuels/src/setup-launch-node-server.test.ts +++ b/packages/fuels/src/setup-launch-node-server.test.ts @@ -2,9 +2,12 @@ import { Provider } from '@fuel-ts/account'; import { waitUntilUnreachable } from '@fuel-ts/utils/test-utils'; import { spawn } from 'node:child_process'; -function startServer( - port: number = 0 -): Promise<{ serverUrl: string; killServer: () => void } & Disposable> { +interface ServerInfo extends Disposable { + serverUrl: string; + killServer: () => void; +} + +function startServer(port: number = 0): Promise { return new Promise((resolve, reject) => { const cp = spawn(`pnpm tsx packages/fuels/src/setup-launch-node-server.ts ${port}`, { detached: true, @@ -18,20 +21,26 @@ function startServer( }; cp.stdout?.on('data', (chunk) => { - // first message is server url + // first message is server url and we resolve immediately because that's what we care about const message: string[] = chunk.toString().split('\n'); - const serverUrl = message[0].startsWith('http://') ? message[0] : ''; - // teardown + resolve({ - serverUrl, + serverUrl: message[0], killServer, [Symbol.dispose]: killServer, }); }); cp.on('error', (err) => { + killServer(); reject(err); }); + + cp.on('exit', (code) => { + if (code !== 0) { + reject(new Error(`Server process exited with code ${code}`)); + } + }); }); } diff --git a/packages/fuels/src/setup-launch-node-server.ts b/packages/fuels/src/setup-launch-node-server.ts index 16a9724b6e6..884c3d36111 100644 --- a/packages/fuels/src/setup-launch-node-server.ts +++ b/packages/fuels/src/setup-launch-node-server.ts @@ -6,14 +6,18 @@ import type { AddressInfo } from 'net'; process.setMaxListeners(Infinity); -async function parseBody(req: http.IncomingMessage) { - return new Promise((resolve, reject) => { +async function parseBody(req: http.IncomingMessage): Promise { + return new Promise((resolve, reject) => { const body: Buffer[] = []; req.on('data', (chunk) => { body.push(chunk); }); req.on('end', () => { - resolve(JSON.parse(body.length === 0 ? '{}' : Buffer.concat(body).toString())); + try { + resolve(JSON.parse(body.length === 0 ? '{}' : Buffer.concat(body).toString())); + } catch (err) { + reject(err); + } }); req.on('error', reject); }); @@ -29,19 +33,24 @@ const server = http.createServer(async (req, res) => { res.setHeader('Access-Control-Allow-Origin', '*'); if (req.url === '/') { - const body = (await parseBody(req)) as LaunchNodeOptions; + try { + const body = await parseBody(req); - const node = await launchNode({ - port: '0', - ...body, - fuelCorePath: 'fuels-core', - }); - cleanupFns.set(node.url, () => { - node.cleanup(); - cleanupFns.delete(node.url); - }); - res.write(node.url); - res.end(); + const node = await launchNode({ + port: '0', + ...body, + fuelCorePath: 'fuels-core', + }); + cleanupFns.set(node.url, () => { + node.cleanup(); + cleanupFns.delete(node.url); + }); + res.end(node.url); + } catch (err) { + console.error(err); + res.writeHead(500, { 'Content-Type': 'text/plain' }); + res.end(JSON.stringify(err)); + } return; } diff --git a/vitest.global-browser-setup.ts b/vitest.global-browser-setup.ts index 1a2cd0ca467..f26e4721d25 100644 --- a/vitest.global-browser-setup.ts +++ b/vitest.global-browser-setup.ts @@ -7,18 +7,29 @@ export default async function setup() { shell: 'sh', }); - cp.stdout?.on('data', () => { - // return teardown function to be called when tests finish - // it will kill the server - resolve(() => { + const killServer = () => { + if (cp.pid) { // https://github.com/nodejs/node/issues/2098#issuecomment-169549789 - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - process.kill(-cp.pid!); - }); + process.kill(-cp.pid); + } + }; + + cp.stdout?.on('data', () => { + // Return teardown function to be called when tests finish + // It will kill the server + resolve(killServer); }); cp.on('error', (err) => { + // Ensure server is killed if there's an error + killServer(); reject(err); }); + + cp.on('exit', (code) => { + if (code !== 0) { + reject(new Error(`Server process exited with code ${code}`)); + } + }); }); } From 624f24b2e9f3065057a33b478fc9aa333b83c5de Mon Sep 17 00:00:00 2001 From: nedsalk Date: Fri, 21 Jun 2024 15:29:23 +0200 Subject: [PATCH 09/13] add /close-server endpoint --- .../src/setup-launch-node-server.test.ts | 52 +++++++++++---- .../fuels/src/setup-launch-node-server.ts | 66 +++++++++++++------ vitest.global-browser-setup.ts | 47 +++++++++---- 3 files changed, 120 insertions(+), 45 deletions(-) diff --git a/packages/fuels/src/setup-launch-node-server.test.ts b/packages/fuels/src/setup-launch-node-server.test.ts index 1304e4718b8..410b925150a 100644 --- a/packages/fuels/src/setup-launch-node-server.test.ts +++ b/packages/fuels/src/setup-launch-node-server.test.ts @@ -4,7 +4,7 @@ import { spawn } from 'node:child_process'; interface ServerInfo extends Disposable { serverUrl: string; - killServer: () => void; + closeServer: () => Promise; } function startServer(port: number = 0): Promise { @@ -14,25 +14,37 @@ function startServer(port: number = 0): Promise { shell: 'sh', }); - const killServer = () => { - // https://github.com/nodejs/node/issues/2098#issuecomment-169549789 - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - process.kill(-cp.pid!); + const server = { + killed: false, + url: undefined as string | undefined, }; + const closeServer = async () => { + if (server.killed) { + return; + } + server.killed = true; + await fetch(`${server.url}/close-server`); + }; + + cp.stderr?.on('data', (chunk) => { + console.log(chunk.toString()); + }); + cp.stdout?.on('data', (chunk) => { // first message is server url and we resolve immediately because that's what we care about const message: string[] = chunk.toString().split('\n'); - + const serverUrl = message[0]; + server.url ??= serverUrl; resolve({ - serverUrl: message[0], - killServer, - [Symbol.dispose]: killServer, + serverUrl, + closeServer, + [Symbol.dispose]: closeServer, }); }); - cp.on('error', (err) => { - killServer(); + cp.on('error', async (err) => { + await closeServer(); reject(err); }); @@ -41,6 +53,13 @@ function startServer(port: number = 0): Promise { reject(new Error(`Server process exited with code ${code}`)); } }); + + process.on('SIGINT', closeServer); + process.on('SIGUSR1', closeServer); + process.on('SIGUSR2', closeServer); + process.on('uncaughtException', closeServer); + process.on('unhandledRejection', closeServer); + process.on('beforeExit', closeServer); }); } @@ -57,6 +76,13 @@ describe('setup-launch-node-server', () => { { timeout: 10000 } ); + test('the /close-server endpoint closes the server', async () => { + const { serverUrl } = await startServer(); + await fetch(`${serverUrl}/close-server`); + + await waitUntilUnreachable(serverUrl); + }); + test( 'returns a valid fuel-core node url on request', async () => { @@ -87,11 +113,11 @@ describe('setup-launch-node-server', () => { test( 'kills all nodes when the server is shut down', async () => { - const { serverUrl, killServer } = await startServer(); + const { serverUrl, closeServer: killServer } = await startServer(); const url1 = await (await fetch(serverUrl)).text(); const url2 = await (await fetch(serverUrl)).text(); - killServer(); + await killServer(); // if the nodes remained live then the test would time out await waitUntilUnreachable(url1); diff --git a/packages/fuels/src/setup-launch-node-server.ts b/packages/fuels/src/setup-launch-node-server.ts index 884c3d36111..0a274350bc7 100644 --- a/packages/fuels/src/setup-launch-node-server.ts +++ b/packages/fuels/src/setup-launch-node-server.ts @@ -1,6 +1,8 @@ +/* eslint-disable @typescript-eslint/no-floating-promises */ /* eslint-disable no-console */ -import type { LaunchNodeOptions, LaunchNodeResult } from '@fuel-ts/account/test-utils'; +import type { LaunchNodeOptions } from '@fuel-ts/account/test-utils'; import { launchNode } from '@fuel-ts/account/test-utils'; +import { waitUntilUnreachable } from '@fuel-ts/utils/test-utils'; import http from 'http'; import type { AddressInfo } from 'net'; @@ -23,11 +25,7 @@ async function parseBody(req: http.IncomingMessage): Promise }); } -const cleanupFns: Map['cleanup']> = new Map(); - -function cleanupAllNodes() { - cleanupFns.forEach((fn) => fn()); -} +const cleanupFns: Map Promise> = new Map(); const server = http.createServer(async (req, res) => { res.setHeader('Access-Control-Allow-Origin', '*'); @@ -38,13 +36,18 @@ const server = http.createServer(async (req, res) => { const node = await launchNode({ port: '0', + loggingEnabled: false, + debugEnabled: false, ...body, fuelCorePath: 'fuels-core', }); - cleanupFns.set(node.url, () => { + + cleanupFns.set(node.url, async () => { node.cleanup(); + await waitUntilUnreachable(node.url); cleanupFns.delete(node.url); }); + res.end(node.url); } catch (err) { console.error(err); @@ -58,12 +61,37 @@ const server = http.createServer(async (req, res) => { const nodeUrl = req.url?.match(/\/cleanup\/(.+)/)?.[1]; if (nodeUrl) { const cleanupFn = cleanupFns.get(nodeUrl); - cleanupFn?.(); + await cleanupFn?.(); res.end(); } } }); +function closeServer() { + return new Promise((resolve) => { + if (!server.listening) { + resolve(); + return; + } + + server.close(async () => { + const cleanupCalls: Promise[] = []; + cleanupFns.forEach((fn) => cleanupCalls.push(fn())); + await Promise.all(cleanupCalls); + process.exit(); + }); + + resolve(); + }); +} + +server.on('request', async (req, res) => { + if (req.url === '/close-server') { + await closeServer(); + res.end(); + } +}); + const port = process.argv[2] ? parseInt(process.argv[2], 10) : 49342; server.listen(port); @@ -77,42 +105,38 @@ server.on('listening', () => { console.log( "To kill the node, make a POST request to '/cleanup/' where is the url of the node you want to kill." ); - console.log('All nodes will be killed when the server is killed.'); -}); - -server.on('close', () => { - console.log('close'); - cleanupAllNodes(); + console.log('All nodes will be killed when the server is closed.'); + console.log('You can close the server by sending a request to /close-server.'); }); process.on('exit', () => { console.log('exit'); - cleanupAllNodes(); + closeServer(); }); process.on('SIGINT', () => { console.log('sigint'); - cleanupAllNodes(); + closeServer(); }); process.on('SIGUSR1', () => { console.log('SIGUSR1'); - cleanupAllNodes(); + closeServer(); }); process.on('SIGUSR2', () => { console.log('SIGUSR2'); - cleanupAllNodes(); + closeServer(); }); process.on('uncaughtException', (e) => { console.log('uncaughtException'); console.log(e); - cleanupAllNodes(); + closeServer(); }); process.on('unhandledRejection', (reason) => { console.log('unhandledRejection'); console.log(reason); - cleanupAllNodes(); + closeServer(); }); process.on('beforeExit', () => { console.log('beforeExit'); - cleanupAllNodes(); + closeServer(); }); diff --git a/vitest.global-browser-setup.ts b/vitest.global-browser-setup.ts index f26e4721d25..8d9bdc4dfa1 100644 --- a/vitest.global-browser-setup.ts +++ b/vitest.global-browser-setup.ts @@ -1,35 +1,60 @@ +/* eslint-disable @typescript-eslint/no-floating-promises */ import { spawn } from 'node:child_process'; -export default async function setup() { +export default function setup() { return new Promise((resolve, reject) => { + const server = { + closed: false, + }; + const teardown = async () => { + if (server.closed) { + return; + } + server.closed = true; + const serverUrl = `http://localhost:49342`; + try { + await fetch(`${serverUrl}/close-server`); + } catch (e) { + console.log('closing of server failed', e); + } + process.exit(); + }; + const cp = spawn('pnpm tsx packages/fuels/src/setup-launch-node-server.ts', { detached: true, shell: 'sh', }); - const killServer = () => { - if (cp.pid) { - // https://github.com/nodejs/node/issues/2098#issuecomment-169549789 - process.kill(-cp.pid); - } - }; + cp.stderr?.on('data', (chunk) => { + console.log(chunk.toString()); + }); - cp.stdout?.on('data', () => { + cp.stdout?.on('data', (data) => { + console.log(data.toString()); // Return teardown function to be called when tests finish // It will kill the server - resolve(killServer); + resolve(teardown); }); cp.on('error', (err) => { + console.log(err); // Ensure server is killed if there's an error - killServer(); + teardown(); reject(err); }); - cp.on('exit', (code) => { + cp.on('exit', (code, signal) => { + console.log('error code', code, signal); if (code !== 0) { reject(new Error(`Server process exited with code ${code}`)); } }); + + process.on('SIGINT', teardown); + process.on('SIGUSR1', teardown); + process.on('SIGUSR2', teardown); + process.on('uncaughtException', teardown); + process.on('unhandledRejection', teardown); + process.on('beforeExit', teardown); }); } From b5132b4e131ffa5ffde2fbf63f9c65fd4d5a8147 Mon Sep 17 00:00:00 2001 From: nedsalk Date: Fri, 21 Jun 2024 15:37:59 +0200 Subject: [PATCH 10/13] fix: linting --- packages/fuels/src/setup-launch-node-server.test.ts | 1 + vitest.global-browser-setup.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/fuels/src/setup-launch-node-server.test.ts b/packages/fuels/src/setup-launch-node-server.test.ts index 410b925150a..c045e370dff 100644 --- a/packages/fuels/src/setup-launch-node-server.test.ts +++ b/packages/fuels/src/setup-launch-node-server.test.ts @@ -28,6 +28,7 @@ function startServer(port: number = 0): Promise { }; cp.stderr?.on('data', (chunk) => { + // eslint-disable-next-line no-console console.log(chunk.toString()); }); diff --git a/vitest.global-browser-setup.ts b/vitest.global-browser-setup.ts index 8d9bdc4dfa1..d227e092f36 100644 --- a/vitest.global-browser-setup.ts +++ b/vitest.global-browser-setup.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ /* eslint-disable @typescript-eslint/no-floating-promises */ import { spawn } from 'node:child_process'; From eb8eff835d49b8b49b0deb0389214b0377f76f0a Mon Sep 17 00:00:00 2001 From: nedsalk Date: Fri, 21 Jun 2024 15:46:49 +0200 Subject: [PATCH 11/13] increase timeout --- .../src/setup-launch-node-server.test.ts | 50 +++++++------------ 1 file changed, 19 insertions(+), 31 deletions(-) diff --git a/packages/fuels/src/setup-launch-node-server.test.ts b/packages/fuels/src/setup-launch-node-server.test.ts index c045e370dff..090b82b1df9 100644 --- a/packages/fuels/src/setup-launch-node-server.test.ts +++ b/packages/fuels/src/setup-launch-node-server.test.ts @@ -67,39 +67,31 @@ function startServer(port: number = 0): Promise { /** * @group node */ -describe('setup-launch-node-server', () => { - test( - 'can start server on specific port', - async () => { +describe( + 'setup-launch-node-server', + () => { + test('can start server on specific port', async () => { using launched = await startServer(9876); expect(launched.serverUrl).toEqual('http://localhost:9876'); - }, - { timeout: 10000 } - ); + }); - test('the /close-server endpoint closes the server', async () => { - const { serverUrl } = await startServer(); - await fetch(`${serverUrl}/close-server`); + test('the /close-server endpoint closes the server', async () => { + const { serverUrl } = await startServer(); + await fetch(`${serverUrl}/close-server`); - await waitUntilUnreachable(serverUrl); - }); + await waitUntilUnreachable(serverUrl); + }); - test( - 'returns a valid fuel-core node url on request', - async () => { + test('returns a valid fuel-core node url on request', async () => { using launched = await startServer(); const url = await (await fetch(launched.serverUrl)).text(); // fetches node-related data // would fail if fuel-core node is not running on url await Provider.create(url); - }, - { timeout: 10000 } - ); + }); - test( - 'the /cleanup endpoint kills the node', - async () => { + test('the /cleanup endpoint kills the node', async () => { using launched = await startServer(); const url = await (await fetch(launched.serverUrl)).text(); @@ -107,13 +99,9 @@ describe('setup-launch-node-server', () => { // if the node remained live then the test would time out await waitUntilUnreachable(url); - }, - { timeout: 10000 } - ); + }); - test( - 'kills all nodes when the server is shut down', - async () => { + test('kills all nodes when the server is shut down', async () => { const { serverUrl, closeServer: killServer } = await startServer(); const url1 = await (await fetch(serverUrl)).text(); const url2 = await (await fetch(serverUrl)).text(); @@ -123,7 +111,7 @@ describe('setup-launch-node-server', () => { // if the nodes remained live then the test would time out await waitUntilUnreachable(url1); await waitUntilUnreachable(url2); - }, - { timeout: 10000 } - ); -}); + }); + }, + { timeout: 25000 } +); From 9d281fd8a8f3d3e54005b9b6498d51f7749c22a3 Mon Sep 17 00:00:00 2001 From: nedsalk Date: Sat, 22 Jun 2024 15:39:40 +0200 Subject: [PATCH 12/13] make it prettier --- .../fuels/src/setup-launch-node-server.ts | 49 ++++++------------- 1 file changed, 14 insertions(+), 35 deletions(-) diff --git a/packages/fuels/src/setup-launch-node-server.ts b/packages/fuels/src/setup-launch-node-server.ts index 0a274350bc7..f8d09deac96 100644 --- a/packages/fuels/src/setup-launch-node-server.ts +++ b/packages/fuels/src/setup-launch-node-server.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-floating-promises */ /* eslint-disable no-console */ import type { LaunchNodeOptions } from '@fuel-ts/account/test-utils'; import { launchNode } from '@fuel-ts/account/test-utils'; @@ -67,7 +66,11 @@ const server = http.createServer(async (req, res) => { } }); -function closeServer() { +const closeServer = (event?: string) => (reason?: unknown) => { + console.log(event); + if (reason) { + console.log(reason); + } return new Promise((resolve) => { if (!server.listening) { resolve(); @@ -83,11 +86,11 @@ function closeServer() { resolve(); }); -} +}; server.on('request', async (req, res) => { if (req.url === '/close-server') { - await closeServer(); + await closeServer('request to /close-server')(); res.end(); } }); @@ -109,34 +112,10 @@ server.on('listening', () => { console.log('You can close the server by sending a request to /close-server.'); }); -process.on('exit', () => { - console.log('exit'); - closeServer(); -}); -process.on('SIGINT', () => { - console.log('sigint'); - closeServer(); -}); -process.on('SIGUSR1', () => { - console.log('SIGUSR1'); - closeServer(); -}); -process.on('SIGUSR2', () => { - console.log('SIGUSR2'); - closeServer(); -}); -process.on('uncaughtException', (e) => { - console.log('uncaughtException'); - console.log(e); - closeServer(); -}); -process.on('unhandledRejection', (reason) => { - console.log('unhandledRejection'); - console.log(reason); - - closeServer(); -}); -process.on('beforeExit', () => { - console.log('beforeExit'); - closeServer(); -}); +process.on('exit', closeServer('exit')); +process.on('SIGINT', closeServer('SIGINT')); +process.on('SIGUSR1', closeServer('SIGUSR1')); +process.on('SIGUSR2', closeServer('SIGUSR2')); +process.on('uncaughtException', closeServer('uncaughtException')); +process.on('unhandledRejection', closeServer('unhandledRejection')); +process.on('beforeExit', closeServer('beforeExit')); From c6005bee43e6cc1a76107a13cbafeec54f946dbe Mon Sep 17 00:00:00 2001 From: nedsalk Date: Sun, 23 Jun 2024 16:19:16 +0200 Subject: [PATCH 13/13] move from environment variable to argument --- .../test-utils/setup-test-provider-and-wallets.ts | 8 +++++--- .../fuels/src/setup-launch-node-server.test.ts | 14 ++++++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/packages/account/src/test-utils/setup-test-provider-and-wallets.ts b/packages/account/src/test-utils/setup-test-provider-and-wallets.ts index 6ff99aca921..e96963923ab 100644 --- a/packages/account/src/test-utils/setup-test-provider-and-wallets.ts +++ b/packages/account/src/test-utils/setup-test-provider-and-wallets.ts @@ -23,6 +23,7 @@ export interface LaunchCustomProviderAndGetWalletsOptions { snapshotConfig: PartialDeep; } >; + launchNodeServerPort?: string; } const defaultWalletConfigOptions: WalletsConfigOptions = { @@ -52,6 +53,7 @@ export async function setupTestProviderAndWallets({ walletsConfig: walletsConfigOptions = {}, providerOptions, nodeOptions = {}, + launchNodeServerPort = process.env.LAUNCH_NODE_SERVER_PORT || undefined, }: Partial = {}): Promise { // @ts-expect-error this is a polyfill (see https://devblogs.microsoft.com/typescript/announcing-typescript-5-2/#using-declarations-and-explicit-resource-management) Symbol.dispose ??= Symbol('Symbol.dispose'); @@ -64,7 +66,7 @@ export async function setupTestProviderAndWallets({ } ); - const launchNodeOptions = { + const launchNodeOptions: LaunchNodeOptions = { loggingEnabled: false, ...nodeOptions, snapshotConfig: mergeDeepRight( @@ -76,8 +78,8 @@ export async function setupTestProviderAndWallets({ let cleanup: () => void; let url: string; - if (process.env.LAUNCH_NODE_SERVER_PORT) { - const serverUrl = `http://localhost:${process.env.LAUNCH_NODE_SERVER_PORT}`; + if (launchNodeServerPort) { + const serverUrl = `http://localhost:${launchNodeServerPort}`; url = await ( await fetch(serverUrl, { method: 'POST', body: JSON.stringify(launchNodeOptions) }) ).text(); diff --git a/packages/fuels/src/setup-launch-node-server.test.ts b/packages/fuels/src/setup-launch-node-server.test.ts index 090b82b1df9..92479b00e54 100644 --- a/packages/fuels/src/setup-launch-node-server.test.ts +++ b/packages/fuels/src/setup-launch-node-server.test.ts @@ -2,6 +2,8 @@ import { Provider } from '@fuel-ts/account'; import { waitUntilUnreachable } from '@fuel-ts/utils/test-utils'; import { spawn } from 'node:child_process'; +import { launchTestNode } from './test-utils'; + interface ServerInfo extends Disposable { serverUrl: string; closeServer: () => Promise; @@ -112,6 +114,18 @@ describe( await waitUntilUnreachable(url1); await waitUntilUnreachable(url2); }); + + test('launchTestNode launches and kills node ', async () => { + using launchedServer = await startServer(); + const port = launchedServer.serverUrl.split(':')[2]; + const { cleanup, provider } = await launchTestNode({ + launchNodeServerPort: port, + }); + + cleanup(); + + await waitUntilUnreachable(provider.url); + }); }, { timeout: 25000 } );