Skip to content

Commit ed0c6d6

Browse files
committed
Merge remote-tracking branch 'origin/staging' into feat/thewhaleking/add-archive-node
2 parents f1d5de3 + e53f27f commit ed0c6d6

File tree

7 files changed

+320
-104
lines changed

7 files changed

+320
-104
lines changed
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
name: Run Tests
2+
3+
on:
4+
push:
5+
branches: [main, staging]
6+
pull_request:
7+
branches: [main, staging]
8+
workflow_dispatch:
9+
10+
jobs:
11+
find-tests:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- name: Check-out repository
15+
uses: actions/checkout@v4
16+
17+
- name: Find test files
18+
id: get-tests
19+
run: |
20+
test_files=$(find tests -name "test*.py" | jq -R -s -c 'split("\n") | map(select(. != ""))')
21+
echo "::set-output name=test-files::$test_files"
22+
23+
pull-docker-image:
24+
runs-on: ubuntu-latest
25+
steps:
26+
- name: Log in to GitHub Container Registry
27+
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u $GITHUB_ACTOR --password-stdin
28+
29+
- name: Pull Docker Image
30+
run: docker pull ghcr.io/opentensor/subtensor-localnet:devnet-ready
31+
32+
- name: Save Docker Image to Cache
33+
run: docker save -o subtensor-localnet.tar ghcr.io/opentensor/subtensor-localnet:devnet-ready
34+
35+
- name: Upload Docker Image as Artifact
36+
uses: actions/upload-artifact@v4
37+
with:
38+
name: subtensor-localnet
39+
path: subtensor-localnet.tar
40+
41+
run-unit-tests:
42+
name: ${{ matrix.test-file }} / Python ${{ matrix.python-version }}
43+
needs:
44+
- find-tests
45+
- pull-docker-image
46+
runs-on: ubuntu-latest
47+
timeout-minutes: 30
48+
strategy:
49+
fail-fast: false
50+
max-parallel: 32
51+
matrix:
52+
test-file: ${{ fromJson(needs.find-tests.outputs.test-files) }}
53+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
54+
55+
steps:
56+
- name: Check-out repository
57+
uses: actions/checkout@v4
58+
59+
- name: Install uv
60+
uses: astral-sh/setup-uv@v4
61+
with:
62+
python-version: ${{ matrix.python-version }}
63+
64+
- name: Install dependencies
65+
run: |
66+
uv venv .venv
67+
source .venv/bin/activate
68+
uv pip install .[dev]
69+
70+
- name: Download Docker Image
71+
uses: actions/download-artifact@v4
72+
with:
73+
name: subtensor-localnet
74+
75+
- name: Load Docker Image
76+
run: docker load -i subtensor-localnet.tar
77+
78+
- name: Run pytest
79+
run: |
80+
source .venv/bin/activate
81+
uv run pytest ${{ matrix.test-file }} -v -s

async_substrate_interface/async_substrate.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,9 @@ def __init__(
539539
"You are instantiating the AsyncSubstrateInterface Websocket outside of an event loop. "
540540
"Verify this is intended."
541541
)
542+
# default value for in case there's no running asyncio loop
543+
# this really doesn't matter in most cases, as it's only used for comparison on the first call to
544+
# see how long it's been since the last call
542545
now = 0.0
543546
self.last_received = now
544547
self.last_sent = now
@@ -730,6 +733,7 @@ def __init__(
730733
)
731734
else:
732735
self.ws = AsyncMock(spec=Websocket)
736+
733737
self._lock = asyncio.Lock()
734738
self.config = {
735739
"use_remote_preset": use_remote_preset,

async_substrate_interface/sync_substrate.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2504,13 +2504,13 @@ def runtime_call(
25042504
Returns:
25052505
ScaleType from the runtime call
25062506
"""
2507-
self.init_runtime(block_hash=block_hash)
2507+
runtime = self.init_runtime(block_hash=block_hash)
25082508

25092509
if params is None:
25102510
params = {}
25112511

25122512
try:
2513-
metadata_v15_value = self.runtime.metadata_v15.value()
2513+
metadata_v15_value = runtime.metadata_v15.value()
25142514

25152515
apis = {entry["name"]: entry for entry in metadata_v15_value["apis"]}
25162516
api_entry = apis[api]

tests/unit_tests/asyncio/test_substrate_interface.py

Lines changed: 0 additions & 69 deletions
This file was deleted.
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
from unittest.mock import AsyncMock, MagicMock
2+
3+
import pytest
4+
from websockets.exceptions import InvalidURI
5+
6+
from async_substrate_interface.async_substrate import AsyncSubstrateInterface
7+
from async_substrate_interface.types import ScaleObj
8+
9+
10+
@pytest.mark.asyncio
11+
async def test_invalid_url_raises_exception():
12+
"""Test that invalid URI raises an InvalidURI exception."""
13+
async_substrate = AsyncSubstrateInterface("non_existent_entry_point")
14+
with pytest.raises(InvalidURI):
15+
await async_substrate.initialize()
16+
17+
with pytest.raises(InvalidURI):
18+
async with AsyncSubstrateInterface(
19+
"non_existent_entry_point"
20+
) as async_substrate:
21+
pass
22+
23+
24+
@pytest.mark.asyncio
25+
async def test_runtime_call(monkeypatch):
26+
substrate = AsyncSubstrateInterface("ws://localhost", _mock=True)
27+
28+
fake_runtime = MagicMock()
29+
fake_metadata_v15 = MagicMock()
30+
fake_metadata_v15.value.return_value = {
31+
"apis": [
32+
{
33+
"name": "SubstrateApi",
34+
"methods": [
35+
{
36+
"name": "SubstrateMethod",
37+
"inputs": [],
38+
"output": "1",
39+
},
40+
],
41+
},
42+
],
43+
"types": {
44+
"types": [
45+
{
46+
"id": "1",
47+
"type": {
48+
"path": ["Vec"],
49+
"def": {"sequence": {"type": "4"}},
50+
},
51+
},
52+
]
53+
},
54+
}
55+
fake_runtime.metadata_v15 = fake_metadata_v15
56+
substrate.init_runtime = AsyncMock(return_value=fake_runtime)
57+
58+
# Patch encode_scale (should not be called in this test since no inputs)
59+
substrate.encode_scale = AsyncMock()
60+
61+
# Patch decode_scale to produce a dummy value
62+
substrate.decode_scale = AsyncMock(return_value="decoded_result")
63+
64+
# Patch RPC request with correct behavior
65+
substrate.rpc_request = AsyncMock(
66+
side_effect=lambda method, params: {
67+
"result": "0x00" if method == "state_call" else {"parentHash": "0xDEADBEEF"}
68+
}
69+
)
70+
71+
# Patch get_block_runtime_info
72+
substrate.get_block_runtime_info = AsyncMock(return_value={"specVersion": "1"})
73+
74+
# Run the call
75+
result = await substrate.runtime_call(
76+
"SubstrateApi",
77+
"SubstrateMethod",
78+
)
79+
80+
# Validate the result is wrapped in ScaleObj
81+
assert isinstance(result, ScaleObj)
82+
assert result.value == "decoded_result"
83+
84+
# Check decode_scale called correctly
85+
substrate.decode_scale.assert_called_once_with("scale_info::1", b"\x00")
86+
87+
# encode_scale should not be called since no inputs
88+
substrate.encode_scale.assert_not_called()
89+
90+
# Check RPC request called for the state_call
91+
substrate.rpc_request.assert_any_call(
92+
"state_call", ["SubstrateApi_SubstrateMethod", "", None]
93+
)
Lines changed: 53 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,74 @@
1-
import unittest.mock
1+
from unittest.mock import MagicMock
22

33
from async_substrate_interface.sync_substrate import SubstrateInterface
44
from async_substrate_interface.types import ScaleObj
55

66

77
def test_runtime_call(monkeypatch):
8-
monkeypatch.setattr(
9-
"async_substrate_interface.sync_substrate.connect", unittest.mock.MagicMock()
10-
)
11-
12-
substrate = SubstrateInterface(
13-
"ws://localhost",
14-
_mock=True,
15-
)
16-
substrate._metadata = unittest.mock.Mock()
17-
substrate.metadata_v15 = unittest.mock.Mock(
18-
**{
19-
"value.return_value": {
20-
"apis": [
8+
substrate = SubstrateInterface("ws://localhost", _mock=True)
9+
fake_runtime = MagicMock()
10+
fake_metadata_v15 = MagicMock()
11+
fake_metadata_v15.value.return_value = {
12+
"apis": [
13+
{
14+
"name": "SubstrateApi",
15+
"methods": [
2116
{
22-
"name": "SubstrateApi",
23-
"methods": [
24-
{
25-
"name": "SubstrateMethod",
26-
"inputs": [],
27-
"output": "1",
28-
},
29-
],
17+
"name": "SubstrateMethod",
18+
"inputs": [],
19+
"output": "1",
3020
},
3121
],
3222
},
33-
}
34-
)
35-
substrate.rpc_request = unittest.mock.Mock(
36-
return_value={
37-
"result": "0x00",
23+
],
24+
"types": {
25+
"types": [
26+
{
27+
"id": "1",
28+
"type": {
29+
"path": ["Vec"],
30+
"def": {"sequence": {"type": "4"}},
31+
},
32+
},
33+
]
3834
},
35+
}
36+
fake_runtime.metadata_v15 = fake_metadata_v15
37+
substrate.init_runtime = MagicMock(return_value=fake_runtime)
38+
39+
# Patch encode_scale (should not be called in this test since no inputs)
40+
substrate.encode_scale = MagicMock()
41+
42+
# Patch decode_scale to produce a dummy value
43+
substrate.decode_scale = MagicMock(return_value="decoded_result")
44+
45+
# Patch RPC request with correct behavior
46+
substrate.rpc_request = MagicMock(
47+
side_effect=lambda method, params: {
48+
"result": "0x00" if method == "state_call" else {"parentHash": "0xDEADBEEF"}
49+
}
3950
)
40-
substrate.decode_scale = unittest.mock.Mock()
4151

52+
# Patch get_block_runtime_info
53+
substrate.get_block_runtime_info = MagicMock(return_value={"specVersion": "1"})
54+
55+
# Run the call
4256
result = substrate.runtime_call(
4357
"SubstrateApi",
4458
"SubstrateMethod",
4559
)
4660

61+
# Validate the result is wrapped in ScaleObj
4762
assert isinstance(result, ScaleObj)
48-
assert result.value is substrate.decode_scale.return_value
63+
assert result.value == "decoded_result"
4964

50-
substrate.rpc_request.assert_called_once_with(
51-
"state_call",
52-
["SubstrateApi_SubstrateMethod", "", None],
53-
)
65+
# Check decode_scale called correctly
5466
substrate.decode_scale.assert_called_once_with("scale_info::1", b"\x00")
67+
68+
# encode_scale should not be called since no inputs
69+
substrate.encode_scale.assert_not_called()
70+
71+
# Check RPC request called for the state_call
72+
substrate.rpc_request.assert_any_call(
73+
"state_call", ["SubstrateApi_SubstrateMethod", "", None]
74+
)

0 commit comments

Comments
 (0)