Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: add browser testing infrastructure #2378

Merged
merged 26 commits into from
Jun 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a6c5d8d
feat: add browser testing support in `launchTestNode` utility
nedsalk Jun 18, 2024
f3724b6
chore: changeset update
nedsalk Jun 18, 2024
f642a31
add missing group to test
nedsalk Jun 18, 2024
99a8aed
remove unnecessary addition
nedsalk Jun 18, 2024
0abbed7
test: add test for specific port
nedsalk Jun 18, 2024
562cbf5
fix: linting
nedsalk Jun 18, 2024
fd4a5f2
Merge branch 'master' into ns/feat/launch-test-node-in-browser-tests
nedsalk Jun 18, 2024
7045dcf
Add longer timeouts
nedsalk Jun 18, 2024
49b7360
Merge branch 'master' into ns/feat/launch-test-node-in-browser-tests
maschad Jun 20, 2024
7a1ebbf
apply change requests
nedsalk Jun 20, 2024
cdd971e
Merge remote-tracking branch 'origin/master' into ns/feat/launch-test…
nedsalk Jun 20, 2024
475e65e
Merge branch 'master' into ns/feat/launch-test-node-in-browser-tests
maschad Jun 20, 2024
9036e45
Merge branch 'master' into ns/feat/launch-test-node-in-browser-tests
danielbate Jun 21, 2024
549b1fa
Merge branch 'master' into ns/feat/launch-test-node-in-browser-tests
petertonysmith94 Jun 21, 2024
624f24b
add /close-server endpoint
nedsalk Jun 21, 2024
b5132b4
fix: linting
nedsalk Jun 21, 2024
eb8eff8
increase timeout
nedsalk Jun 21, 2024
9d281fd
make it prettier
nedsalk Jun 22, 2024
c6005be
move from environment variable to argument
nedsalk Jun 23, 2024
35d7fed
Merge branch 'master' into ns/feat/launch-test-node-in-browser-tests
nedsalk Jun 23, 2024
457652f
Merge branch 'master' into ns/feat/launch-test-node-in-browser-tests
danielbate Jun 24, 2024
646ddc0
Merge branch 'master' into ns/feat/launch-test-node-in-browser-tests
nedsalk Jun 24, 2024
b0e591c
Merge branch 'master' into ns/feat/launch-test-node-in-browser-tests
nedsalk Jun 24, 2024
f448623
Merge branch 'master' into ns/feat/launch-test-node-in-browser-tests
nedsalk Jun 24, 2024
b7a0728
Merge branch 'master' into ns/feat/launch-test-node-in-browser-tests
nedsalk Jun 24, 2024
e9a2156
Merge branch 'master' into ns/feat/launch-test-node-in-browser-tests
nedsalk Jun 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/serious-dogs-wash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@fuel-ts/account": patch
---

chore: add browser testing infrastructure
3 changes: 2 additions & 1 deletion packages/account/src/test-utils/launchNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -217,6 +216,8 @@ export const launchNode = async ({
snapshotDirToUse = tempDir;
}

const { spawn } = await import('child_process');
nedsalk marked this conversation as resolved.
Show resolved Hide resolved

const child = spawn(
command,
[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export interface LaunchCustomProviderAndGetWalletsOptions {
snapshotConfig: PartialDeep<SnapshotConfigs>;
}
>;
launchNodeServerPort?: string;
}

const defaultWalletConfigOptions: WalletsConfigOptions = {
Expand Down Expand Up @@ -52,6 +53,7 @@ export async function setupTestProviderAndWallets({
walletsConfig: walletsConfigOptions = {},
providerOptions,
nodeOptions = {},
launchNodeServerPort = process.env.LAUNCH_NODE_SERVER_PORT || undefined,
}: Partial<LaunchCustomProviderAndGetWalletsOptions> = {}): Promise<SetupTestProviderAndWalletsReturn> {
// @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');
Expand All @@ -64,15 +66,33 @@ export async function setupTestProviderAndWallets({
}
);

const { cleanup, url } = await launchNode({
const launchNodeOptions: LaunchNodeOptions = {
loggingEnabled: false,
...nodeOptions,
snapshotConfig: mergeDeepRight(
defaultSnapshotConfigs,
walletsConfig.apply(nodeOptions?.snapshotConfig)
),
port: '0',
});
};

let cleanup: () => void;
let url: string;
if (launchNodeServerPort) {
const serverUrl = `http://localhost:${launchNodeServerPort}`;
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;

Expand Down
47 changes: 25 additions & 22 deletions packages/fuel-gauge/src/call-test-contract.test.ts
Original file line number Diff line number Diff line change
@@ -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<CallTestContractAbi>({
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());
});
Expand All @@ -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));
Expand All @@ -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();
Expand Down Expand Up @@ -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));
Expand All @@ -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()
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -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();
Expand Down
131 changes: 131 additions & 0 deletions packages/fuels/src/setup-launch-node-server.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
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<void>;
}

function startServer(port: number = 0): Promise<ServerInfo> {
return new Promise((resolve, reject) => {
const cp = spawn(`pnpm tsx packages/fuels/src/setup-launch-node-server.ts ${port}`, {
detached: true,
shell: 'sh',
});

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) => {
// eslint-disable-next-line no-console
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,
closeServer,
[Symbol.dispose]: closeServer,
});
});

cp.on('error', async (err) => {
await closeServer();
reject(err);
});

cp.on('exit', (code) => {
if (code !== 0) {
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);
});
nedsalk marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* @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('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 () => {
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, closeServer: killServer } = await startServer();
const url1 = await (await fetch(serverUrl)).text();
const url2 = await (await fetch(serverUrl)).text();

await killServer();

// if the nodes remained live then the test would time out
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 }
);
Loading
Loading