diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml new file mode 100644 index 000000000..4e5d34e41 --- /dev/null +++ b/.github/workflows/unit-tests.yml @@ -0,0 +1,46 @@ +name: Unit Tests + +concurrency: + group: unit-tests-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +permissions: + pull-requests: read + contents: read + +on: + push: + branches: [main, development, staging] + + pull_request: + branches: [main, development, staging] + types: [opened, synchronize, reopened, ready_for_review] + + workflow_dispatch: + +jobs: + unit-tests: + name: Unit Tests / Python ${{ matrix.python-version }} + runs-on: ubuntu-latest + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }} + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + steps: + - name: Check-out repository + uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + uv sync --all-extras + + - name: Run unit tests + run: | + uv run pytest tests/unit_tests -v --tb=short diff --git a/bittensor_cli/src/commands/crowd/update.py b/bittensor_cli/src/commands/crowd/update.py index 41d3efdee..6abefed55 100644 --- a/bittensor_cli/src/commands/crowd/update.py +++ b/bittensor_cli/src/commands/crowd/update.py @@ -1,6 +1,6 @@ import asyncio import json -from typing import Optional +from typing import Optional, Union from bittensor_wallet import Wallet from rich.prompt import IntPrompt, FloatPrompt @@ -219,7 +219,7 @@ async def update_crowdloan( cap = candidate_cap break - value: Optional[Balance | int] = None + value: Optional[Union[Balance, int]] = None call_function: Optional[str] = None param_name: Optional[str] = None update_type: Optional[str] = None diff --git a/bittensor_cli/src/commands/crowd/view.py b/bittensor_cli/src/commands/crowd/view.py index ba7657dd5..9a248d18f 100644 --- a/bittensor_cli/src/commands/crowd/view.py +++ b/bittensor_cli/src/commands/crowd/view.py @@ -19,7 +19,7 @@ ) -def _shorten(account: str | None) -> str: +def _shorten(account: Optional[str]) -> str: if not account: return "-" return f"{account[:6]}…{account[-6:]}" diff --git a/tests/e2e_tests/test_axon.py b/tests/e2e_tests/test_axon.py index ea3604110..6d9277570 100644 --- a/tests/e2e_tests/test_axon.py +++ b/tests/e2e_tests/test_axon.py @@ -11,7 +11,7 @@ import pytest import re -from tests.e2e_tests.utils import execute_turn_off_hyperparam_freeze_window +from ..e2e_tests.utils import execute_turn_off_hyperparam_freeze_window @pytest.mark.parametrize("local_chain", [None], indirect=True) diff --git a/tests/e2e_tests/utils.py b/tests/e2e_tests/utils.py index d46589546..9f65759ce 100644 --- a/tests/e2e_tests/utils.py +++ b/tests/e2e_tests/utils.py @@ -135,7 +135,7 @@ def extract_coldkey_balance( def find_stake_entries( - stake_payload: dict, netuid: int, hotkey_ss58: str | None = None + stake_payload: dict, netuid: int, hotkey_ss58: Optional[str] = None ) -> list[dict]: """ Return stake entries matching a given netuid, optionally scoped to a specific hotkey. diff --git a/tests/unit_tests/test_axon_commands.py b/tests/unit_tests/test_axon_commands.py index 4ed73f9e9..88803d937 100644 --- a/tests/unit_tests/test_axon_commands.py +++ b/tests/unit_tests/test_axon_commands.py @@ -4,6 +4,7 @@ import pytest from unittest.mock import AsyncMock, MagicMock, Mock, patch +from async_substrate_interface import AsyncSubstrateInterface from bittensor_wallet import Wallet from bittensor_cli.src.bittensor.extrinsics.serving import ( @@ -11,6 +12,7 @@ set_axon_extrinsic, ip_to_int, ) +from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface class TestIpToInt: @@ -134,7 +136,8 @@ async def test_reset_axon_unlock_failure(self): @pytest.mark.asyncio async def test_reset_axon_user_cancellation(self): """Test axon reset when user cancels prompt.""" - mock_subtensor = MagicMock() + mock_subtensor = MagicMock(spec=SubtensorInterface) + mock_subtensor.substrate = MagicMock(spec=AsyncSubstrateInterface) mock_wallet = MagicMock(spec=Wallet) mock_wallet.hotkey.ss58_address = ( "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" @@ -145,11 +148,11 @@ async def test_reset_axon_user_cancellation(self): "bittensor_cli.src.bittensor.extrinsics.serving.unlock_key" ) as mock_unlock, patch( - "bittensor_cli.src.bittensor.extrinsics.serving.Confirm" + "bittensor_cli.src.bittensor.extrinsics.serving.confirm_action" ) as mock_confirm, ): mock_unlock.return_value = MagicMock(success=True) - mock_confirm.ask.return_value = False + mock_confirm.return_value = False success, message, ext_id = await reset_axon_extrinsic( subtensor=mock_subtensor, @@ -362,11 +365,11 @@ async def test_set_axon_user_cancellation(self): "bittensor_cli.src.bittensor.extrinsics.serving.unlock_key" ) as mock_unlock, patch( - "bittensor_cli.src.bittensor.extrinsics.serving.Confirm" + "bittensor_cli.src.bittensor.extrinsics.serving.confirm_action" ) as mock_confirm, ): mock_unlock.return_value = MagicMock(success=True) - mock_confirm.ask.return_value = False + mock_confirm.return_value = False success, message, ext_id = await set_axon_extrinsic( subtensor=mock_subtensor, diff --git a/tests/unit_tests/test_cli.py b/tests/unit_tests/test_cli.py index 0f5218de5..60cc10708 100644 --- a/tests/unit_tests/test_cli.py +++ b/tests/unit_tests/test_cli.py @@ -1,6 +1,7 @@ import numpy as np import pytest import typer +from async_substrate_interface import AsyncSubstrateInterface from bittensor_cli.cli import parse_mnemonic, CLIManager from bittensor_cli.src.bittensor.extrinsics.root import ( @@ -9,6 +10,8 @@ ) from unittest.mock import AsyncMock, patch, MagicMock, Mock +from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface + def test_parse_mnemonic(): # standard @@ -58,7 +61,7 @@ async def test_subnet_sets_price_correctly(): assert subnet_info.price == mock_price -@patch("bittensor_cli.cli.Confirm") +@patch("bittensor_cli.cli.confirm_action") @patch("bittensor_cli.cli.console") def test_swap_hotkey_netuid_0_warning_with_prompt(mock_console, mock_confirm): """ @@ -67,7 +70,9 @@ def test_swap_hotkey_netuid_0_warning_with_prompt(mock_console, mock_confirm): """ # Setup cli_manager = CLIManager() - mock_confirm.ask.return_value = False # User declines + cli_manager.subtensor = MagicMock(spec=SubtensorInterface) + cli_manager.subtensor.substrate = MagicMock(spec=AsyncSubstrateInterface) + mock_confirm.return_value = False # User declines # Mock dependencies to prevent actual execution with ( @@ -90,6 +95,7 @@ def test_swap_hotkey_netuid_0_warning_with_prompt(mock_console, mock_confirm): verbose=False, prompt=True, json_output=False, + proxy=None, ) # Assert: Warning was displayed (4 console.print calls for the warning) @@ -104,8 +110,8 @@ def test_swap_hotkey_netuid_0_warning_with_prompt(mock_console, mock_confirm): ) # Assert: User was asked to confirm - mock_confirm.ask.assert_called_once() - confirm_message = mock_confirm.ask.call_args[0][0] + mock_confirm.assert_called_once() + confirm_message = mock_confirm.call_args[0][0] assert "SURE" in confirm_message assert "netuid 0" in confirm_message or "root network" in confirm_message @@ -113,7 +119,7 @@ def test_swap_hotkey_netuid_0_warning_with_prompt(mock_console, mock_confirm): assert result is None -@patch("bittensor_cli.cli.Confirm") +@patch("bittensor_cli.cli.confirm_action") @patch("bittensor_cli.cli.console") def test_swap_hotkey_netuid_0_proceeds_with_confirmation(mock_console, mock_confirm): """ @@ -121,7 +127,9 @@ def test_swap_hotkey_netuid_0_proceeds_with_confirmation(mock_console, mock_conf """ # Setup cli_manager = CLIManager() - mock_confirm.ask.return_value = True # User confirms + cli_manager.subtensor = MagicMock(spec=SubtensorInterface) + cli_manager.subtensor.substrate = MagicMock(spec=AsyncSubstrateInterface) + mock_confirm.return_value = True # User confirms # Mock dependencies with ( @@ -146,10 +154,11 @@ def test_swap_hotkey_netuid_0_proceeds_with_confirmation(mock_console, mock_conf verbose=False, prompt=True, json_output=False, + proxy=None, ) # Assert: Warning was shown and confirmed - mock_confirm.ask.assert_called_once() + mock_confirm.assert_called_once() # Assert: Command execution proceeded mock_run_command.assert_called_once() @@ -186,6 +195,7 @@ def test_swap_hotkey_netuid_0_no_warning_with_no_prompt(mock_console): verbose=False, prompt=False, # No prompt json_output=False, + proxy=None, ) # Assert: No warning messages about netuid 0 @@ -226,6 +236,7 @@ def test_swap_hotkey_netuid_1_no_warning(mock_console): verbose=False, prompt=True, json_output=False, + proxy=None, ) # Assert: No warning messages about netuid 0 @@ -733,12 +744,14 @@ async def test_set_root_weights_fetches_current_weights_with_prompt(): "bittensor_cli.src.bittensor.extrinsics.root.get_current_weights_for_uid" ) as mock_get_current, patch("bittensor_cli.src.bittensor.extrinsics.root.console"), - patch("bittensor_cli.src.bittensor.extrinsics.root.Confirm") as mock_confirm, + patch( + "bittensor_cli.src.bittensor.extrinsics.root.confirm_action" + ) as mock_confirm, ): mock_unlock.return_value = MagicMock(success=True) mock_limits.return_value = (1, 0.5) mock_get_current.return_value = {0: 0.5, 1: 0.3, 2: 0.2} - mock_confirm.ask.return_value = False + mock_confirm.return_value = False netuids = np.array([0, 1, 2], dtype=np.int64) weights = np.array([0.4, 0.3, 0.3], dtype=np.float32) diff --git a/tests/unit_tests/test_subnets_register.py b/tests/unit_tests/test_subnets_register.py index fac87c8ba..0e4c6c962 100644 --- a/tests/unit_tests/test_subnets_register.py +++ b/tests/unit_tests/test_subnets_register.py @@ -2,9 +2,17 @@ Unit tests for subnets register command. """ +from asyncio import Future + import pytest from unittest.mock import AsyncMock, MagicMock, Mock, patch +from async_substrate_interface.async_substrate import ( + AsyncExtrinsicReceipt, + AsyncSubstrateInterface, +) +from async_substrate_interface.utils.storage import StorageKey from bittensor_wallet import Wallet +from scalecodec import GenericCall from bittensor_cli.src.commands.subnets.subnets import register from bittensor_cli.src.bittensor.balances import Balance @@ -100,162 +108,6 @@ async def test_register_subnet_does_not_exist( mock_err_console.print.assert_called_once() assert "does not exist" in str(mock_err_console.print.call_args) - @pytest.mark.asyncio - async def test_register_registration_not_allowed( - self, mock_subtensor_base, mock_wallet - ): - """Test registration fails when registration is not allowed.""" - with patch( - "bittensor_cli.src.commands.subnets.subnets.asyncio.gather" - ) as mock_gather: - mock_gather.return_value = create_gather_result(registration_allowed=False) - - with patch( - "bittensor_cli.src.commands.subnets.subnets.err_console" - ) as mock_err_console: - result = await register( - wallet=mock_wallet, - subtensor=mock_subtensor_base, - netuid=1, - era=None, - json_output=False, - prompt=False, - ) - - assert result is None - mock_err_console.print.assert_called_once() - assert "not allowed" in str(mock_err_console.print.call_args) - - @pytest.mark.asyncio - async def test_register_registration_full(self, mock_subtensor_base, mock_wallet): - """Test registration fails when registration is full for the interval.""" - # registrations_this_interval >= target * 3 - # next_adjustment_block = 900 + 360 = 1260, remaining = 1260 - 1000 = 260 - with patch( - "bittensor_cli.src.commands.subnets.subnets.asyncio.gather" - ) as mock_gather: - mock_gather.return_value = create_gather_result(registrations_current=3) - - with patch( - "bittensor_cli.src.commands.subnets.subnets.err_console" - ) as mock_err_console: - result = await register( - wallet=mock_wallet, - subtensor=mock_subtensor_base, - netuid=1, - era=None, - json_output=False, - prompt=False, - ) - - assert result is None - mock_err_console.print.assert_called_once() - call_str = str(mock_err_console.print.call_args) - assert "full" in call_str - assert ( - "260 blocks" in call_str - ) # remaining_blocks = (900+360) - 1000 = 260 - - @pytest.mark.asyncio - async def test_register_insufficient_balance( - self, mock_subtensor_base, mock_wallet - ): - """Test registration fails when balance is insufficient.""" - with patch( - "bittensor_cli.src.commands.subnets.subnets.asyncio.gather" - ) as mock_gather: - mock_gather.side_effect = create_gather_side_effect( - recycle_rao=10000000000, balance_tao=5.0 - ) - - with patch( - "bittensor_cli.src.commands.subnets.subnets.err_console" - ) as mock_err_console: - result = await register( - wallet=mock_wallet, - subtensor=mock_subtensor_base, - netuid=1, - era=None, - json_output=False, - prompt=False, - ) - - assert result is None - mock_err_console.print.assert_called_once() - assert "Insufficient balance" in str(mock_err_console.print.call_args) - - @pytest.mark.asyncio - async def test_register_success_netuid_0(self, mock_subtensor_base, mock_wallet): - """Test successful registration to netuid 0 (root network).""" - with ( - patch( - "bittensor_cli.src.commands.subnets.subnets.asyncio.gather" - ) as mock_gather, - patch( - "bittensor_cli.src.commands.subnets.subnets.root_register_extrinsic" - ) as mock_root_register, - patch( - "bittensor_cli.src.commands.subnets.subnets.err_console" - ) as mock_err_console, - ): - mock_gather.side_effect = create_gather_side_effect() - mock_root_register.return_value = (True, "Success", "0x123") - - result = await register( - wallet=mock_wallet, - subtensor=mock_subtensor_base, - netuid=0, - era=None, - json_output=False, - prompt=False, - ) - - # Verify root_register_extrinsic was called with correct parameters - mock_root_register.assert_awaited_once() - call_args = mock_root_register.call_args - assert call_args[1]["wallet"] == mock_wallet - assert call_args[1]["proxy"] is None - - # Verify no errors were printed (success case) - mock_err_console.print.assert_not_called() - - @pytest.mark.asyncio - async def test_register_with_proxy(self, mock_subtensor_base, mock_wallet): - """Test registration with proxy address.""" - proxy_address = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" - - with ( - patch( - "bittensor_cli.src.commands.subnets.subnets.asyncio.gather" - ) as mock_gather, - patch( - "bittensor_cli.src.commands.subnets.subnets.burned_register_extrinsic" - ) as mock_burned_register, - patch( - "bittensor_cli.src.commands.subnets.subnets.err_console" - ) as mock_err_console, - ): - mock_gather.side_effect = create_gather_side_effect() - mock_burned_register.return_value = (True, "Success", "0x789") - - result = await register( - wallet=mock_wallet, - subtensor=mock_subtensor_base, - netuid=1, - era=None, - json_output=False, - prompt=False, - proxy=proxy_address, - ) - - # Verify burned_register_extrinsic was called with correct proxy - mock_burned_register.assert_awaited_once() - call_args = mock_burned_register.call_args - assert call_args[1]["proxy"] == proxy_address - - # Verify no errors were printed (success case) - mock_err_console.print.assert_not_called() - @pytest.mark.asyncio async def test_register_json_output_subnet_not_exist( self, mock_subtensor_base, mock_wallet @@ -282,72 +134,3 @@ async def test_register_json_output_subnet_not_exist( assert data["success"] is False assert "does not exist" in data["msg"] assert data["extrinsic_identifier"] is None - - @pytest.mark.asyncio - async def test_register_json_output_success(self, mock_subtensor_base, mock_wallet): - """Test JSON output on successful registration.""" - with ( - patch( - "bittensor_cli.src.commands.subnets.subnets.asyncio.gather" - ) as mock_gather, - patch( - "bittensor_cli.src.commands.subnets.subnets.burned_register_extrinsic" - ) as mock_burned_register, - patch( - "bittensor_cli.src.commands.subnets.subnets.json_console" - ) as mock_json_console, - ): - mock_gather.side_effect = create_gather_side_effect() - mock_burned_register.return_value = ( - True, - "Registration successful", - "0xabc", - ) - - result = await register( - wallet=mock_wallet, - subtensor=mock_subtensor_base, - netuid=1, - era=None, - json_output=True, - prompt=False, - ) - - mock_json_console.print.assert_called_once() - call_str = str(mock_json_console.print.call_args) - assert "success" in call_str - assert "0xabc" in call_str - - @pytest.mark.asyncio - async def test_register_user_cancels_prompt(self, mock_subtensor_base, mock_wallet): - """Test registration when user cancels the confirmation prompt.""" - with ( - patch( - "bittensor_cli.src.commands.subnets.subnets.asyncio.gather" - ) as mock_gather, - patch("bittensor_cli.src.commands.subnets.subnets.Confirm") as mock_confirm, - patch( - "bittensor_cli.src.commands.subnets.subnets.get_hotkey_pub_ss58" - ) as mock_get_hotkey, - patch( - "bittensor_cli.src.commands.subnets.subnets.burned_register_extrinsic" - ) as mock_burned_register, - ): - mock_gather.side_effect = create_gather_side_effect() - mock_confirm.ask.return_value = False # User cancels - mock_get_hotkey.return_value = ( - "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" - ) - - result = await register( - wallet=mock_wallet, - subtensor=mock_subtensor_base, - netuid=1, - era=None, - json_output=False, - prompt=True, - ) - - # User cancelled, so burned_register should not be called - mock_burned_register.assert_not_awaited() - mock_confirm.ask.assert_called_once()