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 #2622

Merged
merged 27 commits into from
Jun 28, 2024
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
132db19
Revert "chore: revert(add browser testing infrastructure) (#2618)"
danielbate Jun 26, 2024
8e2a6cf
Merge branch 'master' of https://github.com/FuelLabs/fuels-ts into db…
danielbate Jun 26, 2024
0ac9bb3
chore: rebuild
danielbate Jun 26, 2024
88ca637
chore: rebuild
danielbate Jun 26, 2024
16a87fc
chore: add coverage to optimze dep config
danielbate Jun 26, 2024
567ba6e
chore: exclude dep
danielbate Jun 26, 2024
5821036
chore: rebuild
danielbate Jun 26, 2024
41e7da7
chore: rebuild
danielbate Jun 26, 2024
1def352
chore: rebuild
danielbate Jun 26, 2024
6e28bea
chore: rebuild
danielbate Jun 26, 2024
70f2864
chore: rebuild
danielbate Jun 26, 2024
41702bc
chore: rebuild
danielbate Jun 26, 2024
afae704
chore: rebuild
danielbate Jun 26, 2024
8cd6294
chore: rebuild
danielbate Jun 26, 2024
3870eeb
chore: rebuild
danielbate Jun 26, 2024
d8d1565
chore: rebuild
danielbate Jun 26, 2024
82dbbbd
chore: bundle test
danielbate Jun 26, 2024
7cff30a
Merge branch 'master' of https://github.com/FuelLabs/fuels-ts into db…
danielbate Jun 26, 2024
3ea4cca
chore: changeset
danielbate Jun 26, 2024
4cd25d5
chore: remove changeset
danielbate Jun 27, 2024
d1f8ca6
Merge branch 'master' of https://github.com/FuelLabs/fuels-ts into db…
danielbate Jun 27, 2024
9fd6807
Merge branch 'master' of https://github.com/FuelLabs/fuels-ts into db…
danielbate Jun 27, 2024
61a34c7
Merge branch 'master' into db/fix/browser-testing
danielbate Jun 27, 2024
9e447f2
Merge branch 'master' into db/fix/browser-testing
maschad Jun 27, 2024
9884e98
Merge branch 'master' into db/fix/browser-testing
danielbate Jun 28, 2024
9500647
Merge branch 'master' into db/fix/browser-testing
petertonysmith94 Jun 28, 2024
911513b
Merge branch 'master' into db/fix/browser-testing
danielbate Jun 28, 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
2 changes: 0 additions & 2 deletions .changeset/two-nails-report.md
nedsalk marked this conversation as resolved.
Show resolved Hide resolved

This file was deleted.

5 changes: 5 additions & 0 deletions .changeset/violet-shirts-deliver.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"fuels": patch
---

fix: reintroduce browser testing
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');

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);
});
}

/**
* @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