Skip to content

Commit 90e2208

Browse files
committed
feat(reactor): add IReactorProcess and IReactorSocket support
1 parent 14e43d1 commit 90e2208

16 files changed

+162
-53
lines changed

hathor/reactor/memory_reactor.py

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Copyright 2024 Hathor Labs
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from collections.abc import Mapping, Sequence
16+
from typing import AnyStr
17+
18+
from twisted.internet.interfaces import IProcessProtocol, IProcessTransport
19+
from twisted.internet.task import Clock
20+
from twisted.internet.testing import MemoryReactor as TwistedMemoryReactor
21+
22+
23+
class MemoryReactor(TwistedMemoryReactor):
24+
"""A drop-in replacement for Twisted's own MemoryReactor that adds support for IReactorProcess."""
25+
26+
def run(self) -> None:
27+
"""
28+
We have to override TwistedMemoryReactor.run() because the original Twisted implementation weirdly calls stop()
29+
inside run(), and we need the reactor running during our tests.
30+
"""
31+
self.running = True
32+
33+
def spawnProcess(
34+
self,
35+
processProtocol: IProcessProtocol,
36+
executable: bytes | str,
37+
args: Sequence[bytes | str],
38+
env: Mapping[AnyStr, AnyStr] | None = None,
39+
path: bytes | str | None = None,
40+
uid: int | None = None,
41+
gid: int | None = None,
42+
usePTY: bool = False,
43+
childFDs: Mapping[int, int | str] | None = None,
44+
) -> IProcessTransport:
45+
raise NotImplementedError
46+
47+
48+
class MemoryReactorClock(MemoryReactor, Clock):
49+
"""A drop-in replacement for Twisted's own MemoryReactorClock that adds support for IReactorProcess."""
50+
51+
def __init__(self) -> None:
52+
MemoryReactor.__init__(self)
53+
Clock.__init__(self)

hathor/reactor/reactor.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from typing import cast
1616

1717
from structlog import get_logger
18-
from twisted.internet.interfaces import IReactorCore, IReactorTCP, IReactorTime
18+
from twisted.internet.interfaces import IReactorCore, IReactorProcess, IReactorSocket, IReactorTCP, IReactorTime
1919
from zope.interface.verify import verifyObject
2020

2121
from hathor.reactor.reactor_protocol import ReactorProtocol
@@ -81,6 +81,8 @@ def initialize_global_reactor(*, use_asyncio_reactor: bool = False) -> ReactorPr
8181
assert verifyObject(IReactorTime, twisted_reactor) is True
8282
assert verifyObject(IReactorCore, twisted_reactor) is True
8383
assert verifyObject(IReactorTCP, twisted_reactor) is True
84+
assert verifyObject(IReactorProcess, twisted_reactor) is True
85+
assert verifyObject(IReactorSocket, twisted_reactor) is True
8486

8587
# We cast to ReactorProtocol, our own type that stubs the necessary Twisted zope interfaces, to aid typing.
8688
_reactor = cast(ReactorProtocol, twisted_reactor)
+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Copyright 2024 Hathor Labs
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from collections.abc import Mapping, Sequence
16+
from typing import AnyStr, Protocol
17+
18+
from twisted.internet.interfaces import IProcessProtocol, IProcessTransport, IReactorProcess
19+
from zope.interface import implementer
20+
21+
22+
@implementer(IReactorProcess)
23+
class ReactorProcessProtocol(Protocol):
24+
"""A Python protocol that stubs Twisted's IReactorProcess interface."""
25+
26+
def spawnProcess(
27+
self,
28+
processProtocol: IProcessProtocol,
29+
executable: bytes | str,
30+
args: Sequence[bytes | str],
31+
env: Mapping[AnyStr, AnyStr] | None = None,
32+
path: bytes | str | None = None,
33+
uid: int | None = None,
34+
gid: int | None = None,
35+
usePTY: bool = False,
36+
childFDs: Mapping[int, int | str] | None = None,
37+
) -> IProcessTransport:
38+
...

hathor/reactor/reactor_protocol.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
from typing import Protocol
1616

1717
from hathor.reactor.reactor_core_protocol import ReactorCoreProtocol
18+
from hathor.reactor.reactor_process_protocol import ReactorProcessProtocol
19+
from hathor.reactor.reactor_socket_protocol import ReactorSocketProtocol
1820
from hathor.reactor.reactor_tcp_protocol import ReactorTCPProtocol
1921
from hathor.reactor.reactor_time_protocol import ReactorTimeProtocol
2022

@@ -23,9 +25,10 @@ class ReactorProtocol(
2325
ReactorCoreProtocol,
2426
ReactorTimeProtocol,
2527
ReactorTCPProtocol,
28+
ReactorProcessProtocol,
29+
ReactorSocketProtocol,
2630
Protocol,
2731
):
2832
"""
29-
A Python protocol that represents the intersection of Twisted's IReactorCore+IReactorTime+IReactorTCP interfaces.
33+
A Python protocol that represents an intersection of the Twisted reactor interfaces that we use.
3034
"""
31-
pass
+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Copyright 2024 Hathor Labs
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from socket import AddressFamily
16+
from typing import Protocol
17+
18+
from twisted.internet.interfaces import IListeningPort, IReactorSocket
19+
from twisted.internet.protocol import DatagramProtocol, ServerFactory
20+
from zope.interface import implementer
21+
22+
23+
@implementer(IReactorSocket)
24+
class ReactorSocketProtocol(Protocol):
25+
"""A Python protocol that stubs Twisted's IReactorSocket interface."""
26+
27+
def adoptStreamPort(
28+
self,
29+
fileDescriptor: int,
30+
addressFamily: AddressFamily,
31+
factory: ServerFactory,
32+
) -> IListeningPort:
33+
...
34+
35+
def adoptStreamConnection(self, fileDescriptor: int, addressFamily: AddressFamily, factory: ServerFactory) -> None:
36+
...
37+
38+
def adoptDatagramPort(
39+
self,
40+
fileDescriptor: int,
41+
addressFamily: AddressFamily,
42+
protocol: DatagramProtocol,
43+
maxPacketSize: int,
44+
) -> IListeningPort:
45+
...

hathor/simulator/clock.py renamed to hathor/simulator/heap_clock.py

+2-8
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@
1717

1818
from twisted.internet.base import DelayedCall
1919
from twisted.internet.interfaces import IDelayedCall, IReactorTime
20-
from twisted.internet.testing import MemoryReactor
2120
from zope.interface import implementer
2221

22+
from hathor.reactor.memory_reactor import MemoryReactor
23+
2324

2425
@implementer(IReactorTime)
2526
class HeapClock:
@@ -94,10 +95,3 @@ class MemoryReactorHeapClock(MemoryReactor, HeapClock):
9495
def __init__(self):
9596
MemoryReactor.__init__(self)
9697
HeapClock.__init__(self)
97-
98-
def run(self):
99-
"""
100-
We have to override MemoryReactor.run() because the original Twisted implementation weirdly calls stop() inside
101-
run(), and we need the reactor running during our tests.
102-
"""
103-
self.running = True

hathor/simulator/simulator.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
from hathor.feature_activation.feature_service import FeatureService
2828
from hathor.manager import HathorManager
2929
from hathor.p2p.peer import PrivatePeer
30-
from hathor.simulator.clock import HeapClock, MemoryReactorHeapClock
30+
from hathor.simulator.heap_clock import HeapClock, MemoryReactorHeapClock
3131
from hathor.simulator.miner.geometric_miner import GeometricMiner
3232
from hathor.simulator.patches import SimulatorCpuMiningService, SimulatorVertexVerifier
3333
from hathor.simulator.tx_generator import RandomTransactionGenerator

tests/cli/test_events_simulator.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@
1717
from hathor.cli.events_simulator.event_forwarding_websocket_factory import EventForwardingWebsocketFactory
1818
from hathor.cli.events_simulator.events_simulator import create_parser, execute
1919
from hathor.conf.get_settings import get_global_settings
20-
from tests.test_memory_reactor_clock import TestMemoryReactorClock
20+
from hathor.reactor.memory_reactor import MemoryReactorClock
2121

2222

2323
def test_events_simulator() -> None:
2424
parser = create_parser()
2525
args = parser.parse_args(['--scenario', 'ONLY_LOAD'])
26-
reactor = TestMemoryReactorClock()
26+
reactor = MemoryReactorClock()
2727

2828
execute(args, reactor)
2929
reactor.advance(1)

tests/event/websocket/test_factory.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from hathor.event.websocket.factory import EventWebsocketFactory
2222
from hathor.event.websocket.protocol import EventWebsocketProtocol
2323
from hathor.event.websocket.response import EventResponse, InvalidRequestType
24-
from hathor.simulator.clock import MemoryReactorHeapClock
24+
from hathor.simulator.heap_clock import MemoryReactorHeapClock
2525
from tests.utils import EventMocker
2626

2727

tests/p2p/test_bootstrap.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
from hathor.p2p.peer_discovery import DNSPeerDiscovery, PeerDiscovery
1111
from hathor.p2p.peer_discovery.dns import LookupResult
1212
from hathor.pubsub import PubSubManager
13+
from hathor.reactor.memory_reactor import MemoryReactorClock
1314
from tests import unittest
14-
from tests.test_memory_reactor_clock import TestMemoryReactorClock
1515

1616

1717
class MockPeerDiscovery(PeerDiscovery):
@@ -25,7 +25,7 @@ async def discover_and_connect(self, connect_to: Callable[[Entrypoint], None]) -
2525

2626

2727
class MockDNSPeerDiscovery(DNSPeerDiscovery):
28-
def __init__(self, reactor: TestMemoryReactorClock, bootstrap_txt: list[tuple[str, int]], bootstrap_a: list[str]):
28+
def __init__(self, reactor: MemoryReactorClock, bootstrap_txt: list[tuple[str, int]], bootstrap_a: list[str]):
2929
super().__init__(['test.example'])
3030
self.reactor = reactor
3131
self.mocked_lookup_a = [RRHeader(type=A, payload=Record_A(address)) for address in bootstrap_a]

tests/poa/test_poa_block_producer.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,14 @@
2222
from hathor.consensus.poa import PoaBlockProducer
2323
from hathor.crypto.util import get_public_key_bytes_compressed
2424
from hathor.manager import HathorManager
25+
from hathor.reactor.memory_reactor import MemoryReactorClock
2526
from hathor.transaction.poa import PoaBlock
2627
from tests.poa.utils import get_settings, get_signer
27-
from tests.test_memory_reactor_clock import TestMemoryReactorClock
2828
from tests.unittest import TestBuilder
2929

3030

3131
def _get_manager(settings: HathorSettings) -> HathorManager:
32-
reactor = TestMemoryReactorClock()
32+
reactor = MemoryReactorClock()
3333
reactor.advance(settings.GENESIS_BLOCK_TIMESTAMP)
3434

3535
artifacts = TestBuilder() \
@@ -45,7 +45,7 @@ def test_poa_block_producer_one_signer() -> None:
4545
settings = get_settings(signer, time_between_blocks=10)
4646
manager = _get_manager(settings)
4747
reactor = manager.reactor
48-
assert isinstance(reactor, TestMemoryReactorClock)
48+
assert isinstance(reactor, MemoryReactorClock)
4949
manager = Mock(wraps=manager)
5050
producer = PoaBlockProducer(settings=settings, reactor=reactor, poa_signer=signer)
5151
producer.manager = manager
@@ -103,7 +103,7 @@ def test_poa_block_producer_two_signers() -> None:
103103
settings = get_settings(signer1, signer2, time_between_blocks=10)
104104
manager = _get_manager(settings)
105105
reactor = manager.reactor
106-
assert isinstance(reactor, TestMemoryReactorClock)
106+
assert isinstance(reactor, MemoryReactorClock)
107107
manager = Mock(wraps=manager)
108108
producer = PoaBlockProducer(settings=settings, reactor=reactor, poa_signer=signer1)
109109
producer.manager = manager

tests/pubsub/test_pubsub2.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@
1616
from unittest.mock import Mock, patch
1717

1818
import pytest
19-
from twisted.internet.testing import MemoryReactorClock
2019

2120
from hathor.pubsub import HathorEvents, PubSubManager
21+
from hathor.reactor.memory_reactor import MemoryReactorClock
2222

2323

2424
@pytest.mark.parametrize('is_in_main_thread', [False, True])

tests/test_memory_reactor_clock.py

-26
This file was deleted.

tests/tx/test_merged_mining.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ async def test_coordinator(self):
2525
from cryptography.hazmat.primitives.asymmetric import ec
2626

2727
from hathor.crypto.util import get_address_b58_from_public_key
28-
from hathor.simulator.clock import MemoryReactorHeapClock
28+
from hathor.simulator.heap_clock import MemoryReactorHeapClock
2929

3030
super().setUp()
3131
self.manager = self.create_peer('testnet')

tests/tx/test_stratum.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import pytest
88
from twisted.internet.testing import StringTransportWithDisconnection
99

10-
from hathor.simulator.clock import MemoryReactorHeapClock
10+
from hathor.simulator.heap_clock import MemoryReactorHeapClock
1111
from hathor.stratum import (
1212
INVALID_PARAMS,
1313
INVALID_REQUEST,

tests/unittest.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,13 @@
2323
from hathor.p2p.sync_version import SyncVersion
2424
from hathor.pubsub import PubSubManager
2525
from hathor.reactor import ReactorProtocol as Reactor, get_global_reactor
26-
from hathor.simulator.clock import MemoryReactorHeapClock
26+
from hathor.reactor.memory_reactor import MemoryReactorClock
27+
from hathor.simulator.heap_clock import MemoryReactorHeapClock
2728
from hathor.transaction import BaseTransaction, Block, Transaction
2829
from hathor.transaction.storage.transaction_storage import TransactionStorage
2930
from hathor.types import VertexId
3031
from hathor.util import Random, not_none
3132
from hathor.wallet import BaseWallet, HDWallet, Wallet
32-
from tests.test_memory_reactor_clock import TestMemoryReactorClock
3333

3434
logger = get_logger()
3535
main = ut_main
@@ -115,7 +115,7 @@ class TestCase(unittest.TestCase):
115115

116116
def setUp(self) -> None:
117117
self.tmpdirs: list[str] = []
118-
self.clock = TestMemoryReactorClock()
118+
self.clock = MemoryReactorClock()
119119
self.clock.advance(time.time())
120120
self.reactor = self.clock
121121
self.log = logger.new()

0 commit comments

Comments
 (0)