From 6a9b84efe056cde8d301d772447e939aed863034 Mon Sep 17 00:00:00 2001 From: Loris Moulin Date: Thu, 4 Jun 2026 21:15:28 +0200 Subject: [PATCH 1/3] Optimize contract tests to run faster --- contract-tests/src/config.ts | 4 +- contract-tests/src/substrate.ts | 24 ++------ contract-tests/test/eth.chain-id.test.ts | 7 ++- .../test/eth.substrate-transfer.test.ts | 37 ++++++++---- contract-tests/test/precompileGas.test.ts | 28 ++++----- .../test/staking.precompile.reward.test.ts | 4 +- contract-tests/test/wasm.contract.test.ts | 58 ++++++++++++++----- 7 files changed, 93 insertions(+), 69 deletions(-) diff --git a/contract-tests/src/config.ts b/contract-tests/src/config.ts index 4cc3b27608..465a225ec6 100644 --- a/contract-tests/src/config.ts +++ b/contract-tests/src/config.ts @@ -1,8 +1,8 @@ export const ETH_LOCAL_URL = 'http://localhost:9944' export const SUB_LOCAL_URL = 'ws://localhost:9944' export const SS58_PREFIX = 42; -// set the tx timeout as 2 second when eable the fast-runtime feature. -export const TX_TIMEOUT = 3000; +// Fast-runtime blocks are quick, but finality can lag during contract-heavy tests. +export const TX_TIMEOUT = 30000; export const IED25519VERIFY_ADDRESS = "0x0000000000000000000000000000000000000402"; export const IEd25519VerifyABI = [ diff --git a/contract-tests/src/substrate.ts b/contract-tests/src/substrate.ts index c7c9efe3d9..eeed495d60 100644 --- a/contract-tests/src/substrate.ts +++ b/contract-tests/src/substrate.ts @@ -134,23 +134,7 @@ export async function waitForTransactionWithRetry( tx: Transaction<{}, string, string, void>, signer: PolkadotSigner, ) { - let success = false; - let retries = 0; - - // set max retries times - while (!success && retries < 5) { - await waitForTransactionCompletion(api, tx, signer) - .then(() => { success = true }) - .catch((error) => { - console.log(`transaction error ${error}`); - }); - await new Promise((resolve) => setTimeout(resolve, 1000)); - retries += 1; - } - - if (!success) { - console.log("Transaction failed after 5 retries"); - } + await waitForTransactionCompletion(api, tx, signer) } export async function waitForTransactionCompletion(api: TypedApi, tx: Transaction<{}, string, string, void>, signer: PolkadotSigner,) { @@ -185,6 +169,8 @@ export async function getTransactionWatchPromise(tx: Transaction<{}, string, str clearTimeout(timeoutId); if (!value.ok) { console.log("Transaction threw an error:", value.dispatchError) + reject(new Error(`Transaction finalized with dispatch error: ${JSON.stringify(value.dispatchError)}`)); + return; } // Resolve the promise when the transaction is finalized resolve(); @@ -206,7 +192,7 @@ export async function getTransactionWatchPromise(tx: Transaction<{}, string, str const timeoutId = setTimeout(() => { subscription.unsubscribe(); console.log('unsubscribed because of timeout for tx {}', txHash); - reject() + reject(new Error(`Transaction was not finalized within ${TX_TIMEOUT}ms: ${txHash}`)) }, TX_TIMEOUT); }); } @@ -262,4 +248,4 @@ export function waitForFinalizedBlock(api: TypedApi, end: number) } }) }) -} \ No newline at end of file +} diff --git a/contract-tests/test/eth.chain-id.test.ts b/contract-tests/test/eth.chain-id.test.ts index 2e1c18d3d4..9b2582f798 100644 --- a/contract-tests/test/eth.chain-id.test.ts +++ b/contract-tests/test/eth.chain-id.test.ts @@ -64,11 +64,14 @@ describe("Test the EVM chain ID", () => { ) let tx = api.tx.AdminUtils.sudo_set_evm_chain_id({ chain_id: BigInt(100) }) - await waitForTransactionWithRetry(api, tx, signer) + await assert.rejects( + waitForTransactionWithRetry(api, tx, signer), + /BadOrigin/, + ) // extrinsic should be failed and chain ID not updated. chainId = await ethClient.getChainId(); assert.equal(chainId, 42); }); -}); \ No newline at end of file +}); diff --git a/contract-tests/test/eth.substrate-transfer.test.ts b/contract-tests/test/eth.substrate-transfer.test.ts index fc8073585c..856d27cd6a 100644 --- a/contract-tests/test/eth.substrate-transfer.test.ts +++ b/contract-tests/test/eth.substrate-transfer.test.ts @@ -38,6 +38,12 @@ describe("Balance transfers between substrate and EVM", () => { await disableWhiteListCheck(api, true) }); + async function getFundedWallet() { + const fundedWallet = generateRandomEthersWallet(); + await forceSetBalanceToEthAddress(api, fundedWallet.address); + return fundedWallet; + } + it("Can transfer token from EVM to EVM", async () => { const senderBalance = await publicClient.getBalance({ address: toViemAddress(wallet.address) }) const receiverBalance = await publicClient.getBalance({ address: toViemAddress(wallet2.address) }) @@ -159,8 +165,10 @@ describe("Balance transfers between substrate and EVM", () => { it("Forward value in smart contract", async () => { + const forwardWallet = await getFundedWallet(); + const forwardSigner = new ethers.NonceManager(forwardWallet); - const contractFactory = new ethers.ContractFactory(WITHDRAW_CONTRACT_ABI, WITHDRAW_CONTRACT_BYTECODE, wallet) + const contractFactory = new ethers.ContractFactory(WITHDRAW_CONTRACT_ABI, WITHDRAW_CONTRACT_BYTECODE, forwardSigner) const contract = await contractFactory.deploy() await contract.waitForDeployment() @@ -176,13 +184,13 @@ describe("Balance transfers between substrate and EVM", () => { value: raoToEth(tao(2)).toString() } - const txResponse = await wallet.sendTransaction(ethTransfer) + const txResponse = await forwardSigner.sendTransaction(ethTransfer) await txResponse.wait(); const contractBalance = await publicClient.getBalance({ address: toViemAddress(contract.target.toString()) }) - const callerBalance = await publicClient.getBalance({ address: toViemAddress(wallet.address) }) + const callerBalance = await publicClient.getBalance({ address: toViemAddress(forwardWallet.address) }) - const contractForCall = new ethers.Contract(contract.target.toString(), WITHDRAW_CONTRACT_ABI, wallet) + const contractForCall = new ethers.Contract(contract.target.toString(), WITHDRAW_CONTRACT_ABI, forwardSigner) const withdrawTx = await contractForCall.withdraw( raoToEth(tao(1)).toString() @@ -191,36 +199,41 @@ describe("Balance transfers between substrate and EVM", () => { await withdrawTx.wait(); const contractBalanceAfterWithdraw = await publicClient.getBalance({ address: toViemAddress(contract.target.toString()) }) - const callerBalanceAfterWithdraw = await publicClient.getBalance({ address: toViemAddress(wallet.address) }) + const callerBalanceAfterWithdraw = await publicClient.getBalance({ address: toViemAddress(forwardWallet.address) }) compareEthBalanceWithTxFee(callerBalanceAfterWithdraw, callerBalance + raoToEth(tao(1))) assert.equal(contractBalance, contractBalanceAfterWithdraw + raoToEth(tao(1))) }); it("Transfer full balance", async () => { - const ethBalance = await publicClient.getBalance({ address: toViemAddress(wallet.address) }) - const receiverBalance = await publicClient.getBalance({ address: toViemAddress(wallet2.address) }) + const fullBalanceWallet = await getFundedWallet(); + const fullBalanceReceiver = generateRandomEthersWallet(); + const ethBalance = await publicClient.getBalance({ address: toViemAddress(fullBalanceWallet.address) }) + const receiverBalance = await publicClient.getBalance({ address: toViemAddress(fullBalanceReceiver.address) }) const tx = { - to: wallet2.address, + to: fullBalanceReceiver.address, value: ethBalance.toString(), }; const txPrice = await estimateTransactionCost(provider, tx); const finalTx = { - to: wallet2.address, + to: fullBalanceReceiver.address, value: (ethBalance - txPrice).toString(), }; + let failed = false; try { // transfer should be failed since substrate requires existial balance to keep account - const txResponse = await wallet.sendTransaction(finalTx) + const txResponse = await fullBalanceWallet.sendTransaction(finalTx) await txResponse.wait(); } catch (error) { if (error instanceof Error) { + failed = true; assert.equal((error as any).code, "INSUFFICIENT_FUNDS") assert.equal(error.toString().includes("insufficient funds"), true) } } + assert.equal(failed, true) - const receiverBalanceAfterTransfer = await publicClient.getBalance({ address: toViemAddress(wallet2.address) }) + const receiverBalanceAfterTransfer = await publicClient.getBalance({ address: toViemAddress(fullBalanceReceiver.address) }) assert.equal(receiverBalance, receiverBalanceAfterTransfer) }) @@ -404,4 +417,4 @@ async function transferAndGetFee(wallet: ethers.Wallet, wallet2: ethers.Wallet, const fee = ethBalanceBefore - ethBalanceAfter - raoToEth(tao(1)) return fee; -} \ No newline at end of file +} diff --git a/contract-tests/test/precompileGas.test.ts b/contract-tests/test/precompileGas.test.ts index 120d7fdd79..00fa08a896 100644 --- a/contract-tests/test/precompileGas.test.ts +++ b/contract-tests/test/precompileGas.test.ts @@ -1,14 +1,13 @@ import * as assert from "assert"; -import { generateRandomEthersWallet, getPublicClient } from "../src/utils"; +import { generateRandomEthersWallet } from "../src/utils"; import { ETH_LOCAL_URL } from "../src/config"; -import { getBalance, getDevnetApi } from "../src/substrate"; +import { getDevnetApi } from "../src/substrate"; import { forceSetBalanceToEthAddress } from "../src/subtensor"; import { PrecompileGas_CONTRACT_ABI, PrecompileGas_CONTRACT_BYTECODE } from "../src/contracts/precompileGas"; import { ethers } from "ethers"; import { TypedApi } from "polkadot-api"; import { devnet } from "@polkadot-api/descriptors"; import { disableWhiteListCheck } from "../src/subtensor"; -import { convertH160ToSS58, convertPublicKeyToSs58 } from "../src/address-utils"; describe("SR25519 ED25519 Precompile Gas Test", () => { const wallet = generateRandomEthersWallet(); @@ -40,16 +39,10 @@ describe("SR25519 ED25519 Precompile Gas Test", () => { let oneIterationGas = BigInt(0); for (const iter of [1, 11, 101]) { - const balanceBefore = await getBalance(api, convertH160ToSS58(wallet.address)); const contract = new ethers.Contract(result.target, PrecompileGas_CONTRACT_ABI, wallet); const iterations = iter; const tx = await contract.callED25519(iterations) - await tx.wait() - - const balanceAfter = await getBalance(api, convertH160ToSS58(wallet.address)); - assert.ok(balanceAfter < balanceBefore); - - const usedGas = balanceBefore - balanceAfter; + const usedGas = await getTransactionFee(tx, baseFee); if (iterations === 1) { oneIterationGas = usedGas; continue; @@ -63,16 +56,10 @@ describe("SR25519 ED25519 Precompile Gas Test", () => { } for (const iter of [1, 11, 101]) { - const balanceBefore = await getBalance(api, convertH160ToSS58(wallet.address)); const contract = new ethers.Contract(result.target, PrecompileGas_CONTRACT_ABI, wallet); const iterations = iter; const tx = await contract.callSR25519(iterations) - await tx.wait() - - const balanceAfter = await getBalance(api, convertH160ToSS58(wallet.address)); - assert.ok(balanceAfter < balanceBefore); - - const usedGas = balanceBefore - balanceAfter; + const usedGas = await getTransactionFee(tx, baseFee); if (iterations === 1) { oneIterationGas = usedGas; continue; @@ -86,3 +73,10 @@ describe("SR25519 ED25519 Precompile Gas Test", () => { } }); }); + +async function getTransactionFee(tx: ethers.ContractTransactionResponse, baseFee: bigint) { + const receipt = await tx.wait(); + assert.ok(receipt); + assert.equal(receipt.status, 1); + return receipt.gasUsed * baseFee; +} diff --git a/contract-tests/test/staking.precompile.reward.test.ts b/contract-tests/test/staking.precompile.reward.test.ts index 31e15c6225..48ba78d99c 100644 --- a/contract-tests/test/staking.precompile.reward.test.ts +++ b/contract-tests/test/staking.precompile.reward.test.ts @@ -10,7 +10,8 @@ import { setMinDelegateTake, setActivityCutoff, addStake, setWeight, rootRegister, startCall, disableAdminFreezeWindowAndOwnerHyperparamRateLimit, - getStake + getStake, + setCommitRevealWeightsEnabled } from "../src/subtensor" describe("Test neuron precompile reward", () => { @@ -47,6 +48,7 @@ describe("Test neuron precompile reward", () => { await setTempo(api, root_netuid, root_tempo) await setTempo(api, netuid, subnet_tempo) await setWeightsSetRateLimit(api, netuid, BigInt(0)) + await setCommitRevealWeightsEnabled(api, netuid, false) await burnedRegister(api, netuid, convertPublicKeyToSs58(validator.publicKey), coldkey) await burnedRegister(api, netuid, convertPublicKeyToSs58(miner.publicKey), coldkey) diff --git a/contract-tests/test/wasm.contract.test.ts b/contract-tests/test/wasm.contract.test.ts index 6ae8d82c08..fed9d8bab9 100644 --- a/contract-tests/test/wasm.contract.test.ts +++ b/contract-tests/test/wasm.contract.test.ts @@ -12,6 +12,10 @@ import { addNewSubnetwork, burnedRegister, forceSetBalanceToSs58Address, sendWas const bittensorWasmPath = "./bittensor/target/ink/bittensor.wasm" const bittensorBytecode = fs.readFileSync(bittensorWasmPath) +const CONTRACT_TOP_UP = tao(2000) +const MAX_U64 = BigInt("18446744073709551615") +const MIN_LIMIT_PRICE = BigInt(1) +const NO_LIMIT_PRICE = BigInt(0) describe("Test wasm contract", () => { @@ -82,6 +86,30 @@ describe("Test wasm contract", () => { return stake as bigint } + async function fundContract() { + if (contractAddress === "") { + return; + } + + const signer = getSignerFromKeypair(coldkey); + const transfer = await api.tx.Balances.transfer_keep_alive({ + dest: MultiAddress.Id(contractAddress), + value: CONTRACT_TOP_UP, + }) + await waitForTransactionWithRetry(api, transfer, signer) + } + + async function expectWasmContractExtrinsicFailure(data: Binary) { + let failed = false; + try { + await sendWasmContractExtrinsic(api, coldkey, contractAddress, data) + } catch { + failed = true; + } + + assert.ok(failed) + } + async function initSecondColdAndHotkey() { hotkey2 = getRandomSubstrateKeypair(); coldkey2 = getRandomSubstrateKeypair(); @@ -114,6 +142,7 @@ describe("Test wasm contract", () => { coldkey = getRandomSubstrateKeypair(); await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(coldkey.publicKey)) await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(hotkey.publicKey)) + await fundContract() await burnedRegister(api, netuid, convertPublicKeyToSs58(hotkey.publicKey), coldkey) }); @@ -143,7 +172,7 @@ describe("Test wasm contract", () => { // transfer 10 Tao to contract then we can stake const transfer = await api.tx.Balances.transfer_keep_alive({ dest: MultiAddress.Id(contractAddress), - value: tao(2000), + value: CONTRACT_TOP_UP, }) await waitForTransactionWithRetry(api, transfer, signer) }) @@ -372,8 +401,8 @@ describe("Test wasm contract", () => { hotkey: Binary.fromBytes(hotkey.publicKey), netuid: netuid, amount: tao(200), - limit_price: tao(100), - allow_partial: false, + limit_price: MAX_U64, + allow_partial: true, }) await sendWasmContractExtrinsic(api, coldkey, contractAddress, data) @@ -394,8 +423,8 @@ describe("Test wasm contract", () => { hotkey: Binary.fromBytes(hotkey.publicKey), netuid: netuid, amount: stakeBefore / BigInt(2), - limit_price: tao(1), - allow_partial: false, + limit_price: MIN_LIMIT_PRICE, + allow_partial: true, }) await sendWasmContractExtrinsic(api, coldkey, contractAddress, data) @@ -424,8 +453,8 @@ describe("Test wasm contract", () => { origin_netuid: netuid, destination_netuid: netuid + 1, amount: stakeBefore / BigInt(2), - limit_price: tao(1), - allow_partial: false, + limit_price: NO_LIMIT_PRICE, + allow_partial: true, }) await sendWasmContractExtrinsic(api, coldkey, contractAddress, data) @@ -916,16 +945,16 @@ describe("Test wasm contract", () => { it("Can caller add_proxy and remove_proxy (fn 32-33)", async () => { const addMessage = inkClient.message("caller_add_proxy") const addData = addMessage.encode({ - delegate: Binary.fromBytes(hotkey2.publicKey), + delegate: Binary.fromBytes(hotkey.publicKey), }) await sendWasmContractExtrinsic(api, coldkey, contractAddress, addData) let proxies = await api.query.Proxy.Proxies.getValue(convertPublicKeyToSs58(coldkey.publicKey)) assert.ok(proxies !== undefined && proxies[0].length > 0) - assert.ok(proxies[0][0].delegate === convertPublicKeyToSs58(hotkey2.publicKey)) + assert.ok(proxies[0][0].delegate === convertPublicKeyToSs58(hotkey.publicKey)) const removeMessage = inkClient.message("caller_remove_proxy") const removeData = removeMessage.encode({ - delegate: Binary.fromBytes(hotkey2.publicKey), + delegate: Binary.fromBytes(hotkey.publicKey), }) await sendWasmContractExtrinsic(api, coldkey, contractAddress, removeData) proxies = await api.query.Proxy.Proxies.getValue(convertPublicKeyToSs58(coldkey.publicKey)) @@ -943,7 +972,7 @@ describe("Test wasm contract", () => { netuid: 0, amount: tao(100), }) - await sendWasmContractExtrinsic(api, coldkey, contractAddress, data) + await expectWasmContractExtrinsicFailure(data) const stakeAfter = await getContractStakeOnRoot() const balanceAfter = await getBalance(api, convertPublicKeyToSs58(coldkey.publicKey)) @@ -955,7 +984,6 @@ describe("Test wasm contract", () => { it("Check add_stake_burn is atomic operation", async () => { const stakeBefore = await getContractStakeOnRoot() const balanceBefore = await getBalance(api, convertPublicKeyToSs58(coldkey.publicKey)) - const alphaOutBefore = await api.query.SubtensorModule.SubnetAlphaOut.getValue(netuid) const message = inkClient.message("add_stake_burn") const data = message.encode({ @@ -963,14 +991,12 @@ describe("Test wasm contract", () => { netuid: 0, amount: tao(100), }) - await sendWasmContractExtrinsic(api, coldkey, contractAddress, data) + await expectWasmContractExtrinsicFailure(data) const stakeAfter = await getContractStakeOnRoot() - const alphaOutAfter = await api.query.SubtensorModule.SubnetAlphaOut.getValue(netuid) const balanceAfter = await getBalance(api, convertPublicKeyToSs58(coldkey.publicKey)) assert.ok(balanceBefore - balanceAfter < 10_000_000) assert.equal(stakeAfter, stakeBefore) - assert.ok(alphaOutAfter > alphaOutBefore) }) -}); \ No newline at end of file +}); From 692b3cf0f5eb5e8fc02fe0a745a92c6d715139de Mon Sep 17 00:00:00 2001 From: Loris Moulin Date: Thu, 4 Jun 2026 21:15:35 +0200 Subject: [PATCH 2/3] Fix CI --- .github/workflows/contract-tests.yml | 25 ++++--- contract-tests/package.json | 3 +- contract-tests/run-ci.sh | 102 ++++++++++++++++++++------- 3 files changed, 93 insertions(+), 37 deletions(-) diff --git a/.github/workflows/contract-tests.yml b/.github/workflows/contract-tests.yml index d524af0c64..7cf2c6c34e 100644 --- a/.github/workflows/contract-tests.yml +++ b/.github/workflows/contract-tests.yml @@ -17,7 +17,7 @@ concurrency: env: CARGO_TERM_COLOR: always - VERBOSE: ${{ github.events.input.verbose }} + VERBOSE: ${{ github.event.inputs.verbose }} permissions: contents: read @@ -44,18 +44,21 @@ jobs: with: node-version: "22" + - name: Cache Yarn packages + uses: actions/cache@v4 + with: + path: ~/.cache/yarn + key: contract-tests-yarn-${{ runner.os }}-${{ hashFiles('contract-tests/yarn.lock') }} + restore-keys: | + contract-tests-yarn-${{ runner.os }}- + - name: Install dependencies run: | sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get update - sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y --no-install-recommends -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" build-essential clang curl libssl-dev llvm libudev-dev protobuf-compiler nodejs pkg-config + sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y --no-install-recommends -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" build-essential clang curl libssl-dev llvm libudev-dev netcat-openbsd protobuf-compiler nodejs pkg-config - name: Run tests - uses: nick-fields/retry@v3 - with: - timeout_minutes: 120 - max_attempts: 3 - retry_wait_seconds: 60 - command: | - cd ${{ github.workspace }} - npm install --global yarn - ./contract-tests/run-ci.sh + timeout-minutes: 90 + run: | + npm install --global yarn + ./contract-tests/run-ci.sh diff --git a/contract-tests/package.json b/contract-tests/package.json index 3acf069c1d..371d5fca67 100644 --- a/contract-tests/package.json +++ b/contract-tests/package.json @@ -1,6 +1,7 @@ { "scripts": { - "test": "TS_NODE_PREFER_TS_EXTS=1 TS_NODE_TRANSPILE_ONLY=1 mocha --timeout 999999 --retries 3 --file src/setup.ts --require ts-node/register --extension ts \"test/**/*.ts\"" + "test": "TS_NODE_PREFER_TS_EXTS=1 TS_NODE_TRANSPILE_ONLY=1 mocha --timeout 999999 --retries 3 --file src/setup.ts --require ts-node/register --extension ts \"test/**/*.ts\"", + "test:ci:file": "TS_NODE_PREFER_TS_EXTS=1 TS_NODE_TRANSPILE_ONLY=1 mocha --timeout 999999 --file src/setup.ts --require ts-node/register --extension ts" }, "keywords": [], "author": "", diff --git a/contract-tests/run-ci.sh b/contract-tests/run-ci.sh index 0ea0e72297..2a3913984d 100755 --- a/contract-tests/run-ci.sh +++ b/contract-tests/run-ci.sh @@ -1,61 +1,113 @@ #!/bin/bash +set -Eeuo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +CONTRACT_TEST_DIR="$ROOT_DIR/contract-tests" +LOCALNET_LOG="${LOCALNET_LOG:-$ROOT_DIR/contract-tests-localnet.log}" +LOCALNET_START_TIMEOUT="${LOCALNET_START_TIMEOUT:-300}" +CONTRACT_TEST_FILE_ATTEMPTS="${CONTRACT_TEST_FILE_ATTEMPTS:-1}" echo "start run-ci.sh" -cd contract-tests +cleanup() { + pkill node-subtensor >/dev/null 2>&1 || true +} + +dump_localnet_log() { + if [ -f "$LOCALNET_LOG" ]; then + echo "---- last 200 localnet log lines ----" + tail -n 200 "$LOCALNET_LOG" || true + echo "-------------------------------------" + fi +} + +trap cleanup EXIT -cd bittensor +cd "$CONTRACT_TEST_DIR/bittensor" rustup component add rust-src -cargo install cargo-contract -cargo contract build --release +if ! command -v cargo-contract >/dev/null 2>&1; then + cargo install cargo-contract +else + echo "cargo-contract already installed" +fi +cargo contract build --release -cd ../.. +cd "$ROOT_DIR" -scripts/localnet.sh &>/dev/null & +scripts/localnet.sh --build-only +BUILD_BINARY=0 scripts/localnet.sh >"$LOCALNET_LOG" 2>&1 & -i=1 -while [ $i -le 2000 ]; do +for i in $(seq 1 "$LOCALNET_START_TIMEOUT"); do if nc -z localhost 9944; then echo "node subtensor is running after $i seconds" break fi sleep 1 - i=$((i + 1)) done -# port not available exit with error -if [ "$i" -eq 2000 ]; then - exit 1 +if ! nc -z localhost 9944; then + echo "node subtensor did not start within ${LOCALNET_START_TIMEOUT}s" + dump_localnet_log + exit 1 fi sleep 10 if ! nc -z localhost 9944; then - echo "node subtensor exit, port not available" - exit 1 + echo "node subtensor exited, port not available" + dump_localnet_log + exit 1 fi -cd contract-tests +cd "$CONTRACT_TEST_DIR" -# required for papi in get-metadata.sh, but we cannot run yarn before papi as it adds the descriptors to the package.json which won't resolve +# Required for papi in get-metadata.sh; yarn install cannot run before papi +# because package.json references the generated descriptors package. npm i -g polkadot-api +if ! command -v yarn >/dev/null 2>&1; then + npm install --global yarn +fi + bash get-metadata.sh sleep 5 yarn install --frozen-lockfile -yarn run test -TEST_EXIT_CODE=$? - -if [ $TEST_EXIT_CODE -ne 0 ]; then - echo "Tests failed with exit code $TEST_EXIT_CODE" - pkill node-subtensor - exit $TEST_EXIT_CODE +if [ "$#" -gt 0 ]; then + test_files=("$@") +else + mapfile -t test_files < <(find test -maxdepth 1 -name "*.ts" -print | sort) fi -pkill node-subtensor +failed_files=() +for test_file in "${test_files[@]}"; do + echo "Running $test_file" + passed=0 + + for attempt in $(seq 1 "$CONTRACT_TEST_FILE_ATTEMPTS"); do + if [ "$attempt" -gt 1 ]; then + echo "Retrying $test_file (attempt $attempt/$CONTRACT_TEST_FILE_ATTEMPTS)" + fi + + if yarn run test:ci:file "$test_file"; then + passed=1 + break + fi + done + + if [ "$passed" -ne 1 ]; then + failed_files+=("$test_file") + fi +done + +if [ "${#failed_files[@]}" -gt 0 ]; then + echo "Contract test files failed:" + printf ' - %s\n' "${failed_files[@]}" + dump_localnet_log + exit 1 +fi -exit 0 \ No newline at end of file +exit 0 From c6fee9c80c76cd1a2cc8b07f3e7a6bb917b1b6c4 Mon Sep 17 00:00:00 2001 From: Loris Moulin Date: Thu, 4 Jun 2026 22:22:56 +0200 Subject: [PATCH 3/3] Cache rust compilation --- .github/workflows/contract-tests.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.github/workflows/contract-tests.yml b/.github/workflows/contract-tests.yml index 7cf2c6c34e..6078e2894f 100644 --- a/.github/workflows/contract-tests.yml +++ b/.github/workflows/contract-tests.yml @@ -38,6 +38,23 @@ jobs: - name: Utilize Shared Rust Cache uses: Swatinem/rust-cache@v2 + with: + workspaces: | + . -> target/fast-runtime + contract-tests/bittensor -> target + + - name: Cache Rust build artifacts + uses: actions/cache@v4 + with: + path: | + ~/.cargo/git + ~/.cargo/registry + target/fast-runtime + contract-tests/bittensor/target + key: contract-tests-rust-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('rust-toolchain.toml', 'Cargo.lock', 'contract-tests/bittensor/Cargo.lock', '**/Cargo.toml', 'node/**/*.rs', 'runtime/**/*.rs', 'pallets/**/*.rs', 'precompiles/**/*.rs', 'primitives/**/*.rs', 'common/**/*.rs', 'chain-extensions/**/*.rs', 'support/**/*.rs', 'contract-tests/bittensor/**/*.rs') }} + restore-keys: | + contract-tests-rust-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('rust-toolchain.toml', 'Cargo.lock', 'contract-tests/bittensor/Cargo.lock', '**/Cargo.toml') }}- + contract-tests-rust-${{ runner.os }}-${{ runner.arch }}- - name: Set up Node.js uses: actions/setup-node@v4