Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
123 changes: 123 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,126 @@ name: E2E Tests

on:
workflow_dispatch:
inputs:
verbose:
description: "Output more information when triggered manually"
required: false
default: ""

env:
CARGO_TERM_COLOR: always

permissions:
contents: read

jobs:
# Build the node binary once and share it across test jobs.
build:
runs-on: [self-hosted, type-ccx33]
timeout-minutes: 60
env:
RUST_BACKTRACE: full
steps:
- name: Check-out repository
uses: actions/checkout@v4

- name: Install system 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 git make libssl-dev llvm libudev-dev protobuf-compiler pkg-config

- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable

- name: Utilize Shared Rust Cache
uses: Swatinem/rust-cache@v2
with:
key: e2e
cache-on-failure: true

- name: Build node-subtensor
run: cargo build --release -p node-subtensor

- name: Upload binary
uses: actions/upload-artifact@v4
with:
name: node-subtensor
path: target/release/node-subtensor
if-no-files-found: error

# Discover e2e packages that have a "test" script.
discover:
runs-on: ubuntu-latest
outputs:
packages: ${{ steps.find.outputs.packages }}
steps:
- name: Check-out repository
uses: actions/checkout@v4
with:
sparse-checkout: e2e

- name: Find testable packages
id: find
working-directory: e2e
run: |
packages=$(
find . -maxdepth 2 -name package.json -not -path './node_modules/*' \
| while read -r pkg; do
name=$(jq -r '.name // empty' "$pkg")
has_test=$(jq -r '.scripts.test // empty' "$pkg")
if [ -n "$has_test" ] && [ -n "$name" ]; then
echo "$name"
fi
done \
| jq -R -s -c 'split("\n") | map(select(. != ""))'
)
echo "packages=$packages" >> "$GITHUB_OUTPUT"
echo "Discovered packages: $packages"

# Run each e2e package's tests in parallel.
test:
needs: [build, discover]
if: ${{ needs.discover.outputs.packages != '[]' }}
runs-on: [self-hosted, type-ccx33]
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
package: ${{ fromJson(needs.discover.outputs.packages) }}
name: "e2e: ${{ matrix.package }}"
steps:
- name: Check-out repository
uses: actions/checkout@v4

- name: Download node-subtensor binary
uses: actions/download-artifact@v4
with:
name: node-subtensor
path: target/release

- name: Make binary executable
run: chmod +x target/release/node-subtensor

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "24"

- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10

- name: Install e2e dependencies
working-directory: e2e
run: pnpm install --frozen-lockfile

- name: Run tests
working-directory: e2e
env:
BINARY_PATH: ${{ github.workspace }}/target/release/node-subtensor
run: pnpm --filter ${{ matrix.package }} test
3 changes: 3 additions & 0 deletions e2e/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/
node-subtensor/
.papi
1 change: 1 addition & 0 deletions e2e/.nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
24
6 changes: 6 additions & 0 deletions e2e/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"printWidth": 100,
"semi": true,
"singleQuote": false,
"trailingComma": "all"
}
82 changes: 82 additions & 0 deletions e2e/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# E2E Tests

End-to-end tests that run against a local multi-node subtensor network.

## Quick start

```bash
cd e2e

# 1. Set up the development environment (nvm, node, pnpm, jq, yq).
./setup_env.sh

# 2. Build the node binary and generate polkadot-api type descriptors.
# Does not require pnpm deps — uses `pnpm dlx` for the papi CLI.
# Re-run this step whenever runtime metadata changes (new pallets,
# modified storage/calls, etc.) to keep descriptors in sync.
./bootstrap_types.sh

# 3. Install dependencies (requires descriptors from step 2).
pnpm install

# 4. Run a test suite.
pnpm --filter e2e-shield test # run the shield suite
pnpm --filter e2e-<name> test # run any suite by name
pnpm -r test # run all suites
```

## Creating a new test package

```bash
./bootstrap_package.sh <name>
pnpm install
pnpm --filter e2e-<name> test
```

This creates a package with:
- `package.json` — depends on `e2e-shared` and `polkadot-api`
- `vitest.config.ts` — sequential execution, 120s timeout, alphabetical sequencer
- `setup.ts` — global setup/teardown that spawns a 2-node network
- `tests/00-basic.test.ts` — sample test

Edit `setup.ts` to configure the number of nodes, extra authorities, and
ports for your suite. Add test-specific dependencies to `package.json`.

## How it works

### Network lifecycle

Each test suite manages its own local network via vitest's `globalSetup`:

1. **setup()** generates a chain spec, optionally patches it with extra
authorities, spawns validator nodes, waits for peering and finalization,
then writes `NetworkState` to a JSON file under `/tmp/subtensor-e2e/`.
2. **Test files** read the state file in `beforeAll()` to get RPC ports and
connect via polkadot-api. Tests run sequentially (alphabetical by filename),
so later files can build on earlier state changes (e.g. scaling the network).
3. **teardown()** stops all nodes (including extras added mid-suite), cleans
up temp directories and the state file.

### Shared utilities (`e2e-shared`)

The `shared/` package provides reusable helpers for all test suites:
spawning and monitoring substrate nodes, generating and patching chain specs,
connecting polkadot-api clients with dev signers, and a custom vitest
sequencer that ensures test files run in alphabetical order.

### Conventions

- **File prefixes** — Name test files `00-`, `01-`, `02-` etc. The custom
sequencer sorts alphabetically, so numbering controls execution order.
- **State file** — Each suite writes to `/tmp/subtensor-e2e/<name>/`. Tests
can update this file mid-suite (e.g. to register extra nodes).
- **Catalog versions** — To add a new dependency, first pin its version in
`pnpm-workspace.yaml` under `catalog:`, then reference it in your
package's `package.json` with `"catalog:"` as the version. This prevents
version drift across packages.
- **Query at "best"** — Storage queries for values that change every block
(e.g. rotating keys) should use `{ at: "best" }` instead of the default
`"finalized"`, since finalized lags ~2 blocks behind with GRANDPA.
- **Built-in shortcuts** — Substrate dev accounts (`one`, `two`, `alice`,
`bob`, etc.) have their keys auto-injected. Custom authorities need
`insertKeys()` before starting the node.
Loading
Loading