From 4ffdb11ed655df81d361bc85b46ecfcbefd12dd6 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Tue, 4 Nov 2025 19:15:36 +0200 Subject: [PATCH 01/12] Wrong check during Websocket shutdown. --- async_substrate_interface/async_substrate.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/async_substrate_interface/async_substrate.py b/async_substrate_interface/async_substrate.py index b9c2429..ca36476 100644 --- a/async_substrate_interface/async_substrate.py +++ b/async_substrate_interface/async_substrate.py @@ -793,7 +793,7 @@ async def _handler(self, ws: ClientConnection) -> Union[None, Exception]: async def __aexit__(self, exc_type, exc_val, exc_tb): if self.shutdown_timer is not None: - if not self.state != State.CONNECTING: + if self.state != State.CONNECTING: if self._exit_task is not None: self._exit_task.cancel() try: @@ -1005,6 +1005,8 @@ async def retrieve(self, item_id: str) -> Optional[dict]: class AsyncSubstrateInterface(SubstrateMixin): + ws: "Websocket" + def __init__( self, url: str, From 61f3a401ced2e92dcca47e05bdf6f80c21569299 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Tue, 4 Nov 2025 19:17:10 +0200 Subject: [PATCH 02/12] Safer subscription removal. --- async_substrate_interface/async_substrate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/async_substrate_interface/async_substrate.py b/async_substrate_interface/async_substrate.py index ca36476..59d4a85 100644 --- a/async_substrate_interface/async_substrate.py +++ b/async_substrate_interface/async_substrate.py @@ -952,7 +952,7 @@ async def unsubscribe( original_id = get_next_id() while original_id in self._in_use_ids: original_id = get_next_id() - del self._received_subscriptions[subscription_id] + self._received_subscriptions.pop(subscription_id, None) to_send = { "jsonrpc": "2.0", From 694aa11a1a1715eecb9692cb18baf0ed4e05c287 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Tue, 4 Nov 2025 19:54:08 +0200 Subject: [PATCH 03/12] Maybe better non-polling --- async_substrate_interface/async_substrate.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/async_substrate_interface/async_substrate.py b/async_substrate_interface/async_substrate.py index 59d4a85..d38c1db 100644 --- a/async_substrate_interface/async_substrate.py +++ b/async_substrate_interface/async_substrate.py @@ -974,11 +974,13 @@ async def retrieve(self, item_id: str) -> Optional[dict]: """ item: Optional[asyncio.Future] = self._received.get(item_id) if item is not None: - if item.done(): - self.max_subscriptions.release() - res = item.result() - del self._received[item_id] - return res + # For regular requests, await the Future directly instead of polling + if not item.done(): + await item + self.max_subscriptions.release() + res = item.result() + del self._received[item_id] + return res else: try: subscription = self._received_subscriptions[item_id].get_nowait() From f20a46021e778466c4ae7cb654aee47d822e8e69 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Tue, 4 Nov 2025 20:20:07 +0200 Subject: [PATCH 04/12] Remove double sleep --- async_substrate_interface/async_substrate.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/async_substrate_interface/async_substrate.py b/async_substrate_interface/async_substrate.py index d38c1db..59d4a85 100644 --- a/async_substrate_interface/async_substrate.py +++ b/async_substrate_interface/async_substrate.py @@ -974,13 +974,11 @@ async def retrieve(self, item_id: str) -> Optional[dict]: """ item: Optional[asyncio.Future] = self._received.get(item_id) if item is not None: - # For regular requests, await the Future directly instead of polling - if not item.done(): - await item - self.max_subscriptions.release() - res = item.result() - del self._received[item_id] - return res + if item.done(): + self.max_subscriptions.release() + res = item.result() + del self._received[item_id] + return res else: try: subscription = self._received_subscriptions[item_id].get_nowait() From 89544ada863334965b7086f3e0df3d773ad4005e Mon Sep 17 00:00:00 2001 From: bdhimes Date: Wed, 12 Nov 2025 16:19:31 +0200 Subject: [PATCH 05/12] Remove double sleep --- async_substrate_interface/async_substrate.py | 1 - 1 file changed, 1 deletion(-) diff --git a/async_substrate_interface/async_substrate.py b/async_substrate_interface/async_substrate.py index 59d4a85..dc3f32b 100644 --- a/async_substrate_interface/async_substrate.py +++ b/async_substrate_interface/async_substrate.py @@ -1000,7 +1000,6 @@ async def retrieve(self, item_id: str) -> Optional[dict]: elif isinstance((e := self._send_recv_task.result()), Exception): logger.exception(f"Websocket sending exception: {e}") raise e - await asyncio.sleep(0.01) return None From 8d78fdec20713b7e890b119fc1ba654b6737547b Mon Sep 17 00:00:00 2001 From: bdhimes Date: Wed, 12 Nov 2025 16:41:20 +0200 Subject: [PATCH 06/12] Reset attempts at ws shutdown --- async_substrate_interface/async_substrate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/async_substrate_interface/async_substrate.py b/async_substrate_interface/async_substrate.py index dc3f32b..81ed1df 100644 --- a/async_substrate_interface/async_substrate.py +++ b/async_substrate_interface/async_substrate.py @@ -802,6 +802,7 @@ async def __aexit__(self, exc_type, exc_val, exc_tb): pass if self.ws is not None: self._exit_task = asyncio.create_task(self._exit_with_timer()) + self._attempts = 0 async def _exit_with_timer(self): """ From 5e7230f8d9cc6f9d08ce792c8b9966883d4bf455 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Wed, 12 Nov 2025 17:27:19 +0200 Subject: [PATCH 07/12] Updates btcli runner --- .github/workflows/check-btcli-tests.yml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/check-btcli-tests.yml b/.github/workflows/check-btcli-tests.yml index 89552d7..55f7bc0 100644 --- a/.github/workflows/check-btcli-tests.yml +++ b/.github/workflows/check-btcli-tests.yml @@ -166,9 +166,15 @@ jobs: source ${{ github.workspace }}/venv/bin/activate git checkout staging git fetch origin staging - python3 -m pip install --upgrade pip uv - uv pip install '.[dev]' - uv pip install pytest + + - name: Install uv + uses: astral-sh/setup-uv@v4 + with: + python-version: 3.13 + + - name: Install dependencies + run: | + uv sync --extras dev --dev - name: Clone async-substrate-interface repo run: git clone https://github.com/opentensor/async-substrate-interface.git @@ -249,7 +255,7 @@ jobs: pip uninstall async-substrate-interface -y uv pip install . - - name: Run SDK unit tests + - name: Run BTCLI unit tests run: | source ${{ github.workspace }}/venv/bin/activate pytest ${{ github.workspace }}/btcli/tests/unit_tests \ No newline at end of file From b2d812936b3c8820f3e3272778236d47a53bce71 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Wed, 12 Nov 2025 17:29:04 +0200 Subject: [PATCH 08/12] Updates btcli runner --- .github/workflows/check-btcli-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check-btcli-tests.yml b/.github/workflows/check-btcli-tests.yml index 55f7bc0..6306e80 100644 --- a/.github/workflows/check-btcli-tests.yml +++ b/.github/workflows/check-btcli-tests.yml @@ -253,7 +253,7 @@ jobs: run: | source ${{ github.workspace }}/venv/bin/activate pip uninstall async-substrate-interface -y - uv pip install . + uv pip install ".[dev]" - name: Run BTCLI unit tests run: | From bafa7c4cb273a8ba8607d9d216e2945577ec067a Mon Sep 17 00:00:00 2001 From: bdhimes Date: Wed, 12 Nov 2025 17:32:03 +0200 Subject: [PATCH 09/12] Typo --- .github/workflows/check-btcli-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check-btcli-tests.yml b/.github/workflows/check-btcli-tests.yml index 6306e80..4740c86 100644 --- a/.github/workflows/check-btcli-tests.yml +++ b/.github/workflows/check-btcli-tests.yml @@ -174,7 +174,7 @@ jobs: - name: Install dependencies run: | - uv sync --extras dev --dev + uv sync --extra dev --dev - name: Clone async-substrate-interface repo run: git clone https://github.com/opentensor/async-substrate-interface.git From dc34bd535815c0495c079622566421b8ab78c101 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Wed, 12 Nov 2025 18:05:44 +0200 Subject: [PATCH 10/12] idk --- .github/workflows/check-btcli-tests.yml | 83 ++++++++++++------------- 1 file changed, 39 insertions(+), 44 deletions(-) diff --git a/.github/workflows/check-btcli-tests.yml b/.github/workflows/check-btcli-tests.yml index 4740c86..f49102f 100644 --- a/.github/workflows/check-btcli-tests.yml +++ b/.github/workflows/check-btcli-tests.yml @@ -61,10 +61,10 @@ jobs: echo "Current labels: $LABELS" if echo "$LABELS" | grep -q "run-bittensor-cli-tests"; then echo "run-cli-tests=true" >> $GITHUB_ENV - echo "::set-output name=run-cli-tests::true" + echo "run-cli-tests=true" >> $GITHUB_OUTPUT else echo "run-cli-tests=false" >> $GITHUB_ENV - echo "::set-output name=run-cli-tests::false" + echo "run-cli-tests=false" >> $GITHUB_OUTPUT fi env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -91,7 +91,7 @@ jobs: id: get-tests run: | test_files=$(find ${{ github.workspace }}/btcli/tests/e2e_tests -name "test*.py" | jq -R -s -c 'split("\n") | map(select(. != ""))') - echo "::set-output name=test-files::$test_files" + echo "test-files=$test_files" >> $GITHUB_OUTPUT shell: bash pull-docker-image: @@ -147,51 +147,47 @@ jobs: - name: Check-out repository uses: actions/checkout@v4 - - name: Install dependencies + - name: Install system dependencies run: | sudo apt-get update && sudo apt-get install -y clang curl libssl-dev llvm libudev-dev protobuf-compiler - - name: Create Python virtual environment - working-directory: ${{ github.workspace }} - run: python3 -m venv ${{ github.workspace }}/venv + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.13' + + - name: Install uv + uses: astral-sh/setup-uv@v4 - name: Clone Bittensor CLI repo - working-directory: ${{ github.workspace }} run: git clone https://github.com/opentensor/btcli.git - - name: Setup Bittensor-cli from cloned repo + - name: Checkout btcli staging branch working-directory: ${{ github.workspace }}/btcli run: | - source ${{ github.workspace }}/venv/bin/activate git checkout staging git fetch origin staging - - name: Install uv - uses: astral-sh/setup-uv@v4 - with: - python-version: 3.13 - - - name: Install dependencies - run: | - uv sync --extra dev --dev + - name: Install btcli dependencies + working-directory: ${{ github.workspace }}/btcli + run: uv pip install --system '.[dev]' - name: Clone async-substrate-interface repo run: git clone https://github.com/opentensor/async-substrate-interface.git - - name: Checkout PR async-substrate-interface repo + - name: Checkout PR branch in async-substrate-interface working-directory: ${{ github.workspace }}/async-substrate-interface run: | git fetch origin ${{ github.event.pull_request.head.ref }} git checkout ${{ github.event.pull_request.head.ref }} echo "Current branch: $(git rev-parse --abbrev-ref HEAD)" - - name: Install async-substrate-interface package + - name: Install async-substrate-interface with dev dependencies working-directory: ${{ github.workspace }}/async-substrate-interface run: | - source ${{ github.workspace }}/venv/bin/activate - python3 -m pip uninstall async-substrate-interface -y - uv pip install . + uv pip uninstall --system async-substrate-interface || true + uv pip install --system '.[dev]' - name: Download Cached Docker Image uses: actions/download-artifact@v4 @@ -201,10 +197,8 @@ jobs: - name: Load Docker Image run: docker load -i subtensor-localnet.tar - - name: Run tests - run: | - source ${{ github.workspace }}/venv/bin/activate - pytest ${{ matrix.test-file }} -s + - name: Run e2e tests + run: pytest ${{ matrix.test-file }} -s run-unit-test: @@ -216,46 +210,47 @@ jobs: - name: Check-out repository uses: actions/checkout@v4 - - name: Install dependencies + - name: Install system dependencies run: | sudo apt-get update && sudo apt-get install -y clang curl libssl-dev llvm libudev-dev protobuf-compiler - - name: Create Python virtual environment - working-directory: ${{ github.workspace }} - run: python3 -m venv venv + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.13' + + - name: Install uv + uses: astral-sh/setup-uv@v4 - name: Clone Bittensor CLI repo - working-directory: ${{ github.workspace }} run: git clone https://github.com/opentensor/btcli.git - - name: Setup Bittensor SDK from cloned repo + - name: Checkout btcli staging branch working-directory: ${{ github.workspace }}/btcli run: | - source ${{ github.workspace }}/venv/bin/activate git checkout staging git fetch origin staging - python3 -m pip install --upgrade pip uv - uv pip install '.[dev]' + + - name: Install btcli dependencies + working-directory: ${{ github.workspace }}/btcli + run: uv pip install --system '.[dev]' - name: Clone async-substrate-interface repo run: git clone https://github.com/opentensor/async-substrate-interface.git - - name: Checkout PR branch in async-substrate-interface repo + - name: Checkout PR branch in async-substrate-interface working-directory: ${{ github.workspace }}/async-substrate-interface run: | git fetch origin ${{ github.event.pull_request.head.ref }} git checkout ${{ github.event.pull_request.head.ref }} echo "Current branch: $(git rev-parse --abbrev-ref HEAD)" - - name: Install /async-substrate-interface package + - name: Install async-substrate-interface with dev dependencies working-directory: ${{ github.workspace }}/async-substrate-interface run: | - source ${{ github.workspace }}/venv/bin/activate - pip uninstall async-substrate-interface -y - uv pip install ".[dev]" + uv pip uninstall --system async-substrate-interface || true + uv pip install --system '.[dev]' - name: Run BTCLI unit tests - run: | - source ${{ github.workspace }}/venv/bin/activate - pytest ${{ github.workspace }}/btcli/tests/unit_tests \ No newline at end of file + run: pytest ${{ github.workspace }}/btcli/tests/unit_tests \ No newline at end of file From 1c565b40943d9aa0761e540a39aa54df6f2b4c68 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Wed, 12 Nov 2025 18:19:13 +0200 Subject: [PATCH 11/12] Improve test --- tests/e2e_tests/test_substrate_addons.py | 67 +++++++++++++++++++++++- 1 file changed, 65 insertions(+), 2 deletions(-) diff --git a/tests/e2e_tests/test_substrate_addons.py b/tests/e2e_tests/test_substrate_addons.py index bd91c99..23c3c34 100644 --- a/tests/e2e_tests/test_substrate_addons.py +++ b/tests/e2e_tests/test_substrate_addons.py @@ -1,5 +1,6 @@ import subprocess import time +import sys import pytest @@ -13,6 +14,61 @@ from tests.helpers.settings import ARCHIVE_ENTRYPOINT, LATENT_LITE_ENTRYPOINT +def wait_for_output(process, target_string, timeout=60): + """ + Wait for a specific string to appear in the subprocess stdout. + + Args: + process: subprocess.Popen object + target_string: String to wait for in stdout + timeout: Maximum time to wait in seconds + + Returns: + bool: True if string was found, False if timeout occurred + """ + import time + start_time = time.time() + + # Make stdout non-blocking on Unix systems + if sys.platform != 'win32': + import fcntl + import os + flags = fcntl.fcntl(process.stdout, fcntl.F_GETFL) + fcntl.fcntl(process.stdout, fcntl.F_SETFL, flags | os.O_NONBLOCK) + + buffer = "" + while time.time() - start_time < timeout: + try: + # Read available data + chunk = process.stdout.read(1024) + if chunk: + chunk_str = chunk.decode('utf-8', errors='ignore') + buffer += chunk_str + print(chunk_str, end='', flush=True) # Echo output for visibility + + if target_string in buffer: + return True + else: + # No data available, sleep briefly + time.sleep(0.1) + except (BlockingIOError, TypeError): + # No data available yet + time.sleep(0.1) + + # Check if process has terminated + if process.poll() is not None: + # Process ended, read remaining output + remaining = process.stdout.read() + if remaining: + remaining_str = remaining.decode('utf-8', errors='ignore') + print(remaining_str, end='', flush=True) + if target_string in remaining_str: + return True + return False + + return False + + @pytest.fixture(scope="function") def docker_containers(): processes = ( @@ -39,7 +95,10 @@ def single_local_chain(): def test_retry_sync_substrate(single_local_chain): - time.sleep(10) + # Wait for the Docker container to be ready + if not wait_for_output(single_local_chain.process, "Imported #1", timeout=60): + raise TimeoutError("Docker container did not start properly - 'Imported #1' not found in output") + with RetrySyncSubstrate( single_local_chain.uri, fallback_chains=[LATENT_LITE_ENTRYPOINT] ) as substrate: @@ -58,7 +117,11 @@ def test_retry_sync_substrate(single_local_chain): "It does run locally, however." ) def test_retry_sync_substrate_max_retries(docker_containers): - time.sleep(10) + # Wait for both Docker containers to be ready + for i, container in enumerate(docker_containers): + if not wait_for_output(container.process, "Imported #1", timeout=60): + raise TimeoutError(f"Docker container {i} did not start properly - 'Imported #1' not found in output") + with RetrySyncSubstrate( docker_containers[0].uri, fallback_chains=[docker_containers[1].uri] ) as substrate: From 4ee7a0fea3b797fe74bb6572c9c542a6d1896052 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Wed, 12 Nov 2025 18:26:38 +0200 Subject: [PATCH 12/12] Maybe this will work --- tests/e2e_tests/test_substrate_addons.py | 25 ++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/tests/e2e_tests/test_substrate_addons.py b/tests/e2e_tests/test_substrate_addons.py index 23c3c34..c187764 100644 --- a/tests/e2e_tests/test_substrate_addons.py +++ b/tests/e2e_tests/test_substrate_addons.py @@ -27,12 +27,14 @@ def wait_for_output(process, target_string, timeout=60): bool: True if string was found, False if timeout occurred """ import time + start_time = time.time() # Make stdout non-blocking on Unix systems - if sys.platform != 'win32': + if sys.platform != "win32": import fcntl import os + flags = fcntl.fcntl(process.stdout, fcntl.F_GETFL) fcntl.fcntl(process.stdout, fcntl.F_SETFL, flags | os.O_NONBLOCK) @@ -42,9 +44,9 @@ def wait_for_output(process, target_string, timeout=60): # Read available data chunk = process.stdout.read(1024) if chunk: - chunk_str = chunk.decode('utf-8', errors='ignore') + chunk_str = chunk.decode("utf-8", errors="ignore") buffer += chunk_str - print(chunk_str, end='', flush=True) # Echo output for visibility + print(chunk_str, end="", flush=True) # Echo output for visibility if target_string in buffer: return True @@ -60,8 +62,8 @@ def wait_for_output(process, target_string, timeout=60): # Process ended, read remaining output remaining = process.stdout.read() if remaining: - remaining_str = remaining.decode('utf-8', errors='ignore') - print(remaining_str, end='', flush=True) + remaining_str = remaining.decode("utf-8", errors="ignore") + print(remaining_str, end="", flush=True) if target_string in remaining_str: return True return False @@ -97,7 +99,9 @@ def single_local_chain(): def test_retry_sync_substrate(single_local_chain): # Wait for the Docker container to be ready if not wait_for_output(single_local_chain.process, "Imported #1", timeout=60): - raise TimeoutError("Docker container did not start properly - 'Imported #1' not found in output") + raise TimeoutError( + "Docker container did not start properly - 'Imported #1' not found in output" + ) with RetrySyncSubstrate( single_local_chain.uri, fallback_chains=[LATENT_LITE_ENTRYPOINT] @@ -111,16 +115,13 @@ def test_retry_sync_substrate(single_local_chain): time.sleep(2) -@pytest.mark.skip( - "There's an issue with this running in the GitHub runner, " - "where it seemingly cannot connect to the docker container. " - "It does run locally, however." -) def test_retry_sync_substrate_max_retries(docker_containers): # Wait for both Docker containers to be ready for i, container in enumerate(docker_containers): if not wait_for_output(container.process, "Imported #1", timeout=60): - raise TimeoutError(f"Docker container {i} did not start properly - 'Imported #1' not found in output") + raise TimeoutError( + f"Docker container {i} did not start properly - 'Imported #1' not found in output" + ) with RetrySyncSubstrate( docker_containers[0].uri, fallback_chains=[docker_containers[1].uri]