Skip to content
Merged
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
6 changes: 3 additions & 3 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ jobs:
strategy:
matrix:
python:
- 3.9
- '3.10'
- '3.11'
- '3.12'
os:
- ubuntu-latest
steps:
Expand All @@ -39,4 +39,4 @@ jobs:
run: poetry run make tests
- name: Upload coverage
uses: codecov/codecov-action@v1
if: matrix.python == 3.9 && matrix.os == 'ubuntu-latest'
if: matrix.python == '3.11' && matrix.os == 'ubuntu-latest'
6 changes: 3 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.9-alpine AS build
FROM python:3.11-alpine AS build

WORKDIR /code

Expand All @@ -13,9 +13,9 @@ COPY poetry.lock pyproject.toml /code/
RUN poetry config virtualenvs.create false \
&& poetry install --only main --no-interaction --no-ansi

FROM python:3.9-alpine
FROM python:3.11-alpine

COPY --from=build /usr/local/lib/python3.9/site-packages /usr/local/lib/python3.9/site-packages
COPY --from=build /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
RUN apk add libgcc

COPY txstratum/ ./txstratum
Expand Down
634 changes: 440 additions & 194 deletions poetry.lock

Large diffs are not rendered by default.

16 changes: 8 additions & 8 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,25 @@ homepage = "https://hathor.network/"
repository = "https://github.com/HathorNetwork/tx-mining-service/"

[tool.poetry.dependencies]
python = ">=3.9,<4.0"
python = ">=3.11,<4.0"
ConfigArgParse = "^1.2.3"
colorama = "^0.4.4"
aiohttp = "~3.9.3"
base58 = "~2.1.1"
structlog = "~22.3.0"
prometheus-client = "^0.9.0"
idna_ssl = "^1.1.0"
asynctest = "^0.13.0"
hathorlib = {version = "^0.14.0", extras = ["client"]}
dataclasses = {version = "^0.8", python = ">=3.6,<3.7"}
# Cap required: setuptools >=82 removed pkg_resources, which is used by pycoin (a transitive dep via hathorlib)
setuptools = ">=68.0,<82"
hathorlib = {version = "^0.14.1", extras = ["client"]}
Comment thread
luislhl marked this conversation as resolved.
python-healthchecklib = "^0.1.0"

[tool.poetry.dev-dependencies]
flake8 = "^3.8.4"
flake8-docstrings = "^1.5.0"
[tool.poetry.group.dev.dependencies]
flake8 = "^7.0.0"
flake8-docstrings = "^1.7.0"
isort = "~5.10.1"
mypy = "^1.0.0"
numpy = "^1.19.4"
numpy = "^1.26.0"
pytest = "~7.2.0"
pytest-aiohttp = "^0.3.0"
pytest-cov = "~4.0.0"
Expand Down
17 changes: 3 additions & 14 deletions tests/test_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
This source code is licensed under the MIT license found in the
LICENSE file in the root directory of this source tree.
"""
import unittest
from typing import List
from unittest.mock import MagicMock
from unittest.mock import AsyncMock

import asynctest # type: ignore
from hathorlib.scripts import P2PKH
from hathorlib.transaction import Transaction, TxInput, TxOutput
from hathorlib.utils import decode_address
Expand All @@ -16,17 +16,6 @@
from txstratum.toi_client import CheckBlacklist, TOIError


class AsyncMock(MagicMock): # type: ignore
"""MagicMock for async functions.

The native unittest.mock.AsyncMock is not being used
because it was added on python 3.8 and we have to support versions 3.6 and 3.7
"""

async def __call__(self, *args, **kwargs):
return super().__call__(*args, **kwargs)


def create_tx_from(inputs: List[TxInput], outputs: List[TxOutput]) -> Transaction:
tx = Transaction()
tx.inputs.extend(
Expand Down Expand Up @@ -67,7 +56,7 @@ def create_tx_with(
return create_tx_from(inputs, outputs)


class FiltersTestCase(asynctest.ClockedTestCase): # type: ignore
class FiltersTestCase(unittest.IsolatedAsyncioTestCase):
async def test_file_filter_address(self):
fail_address = "HTQMV7gbUsJeADqTB9tTt6qin5VJcKy6Kb"
ok_address = "H9ZVe52vMVbGBXCSEXCS9tZ5YEY3tUk1CL"
Expand Down
8 changes: 4 additions & 4 deletions tests/test_healthcheck.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import unittest
from unittest.mock import MagicMock

import asynctest # type: ignore[import]
from healthcheck import HealthcheckStatus

from txstratum.healthcheck.healthcheck import (
Expand All @@ -17,7 +17,7 @@ async def health(self):
return {"status": "pass"}


class TestFullnodeHealthCheck(asynctest.TestCase): # type: ignore[misc]
class TestFullnodeHealthCheck(unittest.IsolatedAsyncioTestCase):
def setUp(self) -> None:
self.mock_hathor_client = MagicMock()
self.fullnode_health_check = FullnodeHealthCheck(
Expand Down Expand Up @@ -75,7 +75,7 @@ async def side_effect():
self.assertEqual(result.output, "Fullnode is not healthy: {'status': 'fail'}")


class TestMiningHealthCheck(asynctest.TestCase): # type: ignore[misc]
class TestMiningHealthCheck(unittest.IsolatedAsyncioTestCase):
def setUp(self):
self.manager = MagicMock()
self.mining_health_check = MiningHealthCheck(manager=self.manager)
Expand Down Expand Up @@ -173,7 +173,7 @@ async def test_return_last_status(self):
)


class TestHealthCheck(asynctest.TestCase): # type: ignore[misc]
class TestHealthCheck(unittest.IsolatedAsyncioTestCase):
def setUp(self):

self.mock_hathor_client = HathorClientMock()
Expand Down
27 changes: 15 additions & 12 deletions tests/test_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@
from typing import List, Optional
from unittest.mock import ANY, MagicMock, Mock

import asynctest # type: ignore
import pytest
from hathorlib.client import BlockTemplate, HathorClient
from hathorlib.exceptions import PushTxFailed

import txstratum.time
from tests.utils import ClockedTestCase
from txstratum.exceptions import JobAlreadyExists
from txstratum.jobs import JobStatus, TxJob
from txstratum.manager import TxMiningManager
Expand Down Expand Up @@ -932,8 +932,9 @@ async def push_tx_or_block(self, raw: bytes) -> bool:
self.assertEqual(tx_job.status, JobStatus.FAILED)


class ManagerClockedTestCase(asynctest.ClockedTestCase): # type: ignore
def setUp(self):
class ManagerClockedTestCase(ClockedTestCase):
async def asyncSetUp(self):
await super().asyncSetUp()
address = "HC7w4j7mPet49BBN5a2An3XUiPvK6C1TL7"

from tests.utils import Clock
Expand All @@ -942,12 +943,12 @@ def setUp(self):
self.clock.enable()

self.client = HathorClientTest(server_url="")
self.loop.run_until_complete(self.client.start())
await self.client.start()
self.manager = TxMiningManager(
backend=self.client, pubsub=MagicMock(), address=address
)
self.loop.run_until_complete(self.manager.start())
self.loop.run_until_complete(self.manager.wait_for_block_template())
await self.manager.start()
await self.manager.wait_for_block_template()
self.assertTrue(len(self.manager.block_template) > 0)

def tearDown(self):
Expand All @@ -971,18 +972,20 @@ async def test_block_timestamp_update(self):
self.assertTrue(True, job.is_block)

job.update_timestamp(force=True)
self.assertEqual(int(txstratum.time.time()), job._block.timestamp)
ts_before = int(txstratum.time.time())
self.assertEqual(ts_before, job._block.timestamp)

# Update timestamp.
# Update timestamp — should reflect the 10s advance.
await self.advance(10)
job.update_timestamp()
self.assertEqual(int(txstratum.time.time()), job._block.timestamp)
ts_after = int(txstratum.time.time())
self.assertEqual(ts_before + 10, ts_after)
self.assertEqual(ts_after, job._block.timestamp)

# Do not update timestamp.
old_ts = txstratum.time.time()
# Do not update timestamp (advance beyond threshold).
await self.advance(40)
job.update_timestamp()
self.assertEqual(int(old_ts), job._block.timestamp)
self.assertEqual(ts_after, job._block.timestamp)

async def test_tx_resubmit(self):
job1 = TxJob(TX1_DATA, timeout=10)
Expand Down
16 changes: 7 additions & 9 deletions tests/test_prometheus.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@
import os
import shutil
import tempfile

import asynctest # type: ignore[import]
import unittest

from txstratum.jobs import TxJob
from txstratum.prometheus import METRIC_INFO, MetricData, PrometheusExporter
Expand Down Expand Up @@ -44,9 +43,9 @@ def get_total_hashrate_ghs(self):
return 1.23


class ManagerTestCase(asynctest.TestCase): # type: ignore[misc]
def setUp(self):
self.pubsub = PubSubManager(self.loop)
class ManagerTestCase(unittest.IsolatedAsyncioTestCase):
async def asyncSetUp(self):
self.pubsub = PubSubManager(asyncio.get_running_loop())
self.manager = TxMiningManagerMock()
self.tmpdir = tempfile.mkdtemp()

Expand All @@ -56,15 +55,14 @@ def tearDown(self):

async def _run_all_pending_events(self):
"""Run all pending events."""
# pending = asyncio.all_tasks(self.loop)
# self.loop.run_until_complete(asyncio.gather(*pending))

async def _fn():
self.ran_all = True

self.loop.create_task(_fn())
asyncio.create_task(_fn())

while getattr(self, "ran_all", False) is False:
await asyncio.sleep(0.1)
await asyncio.sleep(0)

self.ran_all = False

Expand Down
5 changes: 2 additions & 3 deletions tests/test_time.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,11 @@
"""
import time

import asynctest # type: ignore

import txstratum.time
from tests.utils import ClockedTestCase


class TimeTestCase(asynctest.ClockedTestCase): # type: ignore
class TimeTestCase(ClockedTestCase):
async def test_system_clock_time(self):
t1 = time.time()
t2 = txstratum.time.time()
Expand Down
50 changes: 49 additions & 1 deletion tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"""

import asyncio
import unittest
from asyncio.events import AbstractEventLoop
from typing import Optional

Expand All @@ -18,12 +19,59 @@ def __init__(self, loop: Optional[AbstractEventLoop]):
loop = asyncio.get_event_loop()
self.loop = loop
self.ref_time = txstratum.time.time()
# If inside a ClockedTestCase, record the current synthetic offset
# so that time() is derived purely from advance() calls (no real-time drift).
self._initial_offset: Optional[float] = getattr(
loop, "_test_clock_offset", None
)

def time(self) -> float:
return self.ref_time + self.loop.time()
current_offset = getattr(self.loop, "_test_clock_offset", None)
if current_offset is not None and self._initial_offset is not None:
return float(self.ref_time + (current_offset - self._initial_offset))
# Fallback for non-test usage
return float(self.ref_time + self.loop.time())

def enable(self) -> None:
txstratum.time.set_time_function(self.time)

def disable(self) -> None:
txstratum.time.set_time_function(None)


class ClockedTestCase(unittest.IsolatedAsyncioTestCase):
"""Replacement for asynctest.ClockedTestCase compatible with Python 3.11+.

Provides a controllable fake clock for testing time-dependent async code.
After ``asyncSetUp`` runs, ``self.loop`` is the running event loop and its
``time()`` method is patched so that ``await self.advance(seconds)`` jumps
the loop clock forward while still allowing the event loop to run normally.
"""

loop: asyncio.AbstractEventLoop

async def asyncSetUp(self) -> None:
self.loop = asyncio.get_running_loop()
self.loop._test_clock_offset = 0.0 # type: ignore[attr-defined]
_real_time = self.loop.time
loop_ref = self.loop

def _fake_time() -> float:
return float(_real_time() + loop_ref._test_clock_offset) # type: ignore[attr-defined]

# Shadow the loop's time() on the instance so internal scheduling uses it.
self.loop.time = _fake_time # type: ignore[method-assign]

async def advance(self, seconds: float) -> None:
"""Advance the fake clock by ``seconds`` and drain all due callbacks."""
self.loop._test_clock_offset += seconds # type: ignore[attr-defined]
# Yield to the event loop repeatedly so that any asyncio.sleep() or
# call_later() callbacks whose scheduled time has now passed get executed.
for _ in range(1000):
await asyncio.sleep(0)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
ready = bool(getattr(self.loop, "_ready", ()))
scheduled = getattr(self.loop, "_scheduled", ())
if not ready and (not scheduled or scheduled[0]._when > self.loop.time()):
break
else:
raise AssertionError("advance() did not drain due callbacks")
4 changes: 2 additions & 2 deletions txstratum/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -514,8 +514,8 @@ def main() -> None:
args = parser.parse_args()

if args.testnet:
if not os.environ.get("TXMINING_CONFIG_FILE"):
os.environ["TXMINING_CONFIG_FILE"] = "hathorlib.conf.testnet"
if not os.environ.get("HATHOR_CONFIG_FILE"):
os.environ["HATHOR_CONFIG_FILE"] = "hathorlib.conf.testnet"

# Route to the appropriate service runner based on --dev-miner flag.
# Both runners expose the same HTTP API; they differ only in how they
Expand Down
5 changes: 5 additions & 0 deletions txstratum/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,11 @@ def __init__(self, *args: Any, max: int = 0, **kwargs: VT):
self._max: int = max
super().__init__(*args, **kwargs)

def __repr__(self) -> str:
"""Return a consistent repr across Python versions."""
items = list(self.items())
return f"{self.__class__.__name__}({items})"

def __setitem__(self, key: KT, value: VT) -> None:
"""Add a new element to the dict."""
OrderedDict.__setitem__(self, key, value) # type: ignore[assignment]
Expand Down
Loading