Skip to content

Commit 67dd45f

Browse files
authored
Merge pull request #393 from opentensor/feat/roman/improve-e2e-tests
Improve e2e tests' workflow
2 parents fe705bd + fe10f98 commit 67dd45f

File tree

7 files changed

+213
-51
lines changed

7 files changed

+213
-51
lines changed

.github/workflows/e2e-subtensor-tests.yml

Lines changed: 65 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -24,48 +24,81 @@ env:
2424
VERBOSE: ${{ github.event.inputs.verbose }}
2525

2626
jobs:
27-
run-tests:
28-
runs-on: SubtensorCI
29-
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
30-
timeout-minutes: 180
31-
env:
32-
RELEASE_NAME: development
33-
RUSTV: stable
34-
RUST_BACKTRACE: full
35-
RUST_BIN_DIR: target/x86_64-unknown-linux-gnu
36-
TARGET: x86_64-unknown-linux-gnu
3727

28+
find-tests:
29+
runs-on: ubuntu-latest
30+
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
31+
outputs:
32+
test-files: ${{ steps.get-tests.outputs.test-files }}
3833
steps:
3934
- name: Check-out repository under $GITHUB_WORKSPACE
40-
uses: actions/checkout@v2
35+
uses: actions/checkout@v4
4136

42-
- name: Install dependencies
37+
- name: Find test files
38+
id: get-tests
4339
run: |
44-
sudo apt-get update &&
45-
sudo apt-get install -y clang curl libssl-dev llvm libudev-dev protobuf-compiler
40+
test_files=$(find tests/e2e_tests -name "test*.py" | jq -R -s -c 'split("\n") | map(select(. != ""))')
41+
echo "::set-output name=test-files::$test_files"
42+
shell: bash
43+
44+
pull-docker-image:
45+
runs-on: ubuntu-latest
46+
steps:
47+
- name: Log in to GitHub Container Registry
48+
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u $GITHUB_ACTOR --password-stdin
49+
50+
- name: Pull Docker Image
51+
run: docker pull ghcr.io/opentensor/subtensor-localnet:latest
52+
53+
- name: Save Docker Image to Cache
54+
run: docker save -o subtensor-localnet.tar ghcr.io/opentensor/subtensor-localnet:latest
4655

47-
- name: Install Rust ${{ env.RUSTV }}
48-
uses: actions-rs/[email protected]
56+
- name: Upload Docker Image as Artifact
57+
uses: actions/upload-artifact@v4
4958
with:
50-
toolchain: ${{ env.RUSTV }}
51-
components: rustfmt
52-
profile: minimal
59+
name: subtensor-localnet
60+
path: subtensor-localnet.tar
5361

54-
- name: Add wasm32-unknown-unknown target
55-
run: |
56-
rustup target add wasm32-unknown-unknown --toolchain stable-x86_64-unknown-linux-gnu
57-
rustup component add rust-src --toolchain stable-x86_64-unknown-linux-gnu
62+
run-e2e-tests:
63+
name: ${{ matrix.test-file }} / Python ${{ matrix.python-version }}
64+
needs:
65+
- find-tests
66+
- pull-docker-image
67+
runs-on: ubuntu-latest
68+
timeout-minutes: 45
69+
strategy:
70+
fail-fast: false # Allow other matrix jobs to run even if this job fails
71+
max-parallel: 32 # Set the maximum number of parallel jobs (same as we have cores in SubtensorCI runner)
72+
matrix:
73+
os:
74+
- ubuntu-latest
75+
test-file: ${{ fromJson(needs.find-tests.outputs.test-files) }}
76+
# python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
77+
steps:
78+
- name: Check-out repository
79+
uses: actions/checkout@v4
80+
81+
- name: Install uv
82+
uses: astral-sh/setup-uv@v4
83+
with:
84+
python-version: 3.13
5885

59-
- name: Clone subtensor repo
60-
run: git clone https://github.com/opentensor/subtensor.git
86+
- name: install dependencies
87+
run: |
88+
uv venv .venv
89+
source .venv/bin/activate
90+
uv pip install .[dev]
91+
uv pip install pytest
6192
62-
- name: Setup subtensor repo
63-
working-directory: ${{ github.workspace }}/subtensor
64-
run: git checkout devnet-ready
93+
- name: Download Cached Docker Image
94+
uses: actions/download-artifact@v4
95+
with:
96+
name: subtensor-localnet
6597

66-
- name: Install Python dependencies
67-
run: python3 -m pip install -e . pytest
98+
- name: Load Docker Image
99+
run: docker load -i subtensor-localnet.tar
68100

69-
- name: Run all tests
101+
- name: Run tests
70102
run: |
71-
LOCALNET_SH_PATH="${{ github.workspace }}/subtensor/scripts/localnet.sh" pytest tests/e2e_tests -s
103+
source .venv/bin/activate
104+
uv run pytest ${{ matrix.test-file }} -s

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## 9.2.0 /2025-03-18
44

55
## What's Changed
6+
* Improve e2e tests' workflow by @roman-opentensor in https://github.com/opentensor/btcli/pull/393
67
* Updates to E2E suubtensor tests to devnet ready by @ibraheem-opentensor in https://github.com/opentensor/btcli/pull/390
78
* Allow Py 3.13 install by @thewhaleking in https://github.com/opentensor/btcli/pull/392
89
* pip install readme by @thewhaleking in https://github.com/opentensor/btcli/pull/391

pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ dependencies = [
4141
[project.optional-dependencies]
4242
cuda = [
4343
"torch>=1.13.1,<2.6.0",
44-
"cubit>=1.1.0"
4544
]
4645

4746
[project.urls]

tests/e2e_tests/conftest.py

Lines changed: 142 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import shutil
66
import signal
77
import subprocess
8+
import sys
89
import time
910

1011
import pytest
@@ -13,9 +14,48 @@
1314
from .utils import setup_wallet
1415

1516

17+
def wait_for_node_start(process, pattern, timestamp: int = None):
18+
for line in process.stdout:
19+
print(line.strip())
20+
# 20 min as timeout
21+
timestamp = timestamp or int(time.time())
22+
if int(time.time()) - timestamp > 20 * 60:
23+
pytest.fail("Subtensor not started in time")
24+
if pattern.search(line):
25+
print("Node started!")
26+
break
27+
28+
1629
# Fixture for setting up and tearing down a localnet.sh chain between tests
1730
@pytest.fixture(scope="function")
1831
def local_chain(request):
32+
"""Determines whether to run the localnet.sh script in a subprocess or a Docker container."""
33+
args = request.param if hasattr(request, "param") else None
34+
params = "" if args is None else f"{args}"
35+
if shutil.which("docker") and not os.getenv("USE_DOCKER") == "0":
36+
yield from docker_runner(params)
37+
else:
38+
if not os.getenv("USE_DOCKER") == "0":
39+
if sys.platform.startswith("linux"):
40+
docker_command = (
41+
"Install docker with command "
42+
"[blue]sudo apt-get update && sudo apt-get install docker.io -y[/blue]"
43+
" or use documentation [blue]https://docs.docker.com/engine/install/[/blue]"
44+
)
45+
elif sys.platform == "darwin":
46+
docker_command = (
47+
"Install docker with command [blue]brew install docker[/blue]"
48+
)
49+
else:
50+
docker_command = "[blue]Unknown OS, install Docker manually: https://docs.docker.com/get-docker/[/blue]"
51+
52+
logging.warning("Docker not found in the operating system!")
53+
logging.warning(docker_command)
54+
logging.warning("Tests are run in legacy mode.")
55+
yield from legacy_runner(request)
56+
57+
58+
def legacy_runner(request):
1959
param = request.param if hasattr(request, "param") else None
2060
# Get the environment variable for the script path
2161
script_path = os.getenv("LOCALNET_SH_PATH")
@@ -41,18 +81,6 @@ def local_chain(request):
4181
# Install neuron templates
4282
logging.info("Downloading and installing neuron templates from github")
4383

44-
timestamp = int(time.time())
45-
46-
def wait_for_node_start(process, pattern):
47-
for line in process.stdout:
48-
print(line.strip())
49-
# 20 min as timeout
50-
if int(time.time()) - timestamp > 20 * 60:
51-
pytest.fail("Subtensor not started in time")
52-
if pattern.search(line):
53-
print("Node started!")
54-
break
55-
5684
wait_for_node_start(process, pattern)
5785

5886
# Run the test, passing in substrate interface
@@ -72,6 +100,108 @@ def wait_for_node_start(process, pattern):
72100
process.wait()
73101

74102

103+
def docker_runner(params):
104+
"""Starts a Docker container before tests and gracefully terminates it after."""
105+
106+
def is_docker_running():
107+
"""Check if Docker has been run."""
108+
try:
109+
subprocess.run(
110+
["docker", "info"],
111+
stdout=subprocess.DEVNULL,
112+
stderr=subprocess.DEVNULL,
113+
check=True,
114+
)
115+
return True
116+
except subprocess.CalledProcessError:
117+
return False
118+
119+
def try_start_docker():
120+
"""Run docker based on OS."""
121+
try:
122+
subprocess.run(["open", "-a", "Docker"], check=True) # macOS
123+
except (FileNotFoundError, subprocess.CalledProcessError):
124+
try:
125+
subprocess.run(["systemctl", "start", "docker"], check=True) # Linux
126+
except (FileNotFoundError, subprocess.CalledProcessError):
127+
try:
128+
subprocess.run(
129+
["sudo", "service", "docker", "start"], check=True
130+
) # Linux alternative
131+
except (FileNotFoundError, subprocess.CalledProcessError):
132+
print("Failed to start Docker. Manual start may be required.")
133+
return False
134+
135+
# Wait Docker run 10 attempts with 3 sec waits
136+
for _ in range(10):
137+
if is_docker_running():
138+
return True
139+
time.sleep(3)
140+
141+
print("Docker wasn't run. Manual start may be required.")
142+
return False
143+
144+
container_name = f"test_local_chain_{str(time.time()).replace('.', '_')}"
145+
image_name = "ghcr.io/opentensor/subtensor-localnet:latest"
146+
147+
# Command to start container
148+
cmds = [
149+
"docker",
150+
"run",
151+
"--rm",
152+
"--name",
153+
container_name,
154+
"-p",
155+
"9944:9944",
156+
"-p",
157+
"9945:9945",
158+
image_name,
159+
params,
160+
]
161+
162+
try_start_docker()
163+
164+
# Start container
165+
with subprocess.Popen(
166+
cmds,
167+
stdout=subprocess.PIPE,
168+
stderr=subprocess.PIPE,
169+
text=True,
170+
start_new_session=True,
171+
) as process:
172+
try:
173+
substrate = None
174+
try:
175+
pattern = re.compile(r"Imported #1")
176+
wait_for_node_start(process, pattern, int(time.time()))
177+
except TimeoutError:
178+
raise
179+
180+
result = subprocess.run(
181+
["docker", "ps", "-q", "-f", f"name={container_name}"],
182+
capture_output=True,
183+
text=True,
184+
)
185+
if not result.stdout.strip():
186+
raise RuntimeError("Docker container failed to start.")
187+
188+
substrate = AsyncSubstrateInterface(url="ws://127.0.0.1:9944")
189+
yield substrate
190+
191+
finally:
192+
try:
193+
if substrate:
194+
substrate.close()
195+
except Exception:
196+
pass
197+
198+
try:
199+
subprocess.run(["docker", "kill", container_name])
200+
process.wait(timeout=10)
201+
except subprocess.TimeoutExpired:
202+
os.killpg(os.getpgid(process.pid), signal.SIGKILL)
203+
204+
75205
@pytest.fixture(scope="function")
76206
def wallet_setup():
77207
wallet_paths = []

tests/e2e_tests/test_senate.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -216,17 +216,17 @@ def test_senate(local_chain, wallet_setup):
216216
proposals_after_nay_output = proposals_after_nay.stdout.splitlines()
217217

218218
# Total Ayes to remain 1
219-
proposals_after_nay_output[9].split()[2] == "1"
219+
assert proposals_after_nay_output[9].split()[2] == "1"
220220

221221
# Total Nays increased to 1
222-
proposals_after_nay_output[9].split()[4] == "1"
222+
assert proposals_after_nay_output[9].split()[4] == "1"
223223

224224
# Assert Alice has voted Nay
225-
proposals_after_nay_output[10].split()[0].strip(
225+
assert proposals_after_nay_output[10].split()[0].strip(
226226
":"
227227
) == wallet_alice.hotkey.ss58_address
228228

229229
# Assert vote casted as Nay
230-
proposals_after_nay_output[9].split()[1] == "Nay"
230+
assert proposals_after_nay_output[10].split()[1] == "Nay"
231231

232232
print("✅ Passed senate commands")

tests/e2e_tests/test_staking_sudo.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import re
2-
import time
32

43
from bittensor_cli.src.bittensor.balances import Balance
54

tests/e2e_tests/test_wallet_creations.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -486,7 +486,7 @@ def test_wallet_balance_all(local_chain, wallet_setup, capfd):
486486
wallet_name = f"test_wallet_{i}"
487487
wallet_names.append(wallet_name)
488488

489-
result = exec_command(
489+
exec_command(
490490
command="wallet",
491491
sub_command="new-coldkey",
492492
extra_args=[

0 commit comments

Comments
 (0)