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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ allways-venv/

__pycache__
.pytest_cache/
.coverage
htmlcov/

*.egg-info

Expand Down
17 changes: 17 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,26 @@ alw = "allways.cli.main:main"
[dependency-groups]
dev = [
"pytest>=9.0.0",
"pytest-cov>=6.0.0",
"ruff>=0.14.10",
]

[tool.pytest.ini_options]
addopts = "--cov=allways --cov-report=term-missing --cov-report=html:htmlcov"
testpaths = ["tests"]

[tool.coverage.run]
source = ["allways"]
omit = ["allways/cli/*", "neurons/*"]

[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"if __name__ == .__main__.:",
"raise NotImplementedError",
"@abstractmethod",
]

# bitcoin-message-tool incorrectly lists pytest as a runtime dependency
[tool.uv]
override-dependencies = ["pytest>=9.0.0"]
Expand Down
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def test_empty(self):


class TestBIP137SignVerify:
"""Test BIP-137 sign/verify roundtrip using bitcoin-message-tool directly."""
"""BIP-137 sign/verify roundtrip using bitcoin-message-tool directly."""

def test_p2pkh_roundtrip(self):
addr, _, sig = sign_message(TEST_WIF, 'p2pkh', TEST_MESSAGE, deterministic=True)
Expand Down
85 changes: 85 additions & 0 deletions tests/chain_providers/test_init.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"""Tests for allways.chain_providers — create_chain_providers factory."""

from unittest.mock import MagicMock, patch

import pytest

import allways.chain_providers as cp_module
from allways.chain_providers import create_chain_providers


def _mock_registry(*entries):
"""Return a patched PROVIDER_REGISTRY with mock provider classes."""
return patch.object(cp_module, 'PROVIDER_REGISTRY', entries)


class TestCreateChainProviders:
def test_returns_providers_for_all_entries(self):
MockCls = MagicMock(return_value=MagicMock())
with _mock_registry(('btc', MockCls, ()), ('tao', MockCls, ())):
providers = create_chain_providers()
assert set(providers.keys()) == {'btc', 'tao'}

def test_kwargs_forwarded_to_provider(self):
MockCls = MagicMock(return_value=MagicMock())
subtensor = MagicMock()
with _mock_registry(('tao', MockCls, ('subtensor',))):
create_chain_providers(subtensor=subtensor)
MockCls.assert_called_once_with(subtensor=subtensor)

def test_missing_kwarg_not_forwarded(self):
"""If the registry lists 'subtensor' but it isn't passed, it's simply omitted."""
MockCls = MagicMock(return_value=MagicMock())
with _mock_registry(('tao', MockCls, ('subtensor',))):
create_chain_providers() # no subtensor kwarg
MockCls.assert_called_once_with() # called with no args

def test_check_true_calls_check_connection(self):
inst = MagicMock()
MockCls = MagicMock(return_value=inst)
with _mock_registry(('btc', MockCls, ())):
create_chain_providers(check=True, require_send=False)
inst.check_connection.assert_called_once_with(require_send=False)

def test_check_true_require_send_default_true(self):
inst = MagicMock()
MockCls = MagicMock(return_value=inst)
with _mock_registry(('btc', MockCls, ())):
create_chain_providers(check=True)
inst.check_connection.assert_called_once_with(require_send=True)

def test_check_true_provider_init_failure_raises(self):
MockCls = MagicMock(side_effect=RuntimeError('no config'))
MockCls.__name__ = 'MockProvider'
with _mock_registry(('btc', MockCls, ())):
with pytest.raises(RuntimeError, match='failed startup check'):
create_chain_providers(check=True)

def test_check_true_check_connection_failure_raises(self):
inst = MagicMock()
inst.check_connection.side_effect = RuntimeError('node down')
MockCls = MagicMock(return_value=inst)
MockCls.__name__ = 'MockProvider'
with _mock_registry(('btc', MockCls, ())):
with pytest.raises(RuntimeError, match='failed startup check'):
create_chain_providers(check=True)

def test_check_false_failure_skips_provider(self):
MockFail = MagicMock(side_effect=RuntimeError('offline'))
MockFail.__name__ = 'MockFail'
MockOk = MagicMock(return_value=MagicMock())
with _mock_registry(('btc', MockFail, ()), ('tao', MockOk, ())):
providers = create_chain_providers(check=False)
assert 'btc' not in providers
assert 'tao' in providers

def test_check_false_no_check_connection_called(self):
inst = MagicMock()
MockCls = MagicMock(return_value=inst)
with _mock_registry(('btc', MockCls, ())):
create_chain_providers(check=False)
inst.check_connection.assert_not_called()

def test_empty_registry_returns_empty(self):
with _mock_registry():
assert create_chain_providers() == {}
98 changes: 98 additions & 0 deletions tests/chain_providers/test_subtensor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"""Tests for SubtensorProvider SCALE decoding and address validation."""

from unittest.mock import MagicMock

from allways.chain_providers.subtensor import SubtensorProvider


class TestDecodeCompact:
def test_mode0_zero(self):
val, consumed = SubtensorProvider._decode_compact(bytes([0]))
assert val == 0
assert consumed == 1

def test_mode0_max(self):
# 63 in mode 0: 63 << 2 = 252
val, consumed = SubtensorProvider._decode_compact(bytes([252]))
assert val == 63
assert consumed == 1

def test_mode1_64(self):
# Encode 64: (64 << 2) | 1 = 257 -> bytes [1, 1] (LE)
encoded = bytes([((64 << 2) | 1) & 0xFF, (64 << 2 | 1) >> 8])
val, consumed = SubtensorProvider._decode_compact(encoded)
assert val == 64
assert consumed == 2

def test_mode1_roundtrip_various(self):
for n in [64, 100, 1000, 16383]:
raw = (n << 2) | 1
encoded = bytes([raw & 0xFF, (raw >> 8) & 0xFF])
val, consumed = SubtensorProvider._decode_compact(encoded)
assert val == n, f'Failed for n={n}'
assert consumed == 2

def test_mode2_16384(self):
n = 16384
raw = (n << 2) | 2
encoded = raw.to_bytes(4, 'little')
val, consumed = SubtensorProvider._decode_compact(encoded)
assert val == n
assert consumed == 4

def test_mode2_large(self):
n = 100000
raw = (n << 2) | 2
encoded = raw.to_bytes(4, 'little')
val, consumed = SubtensorProvider._decode_compact(encoded)
assert val == n
assert consumed == 4

def test_mode3_big_integer(self):
# Mode 3: first byte = (num_extra_bytes - 4) << 2 | 3
# For a number that fits in 5 bytes: n_bytes=5, first_byte = (5-4)<<2|3 = 7
n = 2**32 + 1 # 4294967297, needs 5 bytes
n_bytes = (n.bit_length() + 7) // 8
first_byte = ((n_bytes - 4) << 2) | 3
encoded = bytes([first_byte]) + n.to_bytes(n_bytes, 'little')
val, consumed = SubtensorProvider._decode_compact(encoded)
assert val == n
assert consumed == 1 + n_bytes

def test_empty_bytes(self):
val, consumed = SubtensorProvider._decode_compact(b'')
assert val == 0
assert consumed == 0

def test_mode1_insufficient(self):
val, consumed = SubtensorProvider._decode_compact(bytes([0x01]))
assert val == 0
assert consumed == 0


class TestIsValidAddress:
def _provider(self):
return SubtensorProvider(MagicMock())

def test_valid_ss58(self):
p = self._provider()
# Typical 48-char SS58 address
addr = '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY'
assert p.is_valid_address(addr) is True

def test_wrong_length(self):
p = self._provider()
assert p.is_valid_address('5GrwvaEF') is False

def test_invalid_chars(self):
p = self._provider()
# Contains 0, O, I, l — invalid in base58
assert p.is_valid_address('0' * 48) is False

def test_empty(self):
p = self._provider()
assert p.is_valid_address('') is False

def test_none(self):
p = self._provider()
assert p.is_valid_address(None) is False
Empty file added tests/contract/__init__.py
Empty file.
Loading