Skip to content

Commit

Permalink
feat!: add launchTestNode utility (#1356)
Browse files Browse the repository at this point in the history
  • Loading branch information
nedsalk authored Jun 6, 2024
1 parent e49b33f commit bb5a123
Show file tree
Hide file tree
Showing 57 changed files with 2,481 additions and 489 deletions.
8 changes: 8 additions & 0 deletions .changeset/breezy-carrots-bow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@fuel-ts/utils": patch
"@fuel-ts/account": minor
"@fuel-ts/contract": patch
"fuels": patch
---

feat!: add `launchTestNode` utility
251 changes: 251 additions & 0 deletions apps/docs-snippets/src/guide/testing/launching-a-test-node.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { WalletUnlocked } from 'fuels';
import { AssetId, TestMessage, launchTestNode } from 'fuels/test-utils';
import { join } from 'path';

import { CounterAbi__factory as TestContract__factory } from '../../../test/typegen/contracts';
import bytecode from '../../../test/typegen/contracts/CounterAbi.hex';

/**
* @group node
*/
describe('launching a test node', () => {
test(`instantiating test nodes - automatic cleanup`, async () => {
// #region automatic-cleanup
// #import { launchTestNode };

using launched = await launchTestNode();

/*
The method `launched.cleanup()` will be automatically
called when the variable `launched` goes out of block scope.
*/

// #endregion automatic-cleanup
});

test('instantiating test nodes - manual cleanup', async () => {
// #region manual-cleanup
// #import { launchTestNode };

const launched = await launchTestNode();

/*
Do your things, run your tests, and then call
`launched.cleanup()` to dispose of everything.
*/

launched.cleanup();
// #endregion manual-cleanup
});

test('options', async () => {
// #region options
// #import { launchTestNode };

using launched = await launchTestNode(/* options */);
// #endregion options
});

test('simple contract deployment', async () => {
// #region basic-example
// #import { launchTestNode };

// #context import { TestContract__factory } from 'path/to/typegen/output';
// #context import bytecode from 'path/to/typegen/output/TestContract.hex.ts';

using launched = await launchTestNode({
contractsConfigs: [
{
deployer: TestContract__factory,
bytecode,
},
],
});

const {
contracts: [contract],
provider,
wallets,
} = launched;

const response = await contract.functions.get_count().call();
// #endregion basic-example
expect(response.value.toNumber()).toBe(0);
expect(provider).toBeDefined();
expect(wallets).toBeDefined();
});

test('multiple contracts and wallets', async () => {
// #region advanced-example
// #import { launchTestNode, AssetId, TestMessage };

// #context import { TestContract__factory } from 'path/to/typegen/output';
// #context import bytecode from 'path/to/typegen/output/TestContract.hex.ts';

const assets = AssetId.random(2);
const message = new TestMessage({ amount: 1000 });

using launched = await launchTestNode({
walletsConfig: {
count: 4,
assets,
coinsPerAsset: 2,
amountPerCoin: 1_000_000,
messages: [message],
},
contractsConfigs: [
{
deployer: TestContract__factory,
bytecode,
walletIndex: 3,
options: { storageSlots: [] },
},
],
});

const {
contracts: [contract],
wallets: [wallet1, wallet2, wallet3, wallet4],
} = launched;
// #endregion advanced-example

expect(contract).toBeDefined();
expect(wallet1).toBeDefined();
expect(wallet2).toBeDefined();
expect(wallet3).toBeDefined();
expect(wallet4).toBeDefined();
});

test('configuring custom fuel-core args', async () => {
// #region custom-fuel-core-args
// #import { launchTestNode };

process.env.DEFAULT_FUEL_CORE_ARGS = `--tx-max-depth 20`;

// `nodeOptions.args` will override the above values if provided.

using launched = await launchTestNode();
// #endregion custom-fuel-core-args

const { provider } = launched;

expect(provider.getNode().maxDepth.toNumber()).toEqual(20);
process.env.DEFAULT_FUEL_CORE_ARGS = '';
});

test('configuring a base chain config', async () => {
const snapshotDirPath = join(__dirname, '../../../../../', '.fuel-core', 'configs');

// #region custom-chain-config
// #import { launchTestNode };

process.env.DEFAULT_CHAIN_SNAPSHOT_DIR = snapshotDirPath;

using launched = await launchTestNode();
// #endregion custom-chain-config

const { provider } = launched;

const { name } = await provider.fetchChain();

expect(name).toEqual('local_testnet');
});

test('customizing node options', async () => {
// #region custom-node-options
// #import { launchTestNode, AssetId };

const [baseAssetId] = AssetId.random();

using launched = await launchTestNode({
nodeOptions: {
snapshotConfig: {
chainConfig: {
consensus_parameters: {
V1: {
base_asset_id: baseAssetId.value,
},
},
},
},
},
});
// #endregion custom-node-options
});

test('using assetId', async () => {
// #region asset-ids
// #import { launchTestNode, AssetId };

const assets = AssetId.random();

using launched = await launchTestNode({
walletsConfig: {
assets,
},
});

const {
wallets: [wallet],
} = launched;

const coins = await wallet.getCoins(assets[0].value);
// #endregion asset-ids
expect(coins[0].assetId).toEqual(assets[0].value);
});

test('generating test messages', async () => {
// #region test-messages
// #import { launchTestNode, TestMessage };

const testMessage = new TestMessage({ amount: 1000 });

using launched = await launchTestNode({
walletsConfig: {
messages: [testMessage],
},
});

const {
wallets: [wallet],
} = launched;

const [message] = await wallet.getMessages();
// message.nonce === testMessage.nonce
// #endregion test-messages

expect(message.nonce).toEqual(testMessage.nonce);
});

test('generating test messages directly on chain', async () => {
// #region test-messages-chain
// #import { launchTestNode, TestMessage, WalletUnlocked };

const recipient = WalletUnlocked.generate();
const testMessage = new TestMessage({
amount: 1000,
recipient: recipient.address,
});

using launched = await launchTestNode({
nodeOptions: {
snapshotConfig: {
stateConfig: {
messages: [testMessage.toChainMessage()],
},
},
},
});

const { provider } = launched;

recipient.provider = provider;

const [message] = await recipient.getMessages();
// message.nonce === testMessage.nonce
// #endregion test-messages-chain

expect(message.nonce).toEqual(testMessage.nonce);
});
});
Original file line number Diff line number Diff line change
@@ -1,24 +1,45 @@
import { launchNode } from '@fuel-ts/account/test-utils';
import { Provider, DateTime } from 'fuels';
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { DateTime } from 'fuels';
import { launchTestNode } from 'fuels/test-utils';

/**
* @group node
*/
test('produceBlocks with custom timestamp docs snippet', async () => {
// TODO: reevaluate/replace after #1356
const { cleanup, ip, port } = await launchNode({});
const url = `http://${ip}:${port}/v1/graphql`;
const provider = await Provider.create(url);
const latestBlock = await provider.getBlock('latest');
if (!latestBlock) {
throw new Error('No latest block');
}
const lastBlockNumber = latestBlock.height;
// #region Provider-produceBlocks-custom-timestamp
const lastBlockTimestamp = DateTime.fromTai64(latestBlock.time).toUnixMilliseconds();
const latestBlockNumber = await provider.produceBlocks(3, lastBlockTimestamp + 1000);
// #endregion Provider-produceBlocks-custom-timestamp
expect(latestBlockNumber.toHex()).toBe(lastBlockNumber.add(3).toHex());
describe('tweaking the blockchain', () => {
test('produceBlocks', async () => {
// #region produce-blocks
using launched = await launchTestNode();
const { provider } = launched;
const block = await provider.getBlock('latest');
if (!block) {
throw new Error('No latest block');
}
const { time: timeLastBlockProduced } = block;

cleanup();
const producedBlockHeight = await provider.produceBlocks(3);

const producedBlock = await provider.getBlock(producedBlockHeight.toNumber());

const oldest = DateTime.fromTai64(timeLastBlockProduced);
const newest = DateTime.fromTai64(producedBlock!.time);
// newest >= oldest
// #endregion produce-blocks
expect(producedBlock).toBeDefined();
expect(newest >= oldest).toBeTruthy();
});

test('produceBlocks with custom timestamp docs snippet', async () => {
// #region produceBlocks-custom-timestamp
using launched = await launchTestNode();
const { provider } = launched;

const latestBlock = await provider.getBlock('latest');
if (!latestBlock) {
throw new Error('No latest block');
}
const latestBlockTimestamp = DateTime.fromTai64(latestBlock.time).toUnixMilliseconds();
const newBlockHeight = await provider.produceBlocks(3, latestBlockTimestamp + 1000);
// #endregion produceBlocks-custom-timestamp
expect(newBlockHeight.toHex()).toBe(latestBlock.height.add(3).toHex());
});
});
28 changes: 18 additions & 10 deletions apps/docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,10 +172,6 @@ export default defineConfig({
text: 'Locking and Unlocking',
link: '/guide/wallets/locking-and-unlocking',
},
{
text: 'Test Wallets',
link: '/guide/wallets/test-wallets',
},
],
},
{
Expand Down Expand Up @@ -378,16 +374,28 @@ export default defineConfig({
collapsed: true,
items: [
{
text: 'Testing in TS',
link: '/guide/testing/testing-in-ts',
text: 'Launching a Test Node',
link: '/guide/testing/launching-a-test-node',
},
{
text: 'Test Node Options',
link: '/guide/testing/test-node-options',
},
{
text: 'Fuel Core Options',
link: '/guide/testing/fuel-core-options',
},
{
text: 'Basic Example',
link: '/guide/testing/basic-example',
},
{
text: 'Setting Up a Custom Chain',
link: '/guide/testing/setting-up-a-custom-chain',
text: 'Advanced Example',
link: '/guide/testing/advanced-example',
},
{
text: 'Tweaking the Blockchain',
link: '/guide/testing/tweaking-the-blockchain',
text: 'Custom Blocks',
link: '/guide/testing/custom-blocks',
},
],
},
Expand Down
9 changes: 8 additions & 1 deletion apps/docs/spell-check-custom-words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@ MULTICALL
TTL
Bech
CLIs
typedoc
backoff
extractable
SHA-256
Expand All @@ -314,4 +315,10 @@ BigNumber
Gwei
onchain
Vercel
hardcoded
hardcoded
tsconfig
deployer
overriden
typesafe
launchTestNode
Vitest
Loading

0 comments on commit bb5a123

Please sign in to comment.