diff --git a/conftest.py b/conftest.py index 1a4f89b..8166295 100644 --- a/conftest.py +++ b/conftest.py @@ -1,8 +1,32 @@ +import logging import pytest from os.path import splitext from tests.utils.gen_utils import GenUtils +logger: logging.Logger = logging.getLogger("conftest") + + +def pytest_runtest_logreport(report: pytest.TestReport) -> None: + if report.outcome != "rerun": # type: ignore - pytest-rerunfailures sets this outcome + return + + message: str + + try: + crash_msg = report.longrepr.reprcrash.message # type: ignore + message = str(crash_msg) if crash_msg is not None else "" # type: ignore + except AttributeError: + message = report.longreprtext + except Exception as e: + message = str(e) + + if len(message) == 0: + message = "Unknwon" + + logger.error(f"EXPECTED FAILURE: {message}") + logger.warning(f"RERUN {report.nodeid}") + def pytest_configure(config: pytest.Config) -> None: # inject current date/time into the configured log file name diff --git a/tests/test_gen_utils.py b/tests/test_gen_utils.py index bed03a7..edf13cd 100644 --- a/tests/test_gen_utils.py +++ b/tests/test_gen_utils.py @@ -18,24 +18,24 @@ class TestGenUtils(BaseTestClass): #region uuid / wait_for / bool_equals def test_get_uuid_format_and_uniqueness(self) -> None: - uuid1 = GenUtils.get_uuid() - uuid2 = GenUtils.get_uuid() + uuid1: str = GenUtils.get_uuid() + uuid2: str = GenUtils.get_uuid() logger.debug(f"get_uuid(): {uuid1}, {uuid2}") assert _UUID_RE.match(uuid1), f"not a UUID: {uuid1}" assert _UUID_RE.match(uuid2), f"not a UUID: {uuid2}" assert uuid1 != uuid2 def test_wait_for_blocks_for_at_least_duration(self) -> None: - start = time.monotonic() + start: float = time.monotonic() GenUtils.wait_for(50) - elapsed_ms = (time.monotonic() - start) * 1000 + elapsed_ms: float = (time.monotonic() - start) * 1000 logger.debug(f"wait_for(50) actually took {elapsed_ms:.1f} ms") assert elapsed_ms >= 50 def test_wait_for_zero_does_not_block(self) -> None: - start = time.monotonic() + start: float = time.monotonic() GenUtils.wait_for(0) - elapsed_ms = (time.monotonic() - start) * 1000 + elapsed_ms: float = (time.monotonic() - start) * 1000 assert elapsed_ms < 50 def test_wait_for_negative_raises(self) -> None: @@ -125,19 +125,19 @@ def test_reconcile_uint64_resolve_max_false_picks_lesser(self) -> None: #region reconcile values - @pytest.mark.xfail(reason="gen_utils::reconcile()'s resolve_true branch casts the boost::optional wrapper to bool instead of its value, so it always returns val1 (ignoring which operand is actually true)", strict=True) + @pytest.mark.xfail(reason="gen_utils::reconcile()'s bug", strict=True) def test_reconcile_bool_resolve_true_prefers_the_true_operand(self) -> None: # val1=False, val2=True, resolve_true=True -> should prefer the # operand that IS true, i.e. val2 - result = GenUtils.reconcile_bool(False, True, resolve_true=True) + result: bool | None = GenUtils.reconcile_bool(False, True, resolve_true=True) logger.debug(f"reconcile_bool(False, True, resolve_true=True) = {result}") assert result is True - @pytest.mark.xfail(reason="gen_utils::reconcile()'s resolve_true branch casts the boost::optional wrapper to bool instead of its value, so it always returns val2 for resolve_true=False (ignoring which operand is actually false)", strict=True) + @pytest.mark.xfail(reason="gen_utils::reconcile()'s bug", strict=True) def test_reconcile_bool_resolve_true_false_prefers_the_false_operand(self) -> None: # val1=False, val2=True, resolve_true=False -> should prefer the # operand that IS false, i.e. val1 - result = GenUtils.reconcile_bool(False, True, resolve_true=False) + result: bool | None = GenUtils.reconcile_bool(False, True, resolve_true=False) logger.debug(f"reconcile_bool(False, True, resolve_true=False) = {result}") assert result is False @@ -145,7 +145,7 @@ def test_reconcile_bool_resolve_true_false_prefers_the_false_operand(self) -> No def test_reconcile_uint64_resolve_true_prefers_the_true_operand(self) -> None: # val1=0 (falsy), val2=1 (truthy), resolve_true=True -> should prefer # val2 since it's the operand whose bool cast is True - result = GenUtils.reconcile_uint64(0, 1, resolve_true=True) + result: int | None = GenUtils.reconcile_uint64(0, 1, resolve_true=True) logger.debug(f"reconcile_uint64(0, 1, resolve_true=True) = {result}") assert result == 1 diff --git a/tests/test_monero_common.py b/tests/test_monero_common.py index df8067e..75f6acb 100644 --- a/tests/test_monero_common.py +++ b/tests/test_monero_common.py @@ -1,14 +1,12 @@ import pytest import logging -from json import loads - from monero import ( SerializableStruct, SslOptions, MoneroError, MoneroRpcError ) -from utils import BaseTestClass +from utils import BaseTestClass, AssertUtils logger: logging.Logger = logging.getLogger("TestMoneroCommon") @@ -37,21 +35,14 @@ def test_monero_error(self) -> None: def test_serializable_struct(self) -> None: SerializableStruct() + # test ssl options serialization integrity + @pytest.mark.xfail(reason="TODO monero-cpp implement ssl_options::from_property_tree()", strict=True) def test_ssl_options(self) -> None: + # create ssl_options objects and populate properties ssl_options: SslOptions = SslOptions() ssl_options.ssl_allow_any_cert = True ssl_options.ssl_allowed_fingerprints = ["fingerprint1", "fingerprint2"] ssl_options.ssl_ca_file = "ca_file" ssl_options.ssl_certificate_path = "certificate_path" ssl_options.ssl_private_key_path = "private_key_path" - logger.debug(f"Testing ssl options: {ssl_options.serialize()}") - obj: dict[str, str] = loads(ssl_options.serialize()) - assert obj['sslAllowAnyCert'] == ssl_options.ssl_allow_any_cert - assert obj['sslCaFile'] == ssl_options.ssl_ca_file - assert obj['sslCertificatePath'] == ssl_options.ssl_certificate_path - assert obj['sslPrivateKeyPath'] == ssl_options.ssl_private_key_path - - allowed_fingerprints: list[str] = obj['sslAllowedFingerprints'] # type: ignore - - for i, allowed_fingerprint in enumerate(allowed_fingerprints): - assert allowed_fingerprint == ssl_options.ssl_allowed_fingerprints[i] + AssertUtils.assert_serialization_integrity(ssl_options) diff --git a/tests/test_monero_daemon_model.py b/tests/test_monero_daemon_model.py index 9ae7747..039b190 100644 --- a/tests/test_monero_daemon_model.py +++ b/tests/test_monero_daemon_model.py @@ -3,6 +3,8 @@ import subprocess import sys +from typing import LiteralString + from monero import ( MoneroVersion, MoneroRpcPaymentInfo, MoneroRpcConnection, MoneroAltChain, MoneroBan, MoneroPruneResult, MoneroMiningStatus, MoneroMinerTxSum, @@ -25,37 +27,37 @@ class TestMoneroDaemonModel(BaseTestClass): #region Common / rpc models def test_version_deserialize(self) -> None: - version = MoneroVersion() + version: MoneroVersion = MoneroVersion() version.number = 65552 version.is_release = True AssertUtils.assert_serialization_integrity(version) def test_rpc_payment_info_deserialize(self) -> None: - info = MoneroRpcPaymentInfo() + info: MoneroRpcPaymentInfo = MoneroRpcPaymentInfo() info.credits = 42 info.top_block_hash = "a" * 64 AssertUtils.assert_serialization_integrity(info) def test_rpc_connection_deserialize(self) -> None: - connection = MoneroRpcConnection("http://127.0.0.1:18081", "user", "pass", "127.0.0.1:9050", "tcp://127.0.0.1:18083", 2, 5000) - json_str = connection.serialize() + connection: MoneroRpcConnection = MoneroRpcConnection("http://127.0.0.1:18081", "user", "pass", "127.0.0.1:9050", "tcp://127.0.0.1:18083", 2, 5000) + json_str: str = connection.serialize() logger.debug(f"Serialized rpc connection: {json_str}") - restored = MoneroRpcConnection.deserialize(json_str) + restored: MoneroRpcConnection = MoneroRpcConnection.deserialize(json_str) assert restored.uri == connection.uri assert restored.username == connection.username assert restored.password == connection.password assert restored.proxy_uri == connection.proxy_uri assert restored.zmq_uri == connection.zmq_uri - @pytest.mark.xfail(reason="monero_rpc_connection::from_property_tree() doesn't read back priority/timeoutMs; fixed upstream in the local everoddandeven/monero-cpp checkout, pending a submodule bump", strict=True) + @pytest.mark.xfail(reason="monero_rpc_connection::from_property_tree() bug", strict=True) def test_rpc_connection_priority_and_timeout_deserialize(self) -> None: # to_rapidjson_val() emits "priority" and "timeoutMs" but # from_property_tree() never read either back - connection = MoneroRpcConnection("http://127.0.0.1:18081", priority=2, timeout_ms=5000) - json_str = connection.serialize() + connection: MoneroRpcConnection = MoneroRpcConnection("http://127.0.0.1:18081", priority=2, timeout_ms=5000) + json_str: str = connection.serialize() logger.debug(f"Serialized rpc connection: {json_str}") assert '"priority"' in json_str and '"timeoutMs"' in json_str - restored = MoneroRpcConnection.deserialize(json_str) + restored: MoneroRpcConnection = MoneroRpcConnection.deserialize(json_str) logger.debug(f"Deserialized rpc connection re-serialized: {restored.serialize()}") assert restored.priority == connection.priority assert restored.timeout_ms == connection.timeout_ms @@ -65,7 +67,7 @@ def test_rpc_connection_priority_and_timeout_deserialize(self) -> None: #region Blockchain / mining models def test_alt_chain_deserialize(self) -> None: - alt_chain = MoneroAltChain() + alt_chain: MoneroAltChain = MoneroAltChain() alt_chain.block_hashes = ["a" * 64, "b" * 64] alt_chain.difficulty_low = 100 alt_chain.difficulty_high = 0 @@ -75,7 +77,7 @@ def test_alt_chain_deserialize(self) -> None: AssertUtils.assert_serialization_integrity(alt_chain) def test_ban_deserialize(self) -> None: - ban = MoneroBan() + ban: MoneroBan = MoneroBan() ban.host = "127.0.0.1" ban.ip = 2130706433 ban.is_banned = True @@ -83,7 +85,7 @@ def test_ban_deserialize(self) -> None: AssertUtils.assert_serialization_integrity(ban) def test_prune_result_deserialize(self) -> None: - result = MoneroPruneResult() + result: MoneroPruneResult = MoneroPruneResult() result.pruning_seed = 387 # is_pruned is deliberately not set here: to_rapidjson_val() serializes it # under "isPruned" but from_property_tree() looks for "pruned" instead, so @@ -92,17 +94,17 @@ def test_prune_result_deserialize(self) -> None: @pytest.mark.xfail(reason="monero_prune_result::from_property_tree() bug", strict=True) def test_prune_result_is_pruned_deserialize(self) -> None: - result = MoneroPruneResult() + result: MoneroPruneResult = MoneroPruneResult() result.is_pruned = True - json_str = result.serialize() + json_str: str = result.serialize() logger.debug(f"Serialized prune result: {json_str}") assert '"isPruned"' in json_str - restored = MoneroPruneResult.deserialize(json_str) + restored: MoneroPruneResult = MoneroPruneResult.deserialize(json_str) logger.debug(f"Deserialized prune result re-serialized: {restored.serialize()}") assert restored.is_pruned == result.is_pruned def test_mining_status_deserialize(self) -> None: - status = MoneroMiningStatus() + status: MoneroMiningStatus = MoneroMiningStatus() status.is_active = True status.is_background = False status.address = "9" + "a" * 94 @@ -111,7 +113,7 @@ def test_mining_status_deserialize(self) -> None: AssertUtils.assert_serialization_integrity(status) def test_miner_tx_sum_deserialize(self) -> None: - summ = MoneroMinerTxSum() + summ: MoneroMinerTxSum = MoneroMinerTxSum() summ.emission_sum_low = 1000 summ.emission_sum_high = 0 summ.fee_sum_low = 10 @@ -119,7 +121,7 @@ def test_miner_tx_sum_deserialize(self) -> None: AssertUtils.assert_serialization_integrity(summ) def test_block_template_deserialize(self) -> None: - template = MoneroBlockTemplate() + template: MoneroBlockTemplate = MoneroBlockTemplate() template.block_template_blob = "abcd" template.block_hashing_blob = "ef01" template.prev_hash = "a" * 64 @@ -134,7 +136,7 @@ def test_block_template_deserialize(self) -> None: AssertUtils.assert_serialization_integrity(template) def test_connection_span_deserialize(self) -> None: - span = MoneroConnectionSpan() + span: MoneroConnectionSpan = MoneroConnectionSpan() span.connection_id = "deadbeef" span.remote_address = "127.0.0.1:18080" span.num_blocks = 10 @@ -145,7 +147,7 @@ def test_connection_span_deserialize(self) -> None: AssertUtils.assert_serialization_integrity(span) def test_peer_deserialize(self) -> None: - peer = MoneroPeer() + peer: MoneroPeer = MoneroPeer() peer.id = "1122334455667788" peer.address = "127.0.0.1:18080" peer.host = "127.0.0.1" @@ -177,23 +179,23 @@ def test_peer_deserialize(self) -> None: @pytest.mark.xfail(reason="monero_peer::from_property_tree() bug", strict=True) def test_peer_is_online_deserialize(self) -> None: - peer = MoneroPeer() + peer: MoneroPeer = MoneroPeer() peer.is_online = True - json_str = peer.serialize() + json_str: str = peer.serialize() logger.debug(f"Serialized peer: {json_str}") assert "isOnline" in json_str - restored = MoneroPeer.deserialize(json_str) + restored: MoneroPeer = MoneroPeer.deserialize(json_str) logger.debug(f"Deserialized peer re-serialized: {restored.serialize()}") assert restored.is_online == peer.is_online @pytest.mark.xfail(reason="monero_peer::to_rapidjson_val() bug/monero-cpp checkout", strict=True) def test_peer_connection_serialization_integrity(self) -> None: - peer = MoneroPeer() + peer: MoneroPeer = MoneroPeer() peer.connection_type = MoneroConnectionType.IPV6 - json_str = peer.serialize() + json_str: str = peer.serialize() logger.debug(f"Serialized peer: {json_str}") assert "addressType" in json_str - restored = MoneroPeer.deserialize(json_str) + restored: MoneroPeer = MoneroPeer.deserialize(json_str) logger.debug(f"Deserialized peer re-serialized: {restored.serialize()}") assert restored.connection_type == peer.connection_type @@ -207,7 +209,7 @@ def test_peer_connection_type_deserialize(self) -> None: (3, MoneroConnectionType.TOR), (4, MoneroConnectionType.I2P), ]: - peer = MoneroPeer.deserialize(f'{{"addressType":{value}}}') + peer: MoneroPeer = MoneroPeer.deserialize(f'{{"addressType":{value}}}') assert peer.connection_type == expected def test_peer_connection_type_invalid(self) -> None: @@ -216,7 +218,7 @@ def test_peer_connection_type_invalid(self) -> None: MoneroPeer.deserialize('{"addressType":5}') def test_submit_tx_result_deserialize(self) -> None: - result = MoneroSubmitTxResult() + result: MoneroSubmitTxResult = MoneroSubmitTxResult() result.credits = 1 result.top_block_hash = "a" * 64 result.is_relayed = True @@ -237,17 +239,17 @@ def test_submit_tx_result_deserialize(self) -> None: @pytest.mark.xfail(reason="monero_submit_tx_result::from_property_tree() bug", strict=True) def test_submit_tx_result_is_good_deserialize(self) -> None: - result = MoneroSubmitTxResult() + result: MoneroSubmitTxResult = MoneroSubmitTxResult() result.is_good = True - json_str = result.serialize() + json_str: str = result.serialize() logger.debug(f"Serialized submit tx result: {json_str}") assert "isGood" in json_str - restored = MoneroSubmitTxResult.deserialize(json_str) + restored: MoneroSubmitTxResult = MoneroSubmitTxResult.deserialize(json_str) logger.debug(f"Deserialized submit tx result re-serialized: {restored.serialize()}") assert restored.is_good == result.is_good def test_output_distribution_entry_deserialize(self) -> None: - entry = MoneroOutputDistributionEntry() + entry: MoneroOutputDistributionEntry = MoneroOutputDistributionEntry() entry.amount = 0 entry.base = 100 entry.distribution = [1, 2, 3, 4] @@ -255,7 +257,7 @@ def test_output_distribution_entry_deserialize(self) -> None: AssertUtils.assert_serialization_integrity(entry) def test_output_histogram_entry_deserialize(self) -> None: - entry = MoneroOutputHistogramEntry() + entry: MoneroOutputHistogramEntry = MoneroOutputHistogramEntry() entry.amount = 0 entry.num_instances = 10 entry.unlocked_instances = 8 @@ -263,7 +265,7 @@ def test_output_histogram_entry_deserialize(self) -> None: AssertUtils.assert_serialization_integrity(entry) def test_tx_pool_stats_deserialize(self) -> None: - stats = MoneroTxPoolStats() + stats: MoneroTxPoolStats = MoneroTxPoolStats() stats.num_txs = 5 stats.num_not_relayed = 1 stats.num_failing = 0 @@ -282,17 +284,17 @@ def test_tx_pool_stats_deserialize(self) -> None: @pytest.mark.xfail(reason="monero_tx_pool_stats::from_property_tree() bug", strict=True) def test_tx_pool_stats_histo_deserialize(self) -> None: - stats = MoneroTxPoolStats() + stats: MoneroTxPoolStats = MoneroTxPoolStats() stats.histo = {100: 1, 200: 2} - json_str = stats.serialize() + json_str: str = stats.serialize() logger.debug(f"Serialized tx pool stats: {json_str}") assert "histo" in json_str - restored = MoneroTxPoolStats.deserialize(json_str) + restored: MoneroTxPoolStats = MoneroTxPoolStats.deserialize(json_str) logger.debug(f"Deserialized tx pool stats re-serialized: {restored.serialize()}") assert dict(restored.histo) == dict(stats.histo) def test_daemon_update_check_result_deserialize(self) -> None: - result = MoneroDaemonUpdateCheckResult() + result: MoneroDaemonUpdateCheckResult = MoneroDaemonUpdateCheckResult() result.is_update_available = True result.version = "0.18.5.1" result.hash = "a" * 64 @@ -301,7 +303,7 @@ def test_daemon_update_check_result_deserialize(self) -> None: AssertUtils.assert_serialization_integrity(result) def test_daemon_update_download_result_deserialize(self) -> None: - result = MoneroDaemonUpdateDownloadResult() + result: MoneroDaemonUpdateDownloadResult = MoneroDaemonUpdateDownloadResult() result.is_update_available = True result.version = "0.18.5.1" result.hash = "a" * 64 @@ -311,14 +313,14 @@ def test_daemon_update_download_result_deserialize(self) -> None: AssertUtils.assert_serialization_integrity(result) def test_fee_estimate_deserialize(self) -> None: - estimate = MoneroFeeEstimate() + estimate: MoneroFeeEstimate = MoneroFeeEstimate() estimate.fee = 20000 estimate.quantization_mask = 10000 estimate.fees = [10000, 20000, 30000, 40000] AssertUtils.assert_serialization_integrity(estimate) def test_daemon_info_deserialize(self) -> None: - info = MoneroDaemonInfo() + info: MoneroDaemonInfo = MoneroDaemonInfo() info.credits = 0 info.top_block_hash = "a" * 64 info.version = "0.18.5.1" @@ -361,7 +363,7 @@ def test_daemon_info_invalid_network_type(self) -> None: MoneroDaemonInfo.deserialize('{"networkType":9}') def test_daemon_sync_info_deserialize(self) -> None: - info = MoneroDaemonSyncInfo() + info: MoneroDaemonSyncInfo = MoneroDaemonSyncInfo() info.credits = 0 info.top_block_hash = "a" * 64 info.height = 3000000 @@ -374,19 +376,19 @@ def test_daemon_sync_info_deserialize(self) -> None: @pytest.mark.xfail(reason="monero_daemon_sync_info::from_property_tree() bug", strict=True) def test_daemon_sync_info_peers_and_spans_deserialize(self) -> None: - info = MoneroDaemonSyncInfo() + info: MoneroDaemonSyncInfo = MoneroDaemonSyncInfo() info.peers = [MoneroPeer()] info.spans = [MoneroConnectionSpan()] - json_str = info.serialize() + json_str: str = info.serialize() logger.debug(f"Serialized daemon sync info: {json_str}") assert "peers" in json_str and "spans" in json_str - restored = MoneroDaemonSyncInfo.deserialize(json_str) + restored: MoneroDaemonSyncInfo = MoneroDaemonSyncInfo.deserialize(json_str) logger.debug(f"Deserialized daemon sync info re-serialized: {restored.serialize()}") assert len(restored.peers) == len(info.peers) assert len(restored.spans) == len(info.spans) def test_hard_fork_info_deserialize(self) -> None: - info = MoneroHardForkInfo() + info: MoneroHardForkInfo = MoneroHardForkInfo() info.credits = 0 info.top_block_hash = "a" * 64 info.earliest_height = 100000 @@ -400,7 +402,7 @@ def test_hard_fork_info_deserialize(self) -> None: AssertUtils.assert_serialization_integrity(info) def test_generate_blocks_result_deserialize(self) -> None: - result = MoneroGenerateBlocksResult() + result: MoneroGenerateBlocksResult = MoneroGenerateBlocksResult() result.block_hashes = ["a" * 64, "b" * 64] result.height = 12345 AssertUtils.assert_serialization_integrity(result) @@ -410,16 +412,16 @@ def test_generate_blocks_result_deserialize(self) -> None: #region Tx / output / key image def test_key_image_deserialize(self) -> None: - key_image = MoneroKeyImage() + key_image: MoneroKeyImage = MoneroKeyImage() key_image.hex = "a" * 64 key_image.signature = "b" * 128 AssertUtils.assert_serialization_integrity(key_image) def test_output_deserialize(self) -> None: - output = MoneroOutput() + output: MoneroOutput = MoneroOutput() output.amount = 1000000 output.index = 5 - key_image = MoneroKeyImage() + key_image: MoneroKeyImage = MoneroKeyImage() key_image.hex = "a" * 64 key_image.signature = "b" * 128 output.key_image = key_image @@ -436,7 +438,7 @@ def test_output_stealth_public_key_not_implemented(self) -> None: @pytest.mark.xfail(reason="monero_output::from_property_tree() bug", strict=True) def test_output_ring_output_indices_and_stealth_public_key_deserialize(self) -> None: - output = MoneroOutput() + output: MoneroOutput = MoneroOutput() output.amount = 1000000 output.index = 5 output.ring_output_indices = [10, 20, 30] @@ -444,7 +446,7 @@ def test_output_ring_output_indices_and_stealth_public_key_deserialize(self) -> AssertUtils.assert_serialization_integrity(output) def test_tx_deserialize(self) -> None: - tx = MoneroTx() + tx: MoneroTx = MoneroTx() tx.hash = "a" * 64 tx.is_miner_tx = False tx.payment_id = "b" * 16 @@ -495,7 +497,7 @@ def test_tx_unimplemented_fields(self, json_fragment: str) -> None: @pytest.mark.xfail(reason="monero_tx::from_property_tree() bug", strict=True) def test_tx_version_common_tx_sets_last_failed_and_max_used_block_height_deserialize(self) -> None: - tx = MoneroTx() + tx: MoneroTx = MoneroTx() tx.version = 2 tx.common_tx_sets = "sets" tx.last_failed_height = 100 @@ -504,26 +506,26 @@ def test_tx_version_common_tx_sets_last_failed_and_max_used_block_height_deseria @pytest.mark.xfail(reason="monero_tx::from_property_tree() bug", strict=True) def test_tx_ring_size_deserialize(self) -> None: - tx = MoneroTx() + tx: MoneroTx = MoneroTx() tx.ring_size = 16 AssertUtils.assert_serialization_integrity(tx) @pytest.mark.xfail(reason="monero_tx::from_property_tree() bug", strict=True) def test_tx_extra_deserialize(self) -> None: - tx = MoneroTx() + tx: MoneroTx = MoneroTx() tx.extra = [1, 2, 3, 255] AssertUtils.assert_serialization_integrity(tx) @pytest.mark.xfail(reason="monero_tx::from_property_tree() bug", strict=True) def test_tx_inputs_outputs_and_output_indices_deserialize(self) -> None: - tx = MoneroTx() + tx: MoneroTx = MoneroTx() tx.output_indices = [100, 101] - vin = MoneroOutput() + vin: MoneroOutput = MoneroOutput() vin.amount = 1 vin.key_image = MoneroKeyImage() vin.key_image.hex = "a" * 64 tx.inputs = [vin] - vout = MoneroOutput() + vout: MoneroOutput = MoneroOutput() vout.amount = 2 vout.index = 0 tx.outputs = [vout] @@ -534,7 +536,7 @@ def test_tx_inputs_outputs_and_output_indices_deserialize(self) -> None: #region Copy / merge / comparators def test_block_header_copy(self) -> None: - header = MoneroBlockHeader() + header: MoneroBlockHeader = MoneroBlockHeader() header.hash = "a" * 64 header.height = 100 header.timestamp = 1700000000 @@ -545,7 +547,7 @@ def test_block_header_copy(self) -> None: header.nonce = 12345 header.reward = 600000000000 - copy = header.copy() + copy: MoneroBlockHeader = header.copy() assert copy is not header assert copy.serialize() == header.serialize() @@ -554,12 +556,12 @@ def test_block_header_copy(self) -> None: assert header.height == 100 def test_block_header_merge(self) -> None: - a = MoneroBlockHeader() + a: MoneroBlockHeader = MoneroBlockHeader() a.hash = "a" * 64 a.height = 100 a.timestamp = 1700000000 - b = a.copy() + b: MoneroBlockHeader = a.copy() b.height = 200 # height can increase -> resolves to the higher value b.timestamp = 1800000000 # timestamp can increase -> resolves to the higher value b.size = 2000 # a.size is unset -> merge fills the gap @@ -573,36 +575,36 @@ def test_block_header_merge(self) -> None: def test_block_header_merge_conflict_raises(self) -> None: # fields without special reconciliation (e.g. hash) must match on both # sides, or merge() raises rather than silently picking one - a = MoneroBlockHeader() + a: MoneroBlockHeader = MoneroBlockHeader() a.hash = "a" * 64 - b = MoneroBlockHeader() + b: MoneroBlockHeader = MoneroBlockHeader() b.hash = "b" * 64 with pytest.raises(Exception, match="[Cc]annot reconcile"): a.merge(b) def test_block_copy(self) -> None: - block = MoneroBlock() + block: MoneroBlock = MoneroBlock() block.hash = "a" * 64 block.height = 100 block.hex = "deadbeef" block.tx_hashes = ["b" * 64, "c" * 64] - copy = block.copy() + copy: MoneroBlock = block.copy() assert copy is not block assert copy.serialize() == block.serialize() def test_block_merge(self) -> None: - a = MoneroBlock() + a: MoneroBlock = MoneroBlock() a.hash = "a" * 64 a.height = 100 - b = a.copy() + b: MoneroBlock = a.copy() b.hex = "deadbeef" # a.hex is unset -> merge fills the gap a.merge(b) assert a.hex == "deadbeef" - @pytest.mark.xfail(reason="merge_tx() dereferences m_hash unconditionally (boost::optional UB when unset); locally this just dedups wrongly, but the same NDEBUG/ODR-ambiguity root cause aborts the process in CI", strict=True) + @pytest.mark.xfail(reason="merge_tx() dereferences m_hash unconditionally (boost::optional UB when unset)", strict=True) def test_block_merge_txs_with_unset_hash_are_kept_distinct(self) -> None: - script = ( + script: LiteralString = ( "import monero, sys\n" "a = monero.MoneroBlock()\n" "a.height = 100\n" @@ -616,7 +618,7 @@ def test_block_merge_txs_with_unset_hash_are_kept_distinct(self) -> None: "n = len(a.txs) if a.txs else 0\n" "sys.exit(0 if n == 2 else f'txs not kept distinct: len={n}')\n" ) - result = subprocess.run([sys.executable, "-c", script], capture_output=True, text=True, timeout=30) + result: subprocess.CompletedProcess[str] = subprocess.run([sys.executable, "-c", script], capture_output=True, text=True, timeout=30) logger.debug(f"subprocess exit code: {result.returncode}, stderr: {result.stderr.strip()}") assert result.returncode == 0, ( f"Block.merge() did not keep unset-hash txs distinct (exit code {result.returncode}): " @@ -624,93 +626,93 @@ def test_block_merge_txs_with_unset_hash_are_kept_distinct(self) -> None: ) def test_tx_copy(self) -> None: - tx = MoneroTx() + tx: MoneroTx = MoneroTx() tx.hash = "a" * 64 tx.is_confirmed = True tx.fee = 7500000 - copy = tx.copy() + copy: MoneroTx = tx.copy() assert copy is not tx assert copy.serialize() == tx.serialize() def test_tx_merge(self) -> None: - a = MoneroTx() + a: MoneroTx = MoneroTx() a.hash = "a" * 64 a.is_confirmed = True # required: merge() dereferences is_confirmed directly a.fee = 7500000 - b = a.copy() + b: MoneroTx = a.copy() b.num_confirmations = 5 # a.num_confirmations is unset -> merge fills the gap a.merge(b) assert a.num_confirmations == 5 @pytest.mark.xfail(reason="gen_utils::reconcile() bug", strict=True) def test_tx_merge_is_confirmed_can_become_true(self) -> None: - a = MoneroTx() + a: MoneroTx = MoneroTx() a.hash = "a" * 64 a.is_confirmed = False - b = a.copy() + b: MoneroTx = a.copy() b.is_confirmed = True a.merge(b) assert a.is_confirmed is True @pytest.mark.xfail(reason="same gen_utils::reconcile() bug", strict=True) def test_tx_merge_is_double_spend_seen_can_become_true(self) -> None: - a = MoneroTx() + a: MoneroTx = MoneroTx() a.hash = "a" * 64 a.is_confirmed = True a.is_double_spend_seen = False - b = a.copy() + b: MoneroTx = a.copy() b.is_double_spend_seen = True a.merge(b) assert a.is_double_spend_seen is True @pytest.mark.xfail(reason="same gen_utils::reconcile() bug", strict=True) def test_tx_merge_in_tx_pool_can_become_true(self) -> None: - a = MoneroTx() + a: MoneroTx = MoneroTx() a.hash = "a" * 64 a.is_confirmed = False a.in_tx_pool = False - b = a.copy() + b: MoneroTx = a.copy() b.in_tx_pool = True a.merge(b) assert a.in_tx_pool is True def test_key_image_copy(self) -> None: - key_image = MoneroKeyImage() + key_image: MoneroKeyImage = MoneroKeyImage() key_image.hex = "a" * 64 key_image.signature = "b" * 128 - copy = key_image.copy() + copy: MoneroKeyImage = key_image.copy() assert copy is not key_image assert copy.serialize() == key_image.serialize() def test_key_image_merge(self) -> None: - a = MoneroKeyImage() + a: MoneroKeyImage = MoneroKeyImage() a.hex = "a" * 64 - b = a.copy() + b: MoneroKeyImage = a.copy() b.signature = "b" * 128 # a.signature is unset -> merge fills the gap a.merge(b) assert a.signature == "b" * 128 def test_output_copy(self) -> None: - output = MoneroOutput() + output: MoneroOutput = MoneroOutput() output.amount = 1000000 output.index = 5 - key_image = MoneroKeyImage() + key_image: MoneroKeyImage = MoneroKeyImage() key_image.hex = "a" * 64 output.key_image = key_image - copy = output.copy() + copy: MoneroOutput = output.copy() assert copy is not output assert copy.key_image is not output.key_image # key_image is deep copied assert copy.serialize() == output.serialize() def test_output_merge(self) -> None: - a = MoneroOutput() + a: MoneroOutput = MoneroOutput() a.amount = 1000000 a.index = 5 - b = a.copy() # preserves the (unset) tx reference, so merge won't recurse into tx merge + b: MoneroOutput = a.copy() # preserves the (unset) tx reference, so merge won't recurse into tx merge b.key_image = MoneroKeyImage() b.key_image.hex = "a" * 64 a.merge(b) # a.key_image is unset -> merge adopts b's key_image @@ -719,10 +721,10 @@ def test_output_merge(self) -> None: @pytest.mark.xfail(reason="monero_tx::merge() bug", strict=True) def test_tx_merge_extra_and_output_indices(self) -> None: - a = MoneroTx() + a: MoneroTx = MoneroTx() a.hash = "a" * 64 a.is_confirmed = True # required: merge() dereferences is_confirmed directly - b = a.copy() + b: MoneroTx = a.copy() b.extra = [1, 2, 3, 255] b.output_indices = [100, 101] a.merge(b) # a.extra/output_indices are unset -> merge should adopt b's @@ -731,10 +733,10 @@ def test_tx_merge_extra_and_output_indices(self) -> None: @pytest.mark.xfail(reason="monero_output::merge() bug", strict=True) def test_output_merge_ring_output_indices_and_stealth_public_key(self) -> None: - a = MoneroOutput() + a: MoneroOutput = MoneroOutput() a.amount = 1000000 a.index = 5 - b = a.copy() # preserves the (unset) tx reference, so merge won't recurse into tx merge + b: MoneroOutput = a.copy() # preserves the (unset) tx reference, so merge won't recurse into tx merge b.ring_output_indices = [10, 20, 30] b.stealth_public_key = "a" * 64 a.merge(b) # a.ring_output_indices/stealth_public_key are unset -> merge should adopt b's @@ -742,11 +744,11 @@ def test_output_merge_ring_output_indices_and_stealth_public_key(self) -> None: assert a.stealth_public_key == "a" * 64 def test_tx_lt_height_comparator(self) -> None: - tx_a = MoneroTx() + tx_a: MoneroTx = MoneroTx() tx_a.block = MoneroBlock() tx_a.block.height = 100 - tx_b = MoneroTx() + tx_b: MoneroTx = MoneroTx() tx_b.block = MoneroBlock() tx_b.block.height = 200 @@ -755,13 +757,13 @@ def test_tx_lt_height_comparator(self) -> None: assert TxHeightComparator.compare(tx_a, tx_b) assert not TxHeightComparator.compare(tx_b, tx_a) - txs = [tx_b, tx_a] + txs: list[MoneroTx] = [tx_b, tx_a] txs.sort() assert txs[0] is tx_a assert txs[1] is tx_b # unconfirmed (no block) transactions sort after confirmed ones - tx_unconfirmed = MoneroTx() + tx_unconfirmed: MoneroTx = MoneroTx() assert tx_a < tx_unconfirmed assert not (tx_unconfirmed < tx_a) diff --git a/tests/test_monero_daemon_rpc.py b/tests/test_monero_daemon_rpc.py index 1417cb4..6a4a414 100644 --- a/tests/test_monero_daemon_rpc.py +++ b/tests/test_monero_daemon_rpc.py @@ -12,7 +12,7 @@ MoneroHardForkInfo, MoneroAltChain, MoneroTx, MoneroSubmitTxResult, MoneroTxPoolStats, MoneroBan, MoneroTxConfig, MoneroDestination, MoneroWalletRpc, MoneroKeyImageSpentStatus, MoneroRpcConnection, - MoneroOutputHistogramEntry, MoneroOutputDistributionEntry + MoneroOutputHistogramEntry, MoneroOutputDistributionEntry, MoneroFeeEstimate ) from utils import ( TestUtils as Utils, TestContext, BinaryBlockContext, RpcConnectionUtils, @@ -99,15 +99,18 @@ def test_is_trusted(self, daemon: MoneroDaemonRpc) -> None: # Can get the blockchain height @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_height(self, daemon: MoneroDaemonRpc) -> None: - height = daemon.get_height() + height: int = daemon.get_height() + logger.debug(f"Daemon height: {height}") assert height > 0, "Height must be greater than 0" # Can get a block hash by height @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_block_id_by_height(self, daemon: MoneroDaemonRpc) -> None: last_header: MoneroBlockHeader = daemon.get_last_block_header() + logger.debug(f"Last block header height: {last_header.height}") assert last_header.height is not None hash_str: str = daemon.get_block_hash(last_header.height) + logger.debug(f"Got block hash: {hash_str}") assert hash_str is not None assert 64 == len(hash_str), f"Invalid block hash '{hash_str}'" @@ -126,7 +129,7 @@ def test_get_last_block_header(self, daemon: MoneroDaemonRpc) -> None: # Can get a block header by hash @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_block_header_by_hash(self, daemon: MoneroDaemonRpc) -> None: - # retrieve by hash of last block + # retrieve last block by hash last_header: MoneroBlockHeader = daemon.get_last_block_header() assert last_header.height is not None hash_str: str = daemon.get_block_hash(last_header.height) @@ -359,7 +362,7 @@ def test_get_tx_by_hash(self, daemon: MoneroDaemonRpc) -> None: # Can get transactions by hashes with and without pruning @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") - @pytest.mark.flaky(reruns=5, reruns_delay=5) + @pytest.mark.flaky(reruns=5, reruns_delay=5, only_rerun=[]) def test_get_txs_by_hashes(self, daemon: MoneroDaemonRpc, wallet: MoneroWalletRpc) -> None: # fetch tx hashses to test tx_hashes: list[str] = DaemonUtils.get_confirmed_tx_hashes(daemon) @@ -385,10 +388,10 @@ def test_get_txs_by_hashes(self, daemon: MoneroDaemonRpc, wallet: MoneroWalletRp TxUtils.test_tx(tx, ctx) # fetch missing hash - dest = MoneroDestination() + dest: MoneroDestination = MoneroDestination() dest.address = wallet.get_primary_address() dest.amount = TxWalletUtils.MAX_FEE - config = MoneroTxConfig() + config: MoneroTxConfig = MoneroTxConfig() config.account_index = 0 config.destinations.append(dest) tx = wallet.create_tx(config) @@ -519,7 +522,7 @@ def test_get_miner_tx_sum(self, daemon: MoneroDaemonRpc) -> None: # Can get fee estimate @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_fee_estimate(self, daemon: MoneroDaemonRpc) -> None: - fee_estimate = daemon.get_fee_estimate() + fee_estimate: MoneroFeeEstimate = daemon.get_fee_estimate() logger.debug(f"Testing fee estimate: {fee_estimate.serialize()}") GenUtils.test_unsigned_big_integer(fee_estimate.fee, True) assert len(fee_estimate.fees) == 4, "Exptected 4 fees" @@ -796,7 +799,7 @@ def test_get_alternative_block_ids(self, daemon: MoneroDaemonRpc) -> None: # Can get, set, and reset a download bandwidth limit @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") - @pytest.mark.flaky(reruns=3, reruns_delay=5) + @pytest.mark.flaky(reruns=5, reruns_delay=10, only_rerun=[]) def test_set_download_bandwidth(self, daemon: MoneroDaemonRpc) -> None: init_val: int = daemon.get_download_limit() assert init_val > 0 @@ -818,7 +821,7 @@ def test_set_download_bandwidth(self, daemon: MoneroDaemonRpc) -> None: # Can get, set, and reset an upload bandwidth limit @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") - @pytest.mark.flaky(reruns=3, reruns_delay=5) + @pytest.mark.flaky(reruns=5, reruns_delay=10, only_rerun=[]) def test_set_upload_bandwidth(self, daemon: MoneroDaemonRpc) -> None: init_val: int = daemon.get_upload_limit() assert init_val > 0 @@ -1014,7 +1017,7 @@ def test_get_mining_status(self, daemon: MoneroDaemonRpc, wallet: MoneroWalletRp # Can submit a mined block to the network @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") - @pytest.mark.flaky(reruns=5, reruns_delay=5) + @pytest.mark.flaky(reruns=5, reruns_delay=5, only_rerun=[]) def test_submit_mined_block(self, daemon: MoneroDaemonRpc) -> None: # get template to mine on template: MoneroBlockTemplate = daemon.get_block_template(Utils.ADDRESS) @@ -1044,14 +1047,14 @@ def test_prune_blockchain(self, daemon: MoneroDaemonRpc) -> None: # Can check for an update @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") - @pytest.mark.flaky(reruns=5, reruns_delay=5) + @pytest.mark.flaky(reruns=5, reruns_delay=5, only_rerun=[]) def test_check_for_update(self, daemon: MoneroDaemonRpc) -> None: result: MoneroDaemonUpdateCheckResult = daemon.check_for_update() DaemonUtils.test_update_check_result(result) # Can download an update @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") - @pytest.mark.flaky(reruns=5, reruns_delay=5) + @pytest.mark.flaky(reruns=5, reruns_delay=5, only_rerun=[]) def test_download_update(self, daemon: MoneroDaemonRpc) -> None: # download to default path result: MoneroDaemonUpdateDownloadResult = daemon.download_update() diff --git a/tests/test_monero_rpc_connection.py b/tests/test_monero_rpc_connection.py index 27a8e81..5bfa927 100644 --- a/tests/test_monero_rpc_connection.py +++ b/tests/test_monero_rpc_connection.py @@ -39,7 +39,7 @@ def wallet_connection(self) -> MoneroRpcConnection: @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_validate_uri(self) -> None: # test daemon uri - connection = MoneroRpcConnection(Utils.DAEMON_RPC_URI) + connection: MoneroRpcConnection = MoneroRpcConnection(Utils.DAEMON_RPC_URI) assert not connection.is_onion() assert not connection.is_i2p() @@ -104,7 +104,7 @@ def test_wallet_rpc_connection(self, wallet_connection: MoneroRpcConnection) -> # Test invalid connection @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_invalid_connection(self) -> None: - connection = MoneroRpcConnection(Utils.OFFLINE_SERVER_URI) + connection: MoneroRpcConnection = MoneroRpcConnection(Utils.OFFLINE_SERVER_URI) RpcConnectionUtils.test_rpc_connection(connection, Utils.OFFLINE_SERVER_URI, False, MoneroConnectionType.INVALID) # Can set credentials diff --git a/tests/test_monero_utils.py b/tests/test_monero_utils.py index 8255a62..6ce5439 100644 --- a/tests/test_monero_utils.py +++ b/tests/test_monero_utils.py @@ -45,7 +45,7 @@ def parse(cls, parser: ConfigParser) -> TestMoneroUtils.Config: :param ConfigParser parser: configuration parser. :returns TestMoneroUtils.Config: parsed test utils configuration. """ - config = cls() + config: TestMoneroUtils.Config = cls() # check section assert parser.has_section("serialization"), "Section [serialization] not found in test config" # load address books @@ -127,7 +127,7 @@ def test_serialize_heights_big(self) -> None: # can serialize height with large unsigned values def test_serialize_large_unsigned_values(self) -> None: - big_value = 18446744073709551615 # UINT64_MAX + big_value: int = 18446744073709551615 # UINT64_MAX json_map: dict[Any, Any] = { "heights": [big_value] } @@ -153,7 +153,7 @@ def test_serialize_text_short(self, config: TestMoneroUtils.Config) -> None: # Can serialize json with long text def test_serialize_text_long(self, config: TestMoneroUtils.Config) -> None: - msg = config.serialization_msg + msg: str = config.serialization_msg json_map: dict[str, str] = { "msg": f"{msg}\n" + f"{msg}\n" + @@ -378,14 +378,14 @@ def test_xmr_to_atomic_units_rounds_down_to_zero(self) -> None: # xmr_to_atomic_units() itself, assert the result lands on one of the # two atomic units the value sits between, not a specific platform's tie-break. def test_xmr_to_atomic_units_half_atomic_unit_rounding(self) -> None: - cases = [ + cases: list[tuple[float, int, int]] = [ (0.5e-12, 0, 1), (1.5e-12, 1, 2), (2.5e-12, 2, 3), (3.5e-12, 3, 4), ] for amount_xmr, floor_atomic, ceil_atomic in cases: - actual = MoneroUtils.xmr_to_atomic_units(amount_xmr) + actual: int = MoneroUtils.xmr_to_atomic_units(amount_xmr) logger.debug(f"xmr_to_atomic_units({amount_xmr!r}) = {actual} (expected {floor_atomic} or {ceil_atomic})") assert actual in (floor_atomic, ceil_atomic), f"xmr_to_atomic_units({amount_xmr!r}) == {actual}, expected {floor_atomic} or {ceil_atomic}" @@ -407,16 +407,16 @@ def test_xmr_to_atomic_units_overflow(self) -> None: # values comfortably below the uint64_t boundary succeed def test_xmr_to_atomic_units_near_uint64_max_boundary(self) -> None: - uint64_max = 2 ** 64 - 1 - boundary_xmr = uint64_max / 1e12 # ~18446744.073709551615 XMR - margin_xmr = 1.0 + uint64_max: int = 2 ** 64 - 1 + boundary_xmr: float = uint64_max / 1e12 # ~18446744.073709551615 XMR + margin_xmr: float = 1.0 - safely_below = boundary_xmr - margin_xmr - below = MoneroUtils.xmr_to_atomic_units(safely_below) + safely_below: float = boundary_xmr - margin_xmr + below: int = MoneroUtils.xmr_to_atomic_units(safely_below) logger.debug(f"xmr_to_atomic_units({safely_below!r}) = {below} (uint64_max = {uint64_max})") assert below <= uint64_max - safely_above = boundary_xmr + margin_xmr + safely_above: float = boundary_xmr + margin_xmr with pytest.raises(RuntimeError, match="amount exceeds maximum representable atomic units"): MoneroUtils.xmr_to_atomic_units(safely_above) @@ -467,40 +467,40 @@ def test_get_ring_size(self) -> None: #region Gather blocks def test_get_blocks_from_txs_dedup_and_order(self) -> None: - block1 = MoneroBlock() + block1: MoneroBlock = MoneroBlock() block1.height = 100 - block2 = MoneroBlock() + block2: MoneroBlock = MoneroBlock() block2.height = 200 - tx1 = MoneroTxWallet() + tx1: MoneroTxWallet = MoneroTxWallet() tx1.hash = "a" * 64 tx1.block = block1 - tx2 = MoneroTxWallet() + tx2: MoneroTxWallet = MoneroTxWallet() tx2.hash = "b" * 64 tx2.block = block2 - tx3 = MoneroTxWallet() # shares block1 with tx1 + tx3: MoneroTxWallet = MoneroTxWallet() # shares block1 with tx1 tx3.hash = "c" * 64 tx3.block = block1 - blocks = MoneroUtils.get_blocks_from_txs([tx1, tx2, tx3]) + blocks: list[MoneroBlock] = MoneroUtils.get_blocks_from_txs([tx1, tx2, tx3]) assert len(blocks) == 2 # block1 deduplicated despite appearing twice assert blocks[0] is block1 # blocks are returned in first-seen order assert blocks[1] is block2 def test_get_blocks_from_txs_unconfirmed_placeholder(self) -> None: - tx1 = MoneroTxWallet() + tx1: MoneroTxWallet = MoneroTxWallet() tx1.hash = "a" * 64 - tx2 = MoneroTxWallet() + tx2: MoneroTxWallet = MoneroTxWallet() tx2.hash = "b" * 64 assert tx1.block is None and tx2.block is None - blocks = MoneroUtils.get_blocks_from_txs([tx1, tx2]) + blocks: list[MoneroBlock] = MoneroUtils.get_blocks_from_txs([tx1, tx2]) # unconfirmed (blockless) txs are grouped under one shared placeholder block assert len(blocks) == 1 - placeholder = blocks[0] + placeholder: MoneroBlock = blocks[0] assert placeholder.height is None assert len(placeholder.txs) == 2 @@ -510,68 +510,68 @@ def test_get_blocks_from_txs_unconfirmed_placeholder(self) -> None: assert tx2.block is placeholder def test_get_blocks_from_txs_mixed_confirmed_and_unconfirmed(self) -> None: - block = MoneroBlock() + block: MoneroBlock = MoneroBlock() block.height = 100 - confirmed = MoneroTxWallet() + confirmed: MoneroTxWallet = MoneroTxWallet() confirmed.hash = "a" * 64 confirmed.block = block - unconfirmed1 = MoneroTxWallet() + unconfirmed1: MoneroTxWallet = MoneroTxWallet() unconfirmed1.hash = "b" * 64 - unconfirmed2 = MoneroTxWallet() + unconfirmed2: MoneroTxWallet = MoneroTxWallet() unconfirmed2.hash = "c" * 64 - blocks = MoneroUtils.get_blocks_from_txs([confirmed, unconfirmed1, unconfirmed2]) + blocks: list[MoneroBlock] = MoneroUtils.get_blocks_from_txs([confirmed, unconfirmed1, unconfirmed2]) assert len(blocks) == 2 # the real block, plus one shared unconfirmed placeholder assert blocks[0] is block assert blocks[1].height is None assert unconfirmed1.block is unconfirmed2.block is blocks[1] def test_get_blocks_from_transfers_dedup_and_order(self) -> None: - block1 = MoneroBlock() + block1: MoneroBlock = MoneroBlock() block1.height = 100 - block2 = MoneroBlock() + block2: MoneroBlock = MoneroBlock() block2.height = 200 - t1 = MoneroIncomingTransfer() + t1: MoneroIncomingTransfer = MoneroIncomingTransfer() t1.tx = MoneroTxWallet() t1.tx.hash = "a" * 64 t1.tx.block = block1 - t2 = MoneroIncomingTransfer() + t2: MoneroIncomingTransfer = MoneroIncomingTransfer() t2.tx = MoneroTxWallet() t2.tx.hash = "b" * 64 t2.tx.block = block2 - t3 = MoneroIncomingTransfer() # tx shares block1 with t1 + t3: MoneroIncomingTransfer = MoneroIncomingTransfer() # tx shares block1 with t1 t3.tx = MoneroTxWallet() t3.tx.hash = "c" * 64 t3.tx.block = block1 - blocks = MoneroUtils.get_blocks_from_transfers([t1, t2, t3]) + blocks: list[MoneroBlock] = MoneroUtils.get_blocks_from_transfers([t1, t2, t3]) assert len(blocks) == 2 assert blocks[0] is block1 assert blocks[1] is block2 def test_get_blocks_from_transfers_unconfirmed_placeholder(self) -> None: - t1 = MoneroIncomingTransfer() + t1: MoneroIncomingTransfer = MoneroIncomingTransfer() t1.tx = MoneroTxWallet() t1.tx.hash = "a" * 64 - t2 = MoneroIncomingTransfer() + t2: MoneroIncomingTransfer = MoneroIncomingTransfer() t2.tx = MoneroTxWallet() t2.tx.hash = "b" * 64 assert t1.tx.block is None and t2.tx.block is None - blocks = MoneroUtils.get_blocks_from_transfers([t1, t2]) + blocks: list[MoneroBlock] = MoneroUtils.get_blocks_from_transfers([t1, t2]) assert len(blocks) == 1 - placeholder = blocks[0] + placeholder: MoneroBlock = blocks[0] assert placeholder.height is None # side effect: mutates transfer.tx.block, same as get_blocks_from_txs() assert t1.tx.block is placeholder assert t2.tx.block is placeholder - @pytest.mark.xfail(reason="get_blocks_from_transfers() dereferences transfer.tx without a null check and segfaults the interpreter when it's unset; fixed upstream in the local everoddandeven/monero-cpp checkout, pending a submodule bump", strict=True) + @pytest.mark.xfail(reason="get_blocks_from_transfers() dereferences transfer.tx without a null check and segfaults the interpreter when it's unset", strict=True) def test_get_blocks_from_transfers_missing_tx_does_not_crash(self) -> None: # a transfer with no tx set is a legitimate, reachable state (it's just # never assigned), but get_blocks_from_transfers() used to dereference @@ -580,13 +580,13 @@ def test_get_blocks_from_transfers_missing_tx_does_not_crash(self) -> None: # subprocess so a regression here only kills a throwaway process # instead of the whole test run; fixed upstream in the local # everoddandeven/monero-cpp checkout, pending a submodule bump. - script = ( + script: str = ( "import monero\n" "t = monero.MoneroIncomingTransfer()\n" "t.amount = 500000\n" "monero.MoneroUtils.get_blocks_from_transfers([t])\n" ) - result = subprocess.run([sys.executable, "-c", script], capture_output=True, text=True, timeout=30) + result: subprocess.CompletedProcess[str] = subprocess.run([sys.executable, "-c", script], capture_output=True, text=True, timeout=30) logger.debug(f"subprocess exit code: {result.returncode}, stderr: {result.stderr.strip()}") assert result.returncode == 0, ( f"get_blocks_from_transfers() crashed the interpreter (exit code {result.returncode}) " @@ -594,26 +594,26 @@ def test_get_blocks_from_transfers_missing_tx_does_not_crash(self) -> None: ) def test_get_blocks_from_outputs_dedup_and_order(self) -> None: - block1 = MoneroBlock() + block1: MoneroBlock = MoneroBlock() block1.height = 100 - block2 = MoneroBlock() + block2: MoneroBlock = MoneroBlock() block2.height = 200 - tx1 = MoneroTxWallet() + tx1: MoneroTxWallet = MoneroTxWallet() tx1.hash = "a" * 64 tx1.block = block1 - tx2 = MoneroTxWallet() + tx2: MoneroTxWallet = MoneroTxWallet() tx2.hash = "b" * 64 tx2.block = block2 - o1 = MoneroOutputWallet() + o1: MoneroOutputWallet = MoneroOutputWallet() o1.tx = tx1 - o2 = MoneroOutputWallet() + o2: MoneroOutputWallet = MoneroOutputWallet() o2.tx = tx2 - o3 = MoneroOutputWallet() # tx shares block1 with o1 + o3: MoneroOutputWallet = MoneroOutputWallet() # tx shares block1 with o1 o3.tx = tx1 - blocks = MoneroUtils.get_blocks_from_outputs([o1, o2, o3]) + blocks: list[MoneroBlock] = MoneroUtils.get_blocks_from_outputs([o1, o2, o3]) assert len(blocks) == 2 assert blocks[0] is block1 assert blocks[1] is block2 @@ -622,7 +622,7 @@ def test_get_blocks_from_outputs_unconfirmed_raises(self) -> None: # unlike get_blocks_from_txs()/get_blocks_from_transfers(), an # unconfirmed (blockless) output's tx does not get a placeholder # block -- it raises instead - output = MoneroOutputWallet() + output: MoneroOutputWallet = MoneroOutputWallet() output.tx = MoneroTxWallet() output.tx.hash = "a" * 64 assert output.tx.block is None @@ -634,13 +634,13 @@ def test_get_blocks_from_outputs_unconfirmed_raises(self) -> None: def test_get_blocks_from_outputs_missing_tx_does_not_crash(self) -> None: # same crash as get_blocks_from_transfers(), for the same reason: # output.tx is a legitimate but unchecked null before the cast/dereference. - script = ( + script: str = ( "import monero\n" "o = monero.MoneroOutputWallet()\n" "o.amount = 1000000\n" "monero.MoneroUtils.get_blocks_from_outputs([o])\n" ) - result = subprocess.run([sys.executable, "-c", script], capture_output=True, text=True, timeout=30) + result: subprocess.CompletedProcess[str] = subprocess.run([sys.executable, "-c", script], capture_output=True, text=True, timeout=30) logger.debug(f"subprocess exit code: {result.returncode}, stderr: {result.stderr.strip()}") assert result.returncode == 0, ( f"get_blocks_from_outputs() crashed the interpreter (exit code {result.returncode}) " @@ -652,9 +652,9 @@ def test_get_blocks_from_outputs_missing_tx_does_not_crash(self) -> None: #region Free memory def test_free_block_breaks_tx_backlink(self) -> None: - block = MoneroBlock() + block: MoneroBlock = MoneroBlock() block.height = 100 - tx = MoneroTxWallet() + tx: MoneroTxWallet = MoneroTxWallet() tx.hash = "a" * 64 tx.block = block block.txs = [tx] @@ -663,16 +663,16 @@ def test_free_block_breaks_tx_backlink(self) -> None: assert tx.block is None def test_free_blocks_list(self) -> None: - block1 = MoneroBlock() + block1: MoneroBlock = MoneroBlock() block1.height = 1 - tx1 = MoneroTxWallet() + tx1: MoneroTxWallet = MoneroTxWallet() tx1.hash = "a" * 64 tx1.block = block1 block1.txs = [tx1] - block2 = MoneroBlock() + block2: MoneroBlock = MoneroBlock() block2.height = 2 - tx2 = MoneroTxWallet() + tx2: MoneroTxWallet = MoneroTxWallet() tx2.hash = "b" * 64 tx2.block = block2 block2.txs = [tx2] @@ -685,16 +685,16 @@ def test_free_tx_without_block_does_not_crash(self) -> None: # free(tx) creates a throwaway placeholder block for an unconfirmed # tx, then immediately frees it. Net no-op on tx.block, but exercises # that code path safely - tx = MoneroTxWallet() + tx: MoneroTxWallet = MoneroTxWallet() tx.hash = "a" * 64 assert tx.block is None MoneroUtils.free(tx) assert tx.block is None def test_free_tx_with_block(self) -> None: - block = MoneroBlock() + block: MoneroBlock = MoneroBlock() block.height = 100 - tx = MoneroTxWallet() + tx: MoneroTxWallet = MoneroTxWallet() tx.hash = "a" * 64 tx.block = block block.txs = [tx] @@ -703,14 +703,14 @@ def test_free_tx_with_block(self) -> None: assert tx.block is None def test_free_txs_list_confirmed_and_unconfirmed(self) -> None: - block = MoneroBlock() + block: MoneroBlock = MoneroBlock() block.height = 100 - confirmed = MoneroTxWallet() + confirmed: MoneroTxWallet = MoneroTxWallet() confirmed.hash = "a" * 64 confirmed.block = block block.txs = [confirmed] - unconfirmed = MoneroTxWallet() + unconfirmed: MoneroTxWallet = MoneroTxWallet() unconfirmed.hash = "b" * 64 MoneroUtils.free([confirmed, unconfirmed]) @@ -718,28 +718,28 @@ def test_free_txs_list_confirmed_and_unconfirmed(self) -> None: assert unconfirmed.block is None def test_free_transfers_list(self) -> None: - block = MoneroBlock() + block: MoneroBlock = MoneroBlock() block.height = 100 - tx = MoneroTxWallet() + tx: MoneroTxWallet = MoneroTxWallet() tx.hash = "a" * 64 tx.block = block block.txs = [tx] - transfer = MoneroIncomingTransfer() + transfer: MoneroIncomingTransfer = MoneroIncomingTransfer() transfer.tx = tx MoneroUtils.free([transfer]) assert transfer.tx.block is None def test_free_outputs_list(self) -> None: - block = MoneroBlock() + block: MoneroBlock = MoneroBlock() block.height = 100 - tx = MoneroTxWallet() + tx: MoneroTxWallet = MoneroTxWallet() tx.hash = "a" * 64 tx.block = block block.txs = [tx] - output = MoneroOutputWallet() + output: MoneroOutputWallet = MoneroOutputWallet() output.tx = tx MoneroUtils.free([output]) @@ -760,11 +760,11 @@ def rss_mb() -> float: def run_batch(rounds: int) -> None: for _ in range(rounds): for _ in range(50): - block = MoneroBlock() + block: MoneroBlock = MoneroBlock() block.height = 100 txs: list[MoneroTx] = [] for i in range(20): - tx = MoneroTxWallet() + tx: MoneroTxWallet = MoneroTxWallet() tx.hash = "a" * 63 + str(i % 10) tx.block = block txs.append(tx) @@ -774,53 +774,53 @@ def run_batch(rounds: int) -> None: gc.disable() try: run_batch(20) # unmeasured warmup: absorb one-time allocator growth - baseline = rss_mb() + baseline: float = rss_mb() run_batch(200) # measured: 200 * 50 * 20 = 200,000 tx objects - after = rss_mb() + after: float = rss_mb() finally: gc.enable() - growth_mb = after - baseline + growth_mb: float = after - baseline logger.debug(f"RSS growth after freeing 200,000 tx objects (post-warmup): {growth_mb:.1f} MB") # generous bound: a real leak of this shape grows ~1.2KB/tx (~240MB for # 200,000 tx); this only needs to rule out that magnitude of leak, not # pin down normal allocator noise assert growth_mb < 100, f"RSS grew {growth_mb:.1f} MB after freeing 200,000 tx objects -- possible leak" - @pytest.mark.xfail(reason="monero_utils::free(block)/free(tx) dereference their argument without a null check and segfault the interpreter when it's None; fixed upstream in the local everoddandeven/monero-cpp checkout, pending a submodule bump", strict=True) + @pytest.mark.xfail(reason="monero_utils::free(block)/free(tx) dereference their argument without a null check and segfault when it's None", strict=True) def test_free_none_does_not_crash(self) -> None: - script = "import monero\nmonero.MoneroUtils.free(None)\n" - result = subprocess.run([sys.executable, "-c", script], capture_output=True, text=True, timeout=30) + script: str = "import monero\nmonero.MoneroUtils.free(None)\n" + result: subprocess.CompletedProcess[str] = subprocess.run([sys.executable, "-c", script], capture_output=True, text=True, timeout=30) logger.debug(f"subprocess exit code: {result.returncode}, stderr: {result.stderr.strip()}") assert result.returncode == 0, ( f"free(None) crashed the interpreter (exit code {result.returncode}) " "instead of raising a Python exception (or being a documented no-op)" ) - @pytest.mark.xfail(reason="free(transfers) delegates to get_blocks_from_transfers(), which segfaults on a transfer with no tx set; fixed upstream in the local everoddandeven/monero-cpp checkout, pending a submodule bump", strict=True) + @pytest.mark.xfail(reason="free(transfers) delegates to get_blocks_from_transfers(), which segfaults on a transfer with no tx set", strict=True) def test_free_transfers_missing_tx_does_not_crash(self) -> None: - script = ( + script: str = ( "import monero\n" "t = monero.MoneroIncomingTransfer()\n" "t.amount = 500000\n" "monero.MoneroUtils.free([t])\n" ) - result = subprocess.run([sys.executable, "-c", script], capture_output=True, text=True, timeout=30) + result: subprocess.CompletedProcess[str] = subprocess.run([sys.executable, "-c", script], capture_output=True, text=True, timeout=30) logger.debug(f"subprocess exit code: {result.returncode}, stderr: {result.stderr.strip()}") assert result.returncode == 0, ( f"free([transfer]) crashed the interpreter (exit code {result.returncode}) " "instead of raising a Python exception for a transfer with no tx set" ) - @pytest.mark.xfail(reason="free(outputs) delegates to get_blocks_from_outputs(), which segfaults on an output with no tx set; fixed upstream in the local everoddandeven/monero-cpp checkout, pending a submodule bump", strict=True) + @pytest.mark.xfail(reason="free(outputs) delegates to get_blocks_from_outputs(), which segfaults on an output with no tx set", strict=True) def test_free_outputs_missing_tx_does_not_crash(self) -> None: - script = ( + script: str = ( "import monero\n" "o = monero.MoneroOutputWallet()\n" "o.amount = 1000000\n" "monero.MoneroUtils.free([o])\n" ) - result = subprocess.run([sys.executable, "-c", script], capture_output=True, text=True, timeout=30) + result: subprocess.CompletedProcess[str] = subprocess.run([sys.executable, "-c", script], capture_output=True, text=True, timeout=30) logger.debug(f"subprocess exit code: {result.returncode}, stderr: {result.stderr.strip()}") assert result.returncode == 0, ( f"free([output]) crashed the interpreter (exit code {result.returncode}) " diff --git a/tests/test_monero_wallet_common.py b/tests/test_monero_wallet_common.py index fc7f208..11d1b7f 100644 --- a/tests/test_monero_wallet_common.py +++ b/tests/test_monero_wallet_common.py @@ -19,7 +19,9 @@ MoneroTxWallet, MoneroOutputWallet, MoneroTx, MoneroAccount, MoneroSubaddress, MoneroMessageSignatureType, MoneroTxPriority, MoneroFeeEstimate, MoneroIntegratedAddress, MoneroCheckTx, MoneroCheckReserve, MoneroAddressBookEntry, - MoneroSubmitTxResult, MoneroAccountTag, MoneroKeyImageExportResult + MoneroSubmitTxResult, MoneroAccountTag, MoneroKeyImageExportResult, MoneroWalletFull, + MoneroKeyImageImportResult, MoneroMessageSignatureResult, + MoneroMiningStatus, MoneroVersion, MoneroSyncResult, ) from utils import ( MultisigSampleCodeTester, TestUtils, WalletEqualityUtils, @@ -63,11 +65,11 @@ def parse(cls, parser: ConfigParser) -> BaseTestMoneroWallet.Config: :param ConfigParser parser: configuration parser. :returns BaseTestMoneroWallet.Config: wallet test configuration. """ - section = "test_create_wallet_from_seed" + section: str = "test_create_wallet_from_seed" if not parser.has_section(section): # raise exception if section not found raise Exception(f"Cannot find section '{section}' in test_monero_wallet_common.ini") - config = cls() + config: BaseTestMoneroWallet.Config = cls() # parse configuration config.seed = parser.get(section, "seed") return config @@ -144,7 +146,7 @@ def _open_wallet_from_path(self, path: str, password: str | None) -> MoneroWalle :param str | None password: wallet password. :returns MoneroWallet: opened wallet. """ - config = MoneroWalletConfig() + config: MoneroWalletConfig = MoneroWalletConfig() config.path = path config.password = password @@ -201,16 +203,16 @@ def after_all(self) -> None: MiningUtils.try_stop_mining(daemon) # close wallet - wallet = self.get_test_wallet() + wallet: MoneroWallet = self.get_test_wallet() wallet.close(self.supports_save()) # Before each test @override def before_each(self, request: pytest.FixtureRequest) -> None: super().before_each(request) - daemon = self._get_test_daemon() - wallet = self.get_test_wallet() - status = daemon.get_mining_status() + daemon: MoneroDaemonRpc = self._get_test_daemon() + wallet: MoneroWallet = self.get_test_wallet() + status: MoneroMiningStatus = daemon.get_mining_status() if status.is_active is True: wallet.stop_mining() @@ -219,8 +221,8 @@ def before_each(self, request: pytest.FixtureRequest) -> None: @override def after_each(self, request: pytest.FixtureRequest) -> None: super().after_each(request) - daemon = self._get_test_daemon() - status = daemon.get_mining_status() + daemon: MoneroDaemonRpc = self._get_test_daemon() + status: MoneroMiningStatus = daemon.get_mining_status() if status.is_active is True: logger.warning(f"Mining is active after test {request.node.name}") # type: ignore @@ -233,11 +235,11 @@ def after_each(self, request: pytest.FixtureRequest) -> None: # Validates inputs when sending funds @pytest.mark.skipif(TestUtils.TEST_RELAYS is False, reason="TEST_RELAYS disabled") - @pytest.mark.flaky(reruns=5, reruns_delay=5) + @pytest.mark.flaky(reruns=5, reruns_delay=5, only_rerun=[]) def test_validate_inputs_sending_funds(self, wallet: MoneroWallet) -> None: # try sending with invalid address try: - tx_config = MoneroTxConfig() + tx_config: MoneroTxConfig = MoneroTxConfig() tx_config.address = "my invalid address" tx_config.account_index = 0 tx_config.amount = TxWalletUtils.MAX_FEE @@ -385,19 +387,19 @@ def test_send_to_self(self, wallet: MoneroWallet) -> None: TestUtils.WALLET_TX_TRACKER.wait_for_unlocked_balance(wallet, 0, None, amount) # collect sender balances before - balance1 = wallet.get_balance() - unlocked_balance1 = wallet.get_unlocked_balance() + balance1: int = wallet.get_balance() + unlocked_balance1: int = wallet.get_unlocked_balance() # test error sending funds to self with integrated subaddress # TODO (monero-project): sending funds to self # with integrated subaddress throws error: https://github.com/monero-project/monero/issues/8380 try: - tx_config = MoneroTxConfig() + tx_config: MoneroTxConfig = MoneroTxConfig() tx_config.account_index = 0 - subaddress = wallet.get_subaddress(0, 1) + subaddress: MoneroSubaddress = wallet.get_subaddress(0, 1) assert subaddress.address is not None - address = subaddress.address + address: str = subaddress.address tx_config.address = MoneroUtils.get_integrated_address(TestUtils.NETWORK_TYPE, address, '').integrated_address tx_config.amount = amount tx_config.relay = True @@ -414,7 +416,7 @@ def test_send_to_self(self, wallet: MoneroWallet) -> None: tx_config.amount = amount tx_config.relay = True - tx = wallet.create_tx(tx_config) + tx: MoneroTxWallet = wallet.create_tx(tx_config) # test balances after balance2: int = wallet.get_balance() @@ -423,7 +425,7 @@ def test_send_to_self(self, wallet: MoneroWallet) -> None: # unlocked balance should decrease assert unlocked_balance2 < unlocked_balance1 assert tx.fee is not None - expected_balance = balance1 - tx.fee + expected_balance: int = balance1 - tx.fee assert expected_balance == balance2, "Balance after send was not balance before - fee" # Can send to external address @@ -457,12 +459,12 @@ def test_send_to_external(self, wallet: MoneroWallet) -> None: # unlocked balance should decrease assert unlocked_balance2 < unlocked_balance1 assert tx.fee is not None - expected_balance = balance1 - tx.get_outgoing_amount() - tx.fee + expected_balance: int = balance1 - tx.get_outgoing_amount() - tx.fee assert expected_balance == balance2, "Balance after send was not balance before - net tx amount - fee (5 - 1 != 4 test)" tx_query: MoneroTxQuery = MoneroTxQuery() tx_query.is_confirmed = False - txs = wallet.get_txs(tx_query) + txs: list[MoneroTxWallet] = wallet.get_txs(tx_query) assert len(txs) > 0 # test recipient balance after @@ -675,6 +677,7 @@ def test_sweep_dust(self, wallet: MoneroWallet) -> None: # Can update a locked tx sent from/to the same account as blocks are added to the chain @pytest.mark.skipif(TestUtils.TEST_RELAYS is False, reason="TEST_RELAYS disabled") @pytest.mark.skipif(TestUtils.TEST_NOTIFICATIONS is False, reason="TEST_NOTIFICATIONS disabled") + @pytest.mark.flaky(reruns=5, reruns_delay=10, only_rerun=["BUSY", r"Cannot reconcile integrals:.*m_is_incoming"]) def test_update_locked_same_account(self, daemon: MoneroDaemonRpc, wallet: MoneroWallet) -> None: config: MoneroTxConfig = MoneroTxConfig() config.address = wallet.get_primary_address() @@ -688,6 +691,7 @@ def test_update_locked_same_account(self, daemon: MoneroDaemonRpc, wallet: Moner @pytest.mark.skipif(TestUtils.TEST_RELAYS is False, reason="TEST_RELAYS disabled") @pytest.mark.skipif(TestUtils.TEST_NOTIFICATIONS is False, reason="TEST_NOTIFICATIONS disabled") @pytest.mark.skipif(TestUtils.LITE_MODE, reason="LITE_MODE enabled") + @pytest.mark.flaky(reruns=5, reruns_delay=10, only_rerun=["BUSY", r"Cannot reconcile integrals:.*m_is_incoming"]) def test_update_locked_same_account_split(self, daemon: MoneroDaemonRpc, wallet: MoneroWallet) -> None: config: MoneroTxConfig = MoneroTxConfig() config.address = wallet.get_primary_address() @@ -702,7 +706,7 @@ def test_update_locked_same_account_split(self, daemon: MoneroDaemonRpc, wallet: @pytest.mark.skipif(TestUtils.TEST_RELAYS is False, reason="TEST_RELAYS disabled") @pytest.mark.skipif(TestUtils.TEST_NOTIFICATIONS is False, reason="TEST_NOTIFICATIONS disabled") @pytest.mark.skipif(TestUtils.LITE_MODE, reason="LITE_MODE enabled") - @pytest.mark.flaky(reruns=3, reruns_delay=5) + @pytest.mark.flaky(reruns=5, reruns_delay=10, only_rerun=["BUSY", r"Cannot reconcile integrals:.*m_is_incoming"]) def test_update_locked_different_accounts(self, daemon: MoneroDaemonRpc, wallet: MoneroWallet) -> None: config: MoneroTxConfig = MoneroTxConfig() config.address = wallet.get_subaddress(1, 0).address @@ -716,7 +720,7 @@ def test_update_locked_different_accounts(self, daemon: MoneroDaemonRpc, wallet: @pytest.mark.skipif(TestUtils.TEST_RELAYS is False, reason="TEST_RELAYS disabled") @pytest.mark.skipif(TestUtils.TEST_NOTIFICATIONS is False, reason="TEST_NOTIFICATIONS disabled") @pytest.mark.skipif(TestUtils.LITE_MODE, reason="LITE_MODE enabled") - @pytest.mark.flaky(reruns=3, reruns_delay=5) + @pytest.mark.flaky(reruns=5, reruns_delay=10, only_rerun=["BUSY", r"Cannot reconcile integrals:.*m_is_incoming"]) def test_update_locked_different_accounts_split(self, daemon: MoneroDaemonRpc, wallet: MoneroWallet) -> None: config: MoneroTxConfig = MoneroTxConfig() config.address = wallet.get_subaddress(1, 0).address @@ -733,14 +737,14 @@ def test_update_locked_different_accounts_split(self, daemon: MoneroDaemonRpc, w # Can get the daemon's max peer height @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_daemon_max_peer_height(self, wallet: MoneroWallet) -> None: - height = wallet.get_daemon_max_peer_height() + height: int = wallet.get_daemon_max_peer_height() assert height > 0 # Can get the daemon's height @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_daemon_height(self, wallet: MoneroWallet) -> None: assert wallet.is_connected_to_daemon(), "Wallet is not connected to daemon" - daemon_height = wallet.get_daemon_height() + daemon_height: int = wallet.get_daemon_height() assert daemon_height > 0 # Can create a random wallet @@ -749,9 +753,9 @@ def test_create_wallet_random(self) -> None: """ Can create a random wallet. """ - config = MoneroWalletConfig() - wallet = self._create_wallet(config) - path = wallet.get_path() + config: MoneroWalletConfig = MoneroWalletConfig() + wallet: MoneroWallet = self._create_wallet(config) + path: str = wallet.get_path() try: MoneroUtils.validate_address(wallet.get_primary_address(), TestUtils.NETWORK_TYPE) @@ -788,17 +792,17 @@ def test_create_wallet_random(self) -> None: @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_create_wallet_from_seed(self, wallet: MoneroWallet, test_config: BaseTestMoneroWallet.Config) -> None: # save for comparison - primary_address = wallet.get_primary_address() - private_view_key = wallet.get_private_view_key() - private_spend_key = wallet.get_private_spend_key() + primary_address: str = wallet.get_primary_address() + private_view_key: str = wallet.get_private_view_key() + private_spend_key: str = wallet.get_private_spend_key() # recreate test wallet from seed - config = MoneroWalletConfig() + config: MoneroWalletConfig = MoneroWalletConfig() config.seed = TestUtils.SEED config.restore_height = TestUtils.FIRST_RECEIVE_HEIGHT w: MoneroWallet = self._create_wallet(config) - path = w.get_path() + path: str = w.get_path() try: assert primary_address == w.get_primary_address() assert private_view_key == w.get_private_view_key() @@ -833,7 +837,7 @@ def test_create_wallet_from_seed(self, wallet: MoneroWallet, test_config: BaseTe @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_create_wallet_from_seed_with_offset(self) -> None: # create test wallet with offset - config = MoneroWalletConfig() + config: MoneroWalletConfig = MoneroWalletConfig() config.seed = TestUtils.SEED config.restore_height = TestUtils.FIRST_RECEIVE_HEIGHT config.seed_offset = "my secret offset!" @@ -853,18 +857,18 @@ def test_create_wallet_from_seed_with_offset(self) -> None: @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_create_wallet_from_keys(self, daemon: MoneroDaemonRpc, wallet: MoneroWallet) -> None: # save for comparison - primary_address = wallet.get_primary_address() - private_view_key = wallet.get_private_view_key() - private_spend_key = wallet.get_private_spend_key() + primary_address: str = wallet.get_primary_address() + private_view_key: str = wallet.get_private_view_key() + private_spend_key: str = wallet.get_private_spend_key() # recreate test wallet from keys - config = MoneroWalletConfig() + config: MoneroWalletConfig = MoneroWalletConfig() config.primary_address = primary_address config.private_view_key = private_view_key config.private_spend_key = private_spend_key config.restore_height = daemon.get_height() w: MoneroWallet = self._create_wallet(config) - path = w.get_path() + path: str = w.get_path() try: assert primary_address == w.get_primary_address() @@ -919,15 +923,15 @@ def test_subaddress_lookahead(self, wallet: MoneroWallet) -> None: receiver: MoneroWallet | None = None try: # create wallet with high subaddress lookahead - config = MoneroWalletConfig() + config: MoneroWalletConfig = MoneroWalletConfig() config.account_lookahead = 1 config.subaddress_lookahead = 100000 receiver = self._create_wallet(config) # transfer funds to subaddress with high index - tx_config = MoneroTxConfig() + tx_config: MoneroTxConfig = MoneroTxConfig() tx_config.account_index = 0 - dest = MoneroDestination() + dest: MoneroDestination = MoneroDestination() dest.address = receiver.get_subaddress(0, 85000).address dest.amount = TxWalletUtils.MAX_FEE tx_config.destinations.append(dest) @@ -946,7 +950,7 @@ def test_subaddress_lookahead(self, wallet: MoneroWallet) -> None: # Can get the wallet's version @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_version(self, wallet: MoneroWallet) -> None: - version = wallet.get_version() + version: MoneroVersion = wallet.get_version() assert version.number is not None assert version.number > 0 assert version.is_release is not None @@ -955,16 +959,16 @@ def test_get_version(self, wallet: MoneroWallet) -> None: @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_path(self) -> None: # create random wallet - config = MoneroWalletConfig() - wallet = self._create_wallet(config) + config: MoneroWalletConfig = MoneroWalletConfig() + wallet: MoneroWallet = self._create_wallet(config) # set a random attribute #String uuid = UUID.randomUUID().toString() - uuid = StringUtils.get_random_string() + uuid: str = StringUtils.get_random_string() wallet.set_attribute("uuid", uuid) # record the wallet's path then save and close - path = wallet.get_path() + path: str = wallet.get_path() self._close_wallet(wallet, True) # re-open the wallet using its path @@ -978,10 +982,10 @@ def test_get_path(self) -> None: @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_set_daemon_connection(self) -> None: # create random wallet with default daemon connection - config = MoneroWalletConfig() - wallet = self._create_wallet(config) - daemon_rpc_uri = self.get_daemon_rpc_uri() - connection = MoneroRpcConnection( + config: MoneroWalletConfig = MoneroWalletConfig() + wallet: MoneroWallet = self._create_wallet(config) + daemon_rpc_uri: str = self.get_daemon_rpc_uri() + connection: MoneroRpcConnection = MoneroRpcConnection( daemon_rpc_uri, TestUtils.DAEMON_RPC_USERNAME, TestUtils.DAEMON_RPC_PASSWORD ) AssertUtils.assert_equals(connection, wallet.get_daemon_connection()) @@ -1051,20 +1055,20 @@ def test_set_daemon_connection(self) -> None: # Can get the seed @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_seed(self, wallet: MoneroWallet) -> None: - seed = wallet.get_seed() + seed: str = wallet.get_seed() MoneroUtils.validate_mnemonic(seed) assert TestUtils.SEED == seed # Can get the language of the seed @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_seed_language(self, wallet: MoneroWallet) -> None: - language = wallet.get_seed_language() + language: str = wallet.get_seed_language() assert MoneroWallet.DEFAULT_LANGUAGE == language # Can get a list of supported languages for the seed @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_seed_languages(self) -> None: - languages = self._get_seed_languages() + languages: list[str] = self._get_seed_languages() assert len(languages) > 0 for language in languages: assert len(language) > 0 @@ -1072,31 +1076,31 @@ def test_get_seed_languages(self) -> None: # Can get the private view key @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_private_view_key(self, wallet: MoneroWallet) -> None: - private_view_key = wallet.get_private_view_key() + private_view_key: str = wallet.get_private_view_key() MoneroUtils.validate_private_view_key(private_view_key) # Can get the private spend key @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_private_spend_key(self, wallet: MoneroWallet) -> None: - private_spend_key = wallet.get_private_spend_key() + private_spend_key: str = wallet.get_private_spend_key() MoneroUtils.validate_private_spend_key(private_spend_key) # Can get the public view key @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_public_view_key(self, wallet: MoneroWallet) -> None: - public_view_key = wallet.get_public_view_key() + public_view_key: str = wallet.get_public_view_key() MoneroUtils.validate_private_spend_key(public_view_key) # Can get the public spend key @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_public_spend_key(self, wallet: MoneroWallet) -> None: - public_spend_key = wallet.get_public_spend_key() + public_spend_key: str = wallet.get_public_spend_key() MoneroUtils.validate_private_spend_key(public_spend_key) # Can get the primary address @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_primary_address(self, wallet: MoneroWallet) -> None: - primary_address = wallet.get_primary_address() + primary_address: str = wallet.get_primary_address() MoneroUtils.validate_address(primary_address, TestUtils.NETWORK_TYPE) assert wallet.get_address(0, 0) == primary_address @@ -1113,10 +1117,10 @@ def test_get_subaddress_address(self, wallet: MoneroWallet) -> None: # Can get addresses out of range of used accounts and subaddresses @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_subaddress_address_out_of_range(self, wallet: MoneroWallet) -> None: - accounts = wallet.get_accounts(True) - account_idx = len(accounts) - 1 - subaddress_idx = len(accounts[account_idx].subaddresses) - address = wallet.get_address(account_idx, subaddress_idx) + accounts: list[MoneroAccount] = wallet.get_accounts(True) + account_idx: int = len(accounts) - 1 + subaddress_idx: int = len(accounts[account_idx].subaddresses) + address: str = wallet.get_address(account_idx, subaddress_idx) assert address is not None assert len(address) > 0 @@ -1124,15 +1128,15 @@ def test_get_subaddress_address_out_of_range(self, wallet: MoneroWallet) -> None @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_address_indices(self, wallet: MoneroWallet) -> None: # get last subaddress to test - accounts = wallet.get_accounts(True) - account_idx = len(accounts) - 1 - subaddress_idx = len(accounts[account_idx].subaddresses) - 1 - address = wallet.get_address(account_idx, subaddress_idx) + accounts: list[MoneroAccount] = wallet.get_accounts(True) + account_idx: int = len(accounts) - 1 + subaddress_idx: int = len(accounts[account_idx].subaddresses) - 1 + address: str = wallet.get_address(account_idx, subaddress_idx) assert address is not None assert len(address) > 0 # get address index - subaddress = wallet.get_address_index(address) + subaddress: MoneroSubaddress = wallet.get_address_index(address) assert account_idx == subaddress.account_index assert subaddress_idx == subaddress.index @@ -1155,8 +1159,8 @@ def test_get_address_indices(self, wallet: MoneroWallet) -> None: # Can decode an integrated address @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_decode_integrated_address(self, wallet: MoneroWallet) -> None: - integrated_address = wallet.get_integrated_address('', "03284e41c342f036") - decoded_address = wallet.decode_integrated_address(integrated_address.integrated_address) + integrated_address: MoneroIntegratedAddress = wallet.get_integrated_address('', "03284e41c342f036") + decoded_address: MoneroIntegratedAddress = wallet.decode_integrated_address(integrated_address.integrated_address) AssertUtils.assert_equals(integrated_address, decoded_address) # decode invalid address @@ -1178,10 +1182,10 @@ def test_decode_integrated_address(self, wallet: MoneroWallet) -> None: # TODO test syncing from start height @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_sync_without_progress(self, daemon: MoneroDaemonRpc, wallet: MoneroWallet) -> None: - num_blocks = 100 - chain_height = daemon.get_height() + num_blocks: int = 100 + chain_height: int = daemon.get_height() assert chain_height >= num_blocks - result = wallet.sync(chain_height - num_blocks) # sync end of chain + result: MoneroSyncResult = wallet.sync(chain_height - num_blocks) # sync end of chain assert result.num_blocks_fetched >= 0 assert result.received_money is not None @@ -1189,7 +1193,7 @@ def test_sync_without_progress(self, daemon: MoneroDaemonRpc, wallet: MoneroWall @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_wallet_equality_ground_truth(self, wallet: MoneroWallet) -> None: TestUtils.WALLET_TX_TRACKER.wait_for_txs_to_clear_pool([wallet]) - wallet_gt = TestUtils.create_wallet_ground_truth( + wallet_gt: MoneroWalletFull = TestUtils.create_wallet_ground_truth( TestUtils.NETWORK_TYPE, TestUtils.SEED, None, TestUtils.FIRST_RECEIVE_HEIGHT ) try: @@ -1200,7 +1204,7 @@ def test_wallet_equality_ground_truth(self, wallet: MoneroWallet) -> None: # Can get the current height that the wallet is synchronized to @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_height(self, wallet: MoneroWallet) -> None: - height = wallet.get_height() + height: int = wallet.get_height() assert height >= 0 # Can get a blockchain height by date @@ -1221,7 +1225,7 @@ def test_get_height_by_date(self, wallet: MoneroWallet) -> None: # test heights by date last_height: Optional[int] = None for date in dates: - height = wallet.get_height_by_date(date.year + 1900, date.month + 1, date.day) + height: int = wallet.get_height_by_date(date.year + 1900, date.month + 1, date.day) assert (height >= 0) if last_height is not None: assert (height >= last_height) @@ -1234,7 +1238,7 @@ def test_get_height_by_date(self, wallet: MoneroWallet) -> None: # test future date try: - tomorrow = datetime.fromtimestamp((yesterday + day_ms * 2) / 1000) + tomorrow: datetime = datetime.fromtimestamp((yesterday + day_ms * 2) / 1000) wallet.get_height_by_date(tomorrow.year + 1900, tomorrow.month + 1, tomorrow.day) raise Exception("Expected exception on future date") except MoneroError as err: @@ -1244,10 +1248,10 @@ def test_get_height_by_date(self, wallet: MoneroWallet) -> None: @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_all_balances(self, wallet: MoneroWallet) -> None: # fetch accounts with all info as reference - accounts = wallet.get_accounts(True) + accounts: list[MoneroAccount] = wallet.get_accounts(True) # test that balances add up between accounts and wallet - accounts_balance = 0 - accounts_unlocked_balance = 0 + accounts_balance: int = 0 + accounts_unlocked_balance: int = 0 for account in accounts: assert account.index is not None assert account.balance is not None @@ -1256,8 +1260,8 @@ def test_get_all_balances(self, wallet: MoneroWallet) -> None: accounts_unlocked_balance += account.unlocked_balance # test that balances add up between subaddresses and accounts - subaddresses_balance = 0 - subaddresses_unlocked_balance = 0 + subaddresses_balance: int = 0 + subaddresses_unlocked_balance: int = 0 for subaddress in account.subaddresses: assert subaddress.account_index is not None assert subaddress.index is not None @@ -1268,7 +1272,7 @@ def test_get_all_balances(self, wallet: MoneroWallet) -> None: # test that balances are consistent with get_accounts() call assert wallet.get_balance(subaddress.account_index, subaddress.index) == subaddress.balance - unlocked_balance = wallet.get_unlocked_balance(subaddress.account_index, subaddress.index) + unlocked_balance: int = wallet.get_unlocked_balance(subaddress.account_index, subaddress.index) assert unlocked_balance == subaddress.unlocked_balance assert wallet.get_balance(account.index) == subaddresses_balance @@ -1282,7 +1286,7 @@ def test_get_all_balances(self, wallet: MoneroWallet) -> None: # Can get accounts without subaddresses @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_accounts_without_subaddresses(self, wallet: MoneroWallet) -> None: - accounts = wallet.get_accounts() + accounts: list[MoneroAccount] = wallet.get_accounts() assert len(accounts) > 0 for account in accounts: WalletUtils.test_account(account, TestUtils.NETWORK_TYPE) @@ -1291,7 +1295,7 @@ def test_get_accounts_without_subaddresses(self, wallet: MoneroWallet) -> None: # Can get accounts with subaddress @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_accounts_with_subaddresses(self, wallet: MoneroWallet) -> None: - accounts = wallet.get_accounts(True) + accounts: list[MoneroAccount] = wallet.get_accounts(True) assert len(accounts) > 0 for account in accounts: WalletUtils.test_account(account, TestUtils.NETWORK_TYPE) @@ -1300,14 +1304,14 @@ def test_get_accounts_with_subaddresses(self, wallet: MoneroWallet) -> None: # Can get an account at a specified index @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_account(self, wallet: MoneroWallet) -> None: - accounts = wallet.get_accounts() + accounts: list[MoneroAccount] = wallet.get_accounts() assert len(accounts) > 0 for account in accounts: WalletUtils.test_account(account, TestUtils.NETWORK_TYPE) # test without subaddresses assert account.index is not None - retrieved = wallet.get_account(account.index) + retrieved: MoneroAccount = wallet.get_account(account.index) assert len(retrieved.subaddresses) == 0 # test with subaddresses @@ -1317,8 +1321,8 @@ def test_get_account(self, wallet: MoneroWallet) -> None: # Can create a new account without a label @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_create_account_without_label(self, wallet: MoneroWallet) -> None: - accounts_before = wallet.get_accounts() - created_account = wallet.create_account() + accounts_before: list[MoneroAccount] = wallet.get_accounts() + created_account: MoneroAccount = wallet.create_account() WalletUtils.test_account(created_account, TestUtils.NETWORK_TYPE) assert len(accounts_before) == len(wallet.get_accounts()) - 1 @@ -1326,9 +1330,9 @@ def test_create_account_without_label(self, wallet: MoneroWallet) -> None: @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_create_account_with_label(self, wallet: MoneroWallet) -> None: # create account with label - accounts_before = wallet.get_accounts() - label = StringUtils.get_random_string() - created_account = wallet.create_account(label) + accounts_before: list[MoneroAccount] = wallet.get_accounts() + label: str = StringUtils.get_random_string() + created_account: MoneroAccount = wallet.create_account(label) WalletUtils.test_account(created_account, TestUtils.NETWORK_TYPE) assert created_account.index is not None assert len(accounts_before) == len(wallet.get_accounts()) - 1 @@ -1357,18 +1361,18 @@ def test_set_account_label(self, wallet: MoneroWallet) -> None: wallet.create_account() # set account label - label = StringUtils.get_random_string() + label: str = StringUtils.get_random_string() wallet.set_account_label(1, label) assert label == wallet.get_subaddress(1, 0).label # Can get subaddresses at a aspecified account index @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_subaddresses(self, wallet: MoneroWallet) -> None: - accounts = wallet.get_accounts() + accounts: list[MoneroAccount] = wallet.get_accounts() assert len(accounts) > 0 for account in accounts: assert account.index is not None - subaddresses = wallet.get_subaddresses(account.index) + subaddresses: list[MoneroSubaddress] = wallet.get_subaddresses(account.index) assert len(subaddresses) > 0 for subaddress in subaddresses: WalletUtils.test_subaddress(subaddress) @@ -1377,13 +1381,13 @@ def test_get_subaddresses(self, wallet: MoneroWallet) -> None: # Can get subaddresses at a specified account index and subaddress indices @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_subaddresses_by_indices(self, wallet: MoneroWallet) -> None: - accounts = wallet.get_accounts() + accounts: list[MoneroAccount] = wallet.get_accounts() assert len(accounts) > 0 for account in accounts: # get subaddresses assert account.index is not None - subaddresses = wallet.get_subaddresses(account.index) + subaddresses: list[MoneroSubaddress] = wallet.get_subaddresses(account.index) assert len(subaddresses) > 0 # remove a subaddress for query if possible @@ -1398,7 +1402,7 @@ def test_get_subaddresses_by_indices(self, wallet: MoneroWallet) -> None: assert len(subaddress_indices) > 0 # fetch subaddresses by indices - fetched_subaddresses = wallet.get_subaddresses(account.index, subaddress_indices) + fetched_subaddresses: list[MoneroSubaddress] = wallet.get_subaddresses(account.index, subaddress_indices) # original subaddresses (minus one removed if applicable) is equal to fetched subaddresses AssertUtils.assert_list_equals(subaddresses, fetched_subaddresses) @@ -1406,11 +1410,11 @@ def test_get_subaddresses_by_indices(self, wallet: MoneroWallet) -> None: # Can get subaddress at a specified account index and subaddress index @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_subaddress_by_index(self, wallet: MoneroWallet) -> None: - accounts = wallet.get_accounts() + accounts: list[MoneroAccount] = wallet.get_accounts() assert len(accounts) > 0 for account in accounts: assert account.index is not None - subaddresses = wallet.get_subaddresses(account.index) + subaddresses: list[MoneroSubaddress] = wallet.get_subaddresses(account.index) assert len(subaddresses) > 0 for subaddress in subaddresses: assert subaddress.index is not None @@ -1425,26 +1429,26 @@ def test_get_subaddress_by_index(self, wallet: MoneroWallet) -> None: @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_create_subaddress(self, wallet: MoneroWallet) -> None: # create subaddresses across accounts - accounts = wallet.get_accounts() + accounts: list[MoneroAccount] = wallet.get_accounts() if len(accounts) < 2: wallet.create_account() accounts = wallet.get_accounts() assert len(accounts) > 1 - account_idx = 0 + account_idx: int = 0 while account_idx < 2: # create subaddress with no label - subaddresses = wallet.get_subaddresses(account_idx) - subaddress = wallet.create_subaddress(account_idx) + subaddresses: list[MoneroSubaddress] = wallet.get_subaddresses(account_idx) + subaddress: MoneroSubaddress = wallet.create_subaddress(account_idx) assert subaddress.label is None WalletUtils.test_subaddress(subaddress) - subaddresses_new = wallet.get_subaddresses(account_idx) + subaddresses_new: list[MoneroSubaddress] = wallet.get_subaddresses(account_idx) assert len(subaddresses_new) - 1 == len(subaddresses) AssertUtils.assert_equals(subaddress, subaddresses_new[len(subaddresses_new) - 1]) # create subaddress with label subaddresses = wallet.get_subaddresses(account_idx) - uuid = StringUtils.get_random_string() + uuid: str = StringUtils.get_random_string() subaddress = wallet.create_subaddress(account_idx, uuid) assert (uuid == subaddress.label) WalletUtils.test_subaddress(subaddress) @@ -1462,9 +1466,9 @@ def test_set_subaddress_label(self, wallet: MoneroWallet) -> None: wallet.create_subaddress(0) # set subaddress labels - subaddress_idx = 0 + subaddress_idx: int = 0 while subaddress_idx < len(wallet.get_subaddresses(0)): - label = StringUtils.get_random_string() + label: str = StringUtils.get_random_string() wallet.set_subaddress_label(0, subaddress_idx, label) assert label == wallet.get_subaddress(0, subaddress_idx).label subaddress_idx += 1 @@ -1473,13 +1477,13 @@ def test_set_subaddress_label(self, wallet: MoneroWallet) -> None: @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_txs_wallet(self, wallet: MoneroWallet) -> None: #non_default_incoming: bool = False - txs = WalletTxsUtils.get_and_test_txs(wallet, None, None, True, TestUtils.REGTEST) + txs: list[MoneroTxWallet] = WalletTxsUtils.get_and_test_txs(wallet, None, None, True, TestUtils.REGTEST) assert len(txs) > 0, "Wallet has no txs to test" # TODO make consistent with test funded wallet # assert TestUtils.FIRST_RECEIVE_HEIGHT == txs[0].get_height(), "First tx's restore height must match the restore height in TestUtils" # build test context - ctx = TxContext() + ctx: TxContext = TxContext() ctx.wallet = wallet # test each transaction @@ -1488,9 +1492,9 @@ def test_get_txs_wallet(self, wallet: MoneroWallet) -> None: TxWalletUtils.test_tx_wallet(tx, ctx) # test merging equivalent txs - it = txs[i] # is the same as tx - copy1 = it.copy() - copy2 = it.copy() + it: MoneroTxWallet = txs[i] # is the same as tx + copy1: MoneroTxWallet = it.copy() + copy2: MoneroTxWallet = it.copy() if copy1.is_confirmed: assert it.block is not None @@ -1514,9 +1518,9 @@ def test_get_txs_wallet(self, wallet: MoneroWallet) -> None: # ensure unique block reference per height if it.is_confirmed: - h = it.get_height() + h: int | None = it.get_height() assert h is not None - block = block_per_height.get(h) + block: MoneroBlock | None = block_per_height.get(h) if block is None: assert it.block is not None block_per_height[i] = it.block @@ -1535,8 +1539,8 @@ def test_get_txs_by_hash(self, wallet: MoneroWallet) -> None: max_num_txs: int = 10 # fetch all txs for testing - txs = wallet.get_txs() - num_txs = len(txs) + txs: list[MoneroTxWallet] = wallet.get_txs() + num_txs: int = len(txs) assert num_txs > 1, f"Test requires at least 2 txs to fetch by hash, got {num_txs}" # randomly pick a few for fetching by hash @@ -1544,20 +1548,20 @@ def test_get_txs_by_hash(self, wallet: MoneroWallet) -> None: txs = txs[0:min(max_num_txs, num_txs)] # test fetching by hash - tx_hash = txs[0].hash + tx_hash: str | None = txs[0].hash assert tx_hash is not None - fetched_tx = wallet.get_tx(tx_hash) + fetched_tx: MoneroTxWallet | None = wallet.get_tx(tx_hash) assert fetched_tx is not None assert tx_hash == fetched_tx.hash TxWalletUtils.test_tx_wallet(fetched_tx) # test fetching by hashes - tx_id1 = txs[0].hash - tx_id2 = txs[1].hash + tx_id1: str | None = txs[0].hash + tx_id2: str | None = txs[1].hash assert tx_id1 is not None assert tx_id2 is not None - fetched_txs = wallet.get_txs([tx_id1, tx_id2]) - num_fetched_txs = len(fetched_txs) + fetched_txs: list[MoneroTxWallet] = wallet.get_txs([tx_id1, tx_id2]) + num_fetched_txs: int = len(fetched_txs) assert num_fetched_txs == 2, f"Expected 2 txs, got {num_fetched_txs}" # test fetching by hashes as collection @@ -1587,7 +1591,7 @@ def test_get_txs_by_hash(self, wallet: MoneroWallet) -> None: @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_txs_with_query(self, wallet: MoneroWallet) -> None: # get random transactions for testing - random_txs = WalletTxsUtils.get_random_transactions(wallet, None, 3, 5) + random_txs: list[MoneroTxWallet] = WalletTxsUtils.get_random_transactions(wallet, None, 3, 5) for random_tx in random_txs: TxWalletUtils.test_tx_wallet(random_tx, None) @@ -1596,12 +1600,12 @@ def test_get_txs_with_query(self, wallet: MoneroWallet) -> None: for random_tx in random_txs: assert random_tx.hash is not None tx_hashes.append(random_tx.hash) - query = MoneroTxQuery() + query: MoneroTxQuery = MoneroTxQuery() query.hash = random_tx.hash - txs = WalletTxsUtils.get_and_test_txs(wallet, query, None, True, TestUtils.REGTEST) + txs: list[MoneroTxWallet] = WalletTxsUtils.get_and_test_txs(wallet, query, None, True, TestUtils.REGTEST) assert len(txs) == 1 # txs change with chain so check mergeability - merged = txs[0] + merged: MoneroTxWallet = txs[0] merged.merge(random_tx.copy()) TxWalletUtils.test_tx_wallet(merged) @@ -1675,7 +1679,7 @@ def test_get_txs_with_query(self, wallet: MoneroWallet) -> None: # get txs with manually built query that are confirmed and have an outgoing transfer from account 0 ctx = TxContext() ctx.has_outgoing_transfer = True - tx_query = MoneroTxQuery() + tx_query: MoneroTxQuery = MoneroTxQuery() tx_query.is_confirmed = True tx_query.transfer_query = MoneroTransferQuery() tx_query.transfer_query.account_index = 0 @@ -1931,24 +1935,24 @@ def test_validate_inputs_get_txs(self, wallet: MoneroWallet) -> None: random_txs: list[MoneroTxWallet] = WalletTxsUtils.get_random_transactions(wallet, None, 3, 5) # valid, invalid, and unknown tx hashes for tests - tx_hash = random_txs[0].hash - invalid_hash = "invalid_id" - unknown_hash1 = "6c4982f2499ece80e10b627083c4f9b992a00155e98bcba72a9588ccb91d0a61" - unknown_hash2 = "ff397104dd875882f5e7c66e4f852ee134f8cf45e21f0c40777c9188bc92e943" + tx_hash: str | None = random_txs[0].hash + invalid_hash: str = "invalid_id" + unknown_hash1: str = "6c4982f2499ece80e10b627083c4f9b992a00155e98bcba72a9588ccb91d0a61" + unknown_hash2: str = "ff397104dd875882f5e7c66e4f852ee134f8cf45e21f0c40777c9188bc92e943" assert tx_hash is not None and len(tx_hash) > 0 # fetch unknown tx hash - fetched_tx = wallet.get_tx(unknown_hash1) + fetched_tx: MoneroTxWallet | None = wallet.get_tx(unknown_hash1) assert fetched_tx is None # fetch unknown tx hash using query tx_query: MoneroTxQuery = MoneroTxQuery() tx_query.hash = unknown_hash1 - fetched_txs = wallet.get_txs(tx_query) + fetched_txs: list[MoneroTxWallet] = wallet.get_txs(tx_query) assert len(fetched_txs) == 0 # fetch unknwon tx hash in list - txs = wallet.get_txs([tx_hash, unknown_hash1]) + txs: list[MoneroTxWallet] = wallet.get_txs([tx_hash, unknown_hash1]) assert len(txs) == 1 assert txs[0].hash == tx_hash @@ -1990,19 +1994,19 @@ def test_get_transfers(self, wallet: MoneroWallet) -> None: # get transfers by account index non_default_incoming: bool = False for account in wallet.get_accounts(True): - transfer_query = MoneroTransferQuery() + transfer_query: MoneroTransferQuery = MoneroTransferQuery() transfer_query.account_index = account.index - account_transfers = WalletTransfersUtils.get_and_test_transfers(wallet, transfer_query, None, None) + account_transfers: list[MoneroTransfer] = WalletTransfersUtils.get_and_test_transfers(wallet, transfer_query, None, None) for transfer in account_transfers: assert transfer.account_index == account.index # get transfers by subaddress index subaddress_transfers: list[MoneroTransfer] = [] for subaddress in account.subaddresses: - subaddress_query = MoneroTransferQuery() + subaddress_query: MoneroTransferQuery = MoneroTransferQuery() subaddress_query.account_index = subaddress.account_index subaddress_query.subaddress_index = subaddress.index - transfers = WalletTransfersUtils.get_and_test_transfers(wallet, subaddress_query, None, None) + transfers: list[MoneroTransfer] = WalletTransfersUtils.get_and_test_transfers(wallet, subaddress_query, None, None) for transfer in transfers: # test account and subaddress indices @@ -2067,7 +2071,7 @@ def test_get_transfers(self, wallet: MoneroWallet) -> None: else: assert isinstance(transfer, MoneroOutgoingTransfer) intersections: set[int] = set(subaddress_indices) - overlap = intersections.intersection(transfer.subaddress_indices) + overlap: set[int] = intersections.intersection(transfer.subaddress_indices) assert overlap is not None and len(overlap) > 0, "Subaddresses must overlap" # ensure transfer found with non-zero account and subaddress indices @@ -2212,9 +2216,9 @@ def test_validate_inputs_get_transfers(self, wallet: MoneroWallet) -> None: assert len(transfers) == 0 # test invalid hash in list - random_txs = WalletTxsUtils.get_random_transactions(wallet, None, 3, 5) + random_txs: list[MoneroTxWallet] = WalletTxsUtils.get_random_transactions(wallet, None, 3, 5) transfer_query.tx_query = MoneroTxQuery() - random_hash = random_txs[0].hash + random_hash: str | None = random_txs[0].hash assert random_hash is not None transfer_query.tx_query.hashes.append(random_hash) transfer_query.tx_query.hashes.append("invalid_id") @@ -2264,7 +2268,7 @@ def test_get_outputs(self, wallet: MoneroWallet) -> None: # get outputs by account index output_query: MoneroOutputQuery = MoneroOutputQuery() output_query.account_index = account.index - account_outputs = OutputUtils.get_and_test_outputs(wallet, output_query, is_used) + account_outputs: list[MoneroOutputWallet] = OutputUtils.get_and_test_outputs(wallet, output_query, is_used) for ouput in account_outputs: assert ouput.account_index == account.index @@ -2274,7 +2278,7 @@ def test_get_outputs(self, wallet: MoneroWallet) -> None: subaddr_query: MoneroOutputQuery = MoneroOutputQuery() subaddr_query.account_index = account.index subaddr_query.subaddress_index = subaddress.index - outputs = OutputUtils.get_and_test_outputs(wallet, subaddr_query, subaddress.is_used) + outputs: list[MoneroOutputWallet] = OutputUtils.get_and_test_outputs(wallet, subaddr_query, subaddress.is_used) for output in outputs: assert subaddress.account_index == output.account_index assert subaddress.index == output.subaddress_index @@ -2440,7 +2444,7 @@ def test_validate_inputs_get_outputs(self, wallet: MoneroWallet) -> None: output_query = MoneroOutputQuery() output_query.set_tx_query(MoneroTxQuery(), False) assert output_query.tx_query is not None - random_hash = random_txs[0].hash + random_hash: str | None = random_txs[0].hash assert random_hash is not None and len(random_hash) > 0 output_query.tx_query.hashes = [random_hash, "invalid_id"] @@ -2464,7 +2468,7 @@ def test_export_outputs(self, wallet: MoneroWallet) -> None: # wallet exports outputs since last export by default outputs_hex = wallet.export_outputs() - outputs_hex_all = wallet.export_outputs(True) + outputs_hex_all: str = wallet.export_outputs(True) assert len(outputs_hex_all) > len(outputs_hex) # Can import outputs in hex format @@ -2482,10 +2486,10 @@ def test_import_outputs(self, wallet: MoneroWallet) -> None: @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_accounting(self, wallet: MoneroWallet) -> None: # pre-fetch wallet balances, accounts, subaddresses and txs - wallet_balance = wallet.get_balance() - wallet_unlocked_balance = wallet.get_unlocked_balance() + wallet_balance: int = wallet.get_balance() + wallet_unlocked_balance: int = wallet.get_unlocked_balance() # includes subaddresses - accounts = wallet.get_accounts(True) + accounts: list[MoneroAccount] = wallet.get_accounts(True) # test wallet balance GenUtils.test_unsigned_big_integer(wallet_balance) @@ -2510,7 +2514,7 @@ def test_accounting(self, wallet: MoneroWallet) -> None: # balance may not equal sum of unspent outputs if unconfirmed txs # TODO monero-wallet-rpc: reason not to return unspent outputs on unconfirmed txs? then this isn't necessary - txs = wallet.get_txs() + txs: list[MoneroTxWallet] = wallet.get_txs() has_unconfirmed_tx: bool = False for tx in txs: if tx.in_tx_pool: @@ -2540,7 +2544,7 @@ def test_accounting(self, wallet: MoneroWallet) -> None: output_query = MoneroOutputQuery() output_query.account_index = account.index output_query.is_spent = False - account_outputs = wallet.get_outputs(output_query) + account_outputs: list[MoneroOutputWallet] = wallet.get_outputs(output_query) for output in account_outputs: assert output.amount is not None account_sum += output.amount @@ -2556,7 +2560,7 @@ def test_accounting(self, wallet: MoneroWallet) -> None: output_query.account_index = account.index output_query.subaddress_index = subaddress.index output_query.is_spent = False - subaddress_outputs = wallet.get_outputs(output_query) + subaddress_outputs: list[MoneroOutputWallet] = wallet.get_outputs(output_query) for output in subaddress_outputs: assert output.amount is not None subaddress_sum += output.amount @@ -2941,10 +2945,10 @@ def test_get_reserve_proof_account(self, wallet: MoneroWallet) -> None: # Can get and set a transaction note @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_set_tx_note(self, wallet: MoneroWallet) -> None: - txs = WalletTxsUtils.get_random_transactions(wallet, None, 1, 5) + txs: list[MoneroTxWallet] = WalletTxsUtils.get_random_transactions(wallet, None, 1, 5) # set notes - uuid = StringUtils.get_random_string() + uuid: str = StringUtils.get_random_string() for i, tx in enumerate(txs): tx_hash: str | None = tx.hash @@ -3002,15 +3006,15 @@ def test_export_key_images(self, wallet: MoneroWallet) -> None: @pytest.mark.xfail(raises=Exception, reason="TODO these are already known to the wallet, so no new key images will be imported") def test_get_new_key_images_from_last_import(self, wallet: MoneroWallet) -> None: # get outputs hex - outputs_hex = wallet.export_outputs() + outputs_hex: str = wallet.export_outputs() # import outputs hex if outputs_hex != "": - num_imported = wallet.import_outputs(outputs_hex) + num_imported: int = wallet.import_outputs(outputs_hex) assert num_imported >= 0 # get and test new key images from last import - images = wallet.get_new_key_images_from_last_import() + images: list[MoneroKeyImage] = wallet.get_new_key_images_from_last_import() if len(images) == 0: # TODO: these are already known to the wallet, so no new key images will be imported raise Exception("No new key images in last import") @@ -3024,17 +3028,17 @@ def test_get_new_key_images_from_last_import(self, wallet: MoneroWallet) -> None def test_import_key_images(self, wallet: MoneroWallet) -> None: export_result: MoneroKeyImageExportResult = wallet.export_key_images() assert len(export_result.key_images) > 0, "Wallet does not have any key images run send tests" - result = wallet.import_key_images(export_result.key_images) + result: MoneroKeyImageImportResult = wallet.import_key_images(export_result.key_images) assert result.height is not None and result.height > 0 # determine if non-zero spent and unspent amounts are expected - query = MoneroTxQuery() + query: MoneroTxQuery = MoneroTxQuery() query.is_outgoing = True query.is_confirmed = True - txs = wallet.get_txs(query) - balance = wallet.get_balance() - has_spent = len(txs) > 0 - has_unspent = balance > 0 + txs: list[MoneroTxWallet] = wallet.get_txs(query) + balance: int = wallet.get_balance() + has_spent: bool = len(txs) > 0 + has_unspent: bool = balance > 0 # test amounts GenUtils.test_unsigned_big_integer(result.spent_amount, has_spent) @@ -3063,7 +3067,7 @@ def test_view_only_and_offline_wallets(self, wallet: MoneroWallet) -> None: # test tx signing with wallets try: - tester = ViewOnlyAndOfflineWalletTester(wallet, view_only_wallet, offline_wallet) + tester: ViewOnlyAndOfflineWalletTester = ViewOnlyAndOfflineWalletTester(wallet, view_only_wallet, offline_wallet) tester.test() finally: self._close_wallet(view_only_wallet) @@ -3089,12 +3093,12 @@ def test_sign_and_verify_messages(self, wallet: MoneroWallet) -> None: for subaddress in subaddresses: assert subaddress.account_index is not None assert subaddress.index is not None - account_idx = subaddress.account_index - idx = subaddress.index + account_idx: int = subaddress.account_index + idx: int = subaddress.index # sign and verify message with spend key signature: str = wallet.sign_message(msg, MoneroMessageSignatureType.SIGN_WITH_SPEND_KEY, account_idx, idx) - result = wallet.verify_message(msg, wallet.get_address(account_idx, idx), signature) + result: MoneroMessageSignatureResult = wallet.verify_message(msg, wallet.get_address(account_idx, idx), signature) WalletUtils.test_message_signature_result(result, True) assert result.signature_type == MoneroMessageSignatureType.SIGN_WITH_SPEND_KEY @@ -3236,17 +3240,17 @@ def test_set_attributes(self, wallet: MoneroWallet) -> None: @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_payment_uri(self, wallet: MoneroWallet) -> None: # test with address and amount - config1 = MoneroTxConfig() - dest = MoneroDestination() + config1: MoneroTxConfig = MoneroTxConfig() + dest: MoneroDestination = MoneroDestination() dest.address = wallet.get_address(0, 0) dest.amount = 0 config1.destinations.append(dest) - uri = wallet.get_payment_uri(config1) - config2 = wallet.parse_payment_uri(uri) + uri: str = wallet.get_payment_uri(config1) + config2: MoneroTxConfig = wallet.parse_payment_uri(uri) AssertUtils.assert_equals(config1, config2) # test with subaddress and all fields - subaddress = wallet.get_subaddress(0, 1) + subaddress: MoneroSubaddress = wallet.get_subaddress(0, 1) assert subaddress.address is not None config1.destinations[0].address = subaddress.address config1.destinations[0].amount = 425000000000 @@ -3257,7 +3261,7 @@ def test_get_payment_uri(self, wallet: MoneroWallet) -> None: AssertUtils.assert_equals(config1, config2) # test with undefined address - address = config1.destinations[0].address + address: str | None = config1.destinations[0].address config1.destinations[0].address = None try: wallet.get_payment_uri(config1) @@ -3278,7 +3282,7 @@ def test_get_payment_uri(self, wallet: MoneroWallet) -> None: # Can start and stop mining @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_mining(self, daemon: MoneroDaemonRpc, wallet: MoneroWallet) -> None: - status = daemon.get_mining_status() + status: MoneroMiningStatus = daemon.get_mining_status() if status.is_active: wallet.stop_mining() wallet.start_mining(1, False, True) @@ -3288,9 +3292,9 @@ def test_mining(self, daemon: MoneroDaemonRpc, wallet: MoneroWallet) -> None: @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_change_password(self) -> None: # create random wallet - config = MoneroWalletConfig() + config: MoneroWalletConfig = MoneroWalletConfig() config.password = TestUtils.WALLET_PASSWORD - wallet = self._create_wallet(config) + wallet: MoneroWallet = self._create_wallet(config) path: str = wallet.get_path() # change password @@ -3343,9 +3347,9 @@ def test_change_password(self) -> None: def test_save_and_close(self) -> None: # create random wallet password: str = "" - config = MoneroWalletConfig() + config: MoneroWalletConfig = MoneroWalletConfig() config.password = password - wallet = self._create_wallet(config) + wallet: MoneroWallet = self._create_wallet(config) path: str = wallet.get_path() # set an attribute @@ -3539,7 +3543,7 @@ def test_input_key_images(self, wallet: MoneroWallet) -> None: # test unrelayed sweep output tx_config = MoneroTxConfig() tx_config.address = wallet.get_primary_address() - output_key_image = outputs[0].key_image + output_key_image: MoneroKeyImage | None = outputs[0].key_image assert output_key_image is not None tx_config.key_image = output_key_image.hex spend_tx = wallet.sweep_output(tx_config) @@ -3738,7 +3742,7 @@ def test_stop_listening(self) -> None: # Can be created and receive funds # TODO this test is flaky on monero-wallet-rpc because of mining speed @pytest.mark.skipif(TestUtils.TEST_NOTIFICATIONS is False, reason="TEST_NOTIFICATIONS disabled") - @pytest.mark.flaky(reruns=3, reruns_delay=5) + @pytest.mark.flaky(reruns=5, reruns_delay=10, only_rerun=[]) def test_create_and_receive(self, daemon: MoneroDaemonRpc, wallet: MoneroWallet) -> None: # create random wallet receiver: MoneroWallet = self._create_wallet(MoneroWalletConfig()) @@ -3938,7 +3942,7 @@ def test_sweep_accounts(self, wallet: MoneroWallet) -> None: # determine if account was swept swept: bool = False for j in range(NUM_ACCOUNTS_TO_SWEEP): - account_unlocked = accounts_unlocked[j] + account_unlocked: MoneroAccount = accounts_unlocked[j] if account_unlocked.index == account_before.index: swept = True break diff --git a/tests/test_monero_wallet_full.py b/tests/test_monero_wallet_full.py index 46b6a60..813de85 100644 --- a/tests/test_monero_wallet_full.py +++ b/tests/test_monero_wallet_full.py @@ -67,7 +67,7 @@ def _create_wallet(self, config: Optional[MoneroWalletConfig], start_syncing: bo config.regtest = config.network_type == MoneroNetworkType.MAINNET and Utils.REGTEST # create wallet - wallet = MoneroWalletFull.create_wallet(config) + wallet: MoneroWalletFull = MoneroWalletFull.create_wallet(config) if not random: assert config.restore_height == wallet.get_restore_height() if start_syncing is not False and wallet.is_connected_to_daemon(): @@ -90,7 +90,7 @@ def _open_wallet(self, config: Optional[MoneroWalletConfig], start_syncing: bool assert config.network_type is not None assert config.path is not None - wallet = MoneroWalletFull.open_wallet(config.path, config.password, config.network_type) + wallet: MoneroWalletFull = MoneroWalletFull.open_wallet(config.path, config.password, config.network_type) wallet.set_daemon_connection(config.server) if start_syncing is not False and wallet.is_connected_to_daemon(): wallet.start_syncing(Utils.SYNC_PERIOD_IN_MS) @@ -675,7 +675,7 @@ def test_get_height_by_date_regtest(self, wallet: MoneroWallet) -> None: @pytest.mark.unit @pytest.mark.xfail(reason="import_key_images() dereferences m_hex unconditionally (boost::optional UB when unset)", strict=True) def test_import_key_images_hex_not_defined(self) -> None: - script = ( + script: str = ( "import monero, sys, tempfile, os\n" "d = tempfile.mkdtemp()\n" "cfg = monero.MoneroWalletConfig()\n" @@ -690,7 +690,7 @@ def test_import_key_images_hex_not_defined(self) -> None: "except RuntimeError as e:\n" " sys.exit(0 if str(e) == 'key image hex is not defined' else f'wrong message: {e}')\n" ) - result = subprocess.run([sys.executable, "-c", script], capture_output=True, text=True, timeout=30) + result: subprocess.CompletedProcess[str] = subprocess.run([sys.executable, "-c", script], capture_output=True, text=True, timeout=30) logger.debug(f"subprocess exit code: {result.returncode}, stderr: {result.stderr.strip()}") assert result.returncode == 0, ( f"import_key_images() did not cleanly raise 'key image hex is not defined' " @@ -700,7 +700,7 @@ def test_import_key_images_hex_not_defined(self) -> None: @pytest.mark.unit @pytest.mark.xfail(reason="import_key_images() dereferences m_signature unconditionally (boost::optional UB when unset)", strict=True) def test_import_key_images_signature_not_defined(self) -> None: - script = ( + script: str = ( "import monero, sys, tempfile, os\n" "d = tempfile.mkdtemp()\n" "cfg = monero.MoneroWalletConfig()\n" @@ -716,7 +716,7 @@ def test_import_key_images_signature_not_defined(self) -> None: "except RuntimeError as e:\n" " sys.exit(0 if str(e) == 'key image signature is not defined' else f'wrong message: {e}')\n" ) - result = subprocess.run([sys.executable, "-c", script], capture_output=True, text=True, timeout=30) + result: subprocess.CompletedProcess[str] = subprocess.run([sys.executable, "-c", script], capture_output=True, text=True, timeout=30) logger.debug(f"subprocess exit code: {result.returncode}, stderr: {result.stderr.strip()}") assert result.returncode == 0, ( f"import_key_images() did not cleanly raise 'key image signature is not defined' " diff --git a/tests/test_monero_wallet_interface.py b/tests/test_monero_wallet_interface.py index 6d2dc0c..92eb5fb 100644 --- a/tests/test_monero_wallet_interface.py +++ b/tests/test_monero_wallet_interface.py @@ -5,7 +5,7 @@ MoneroWallet, MoneroRpcConnection, MoneroWalletListener, MoneroTransferQuery, MoneroOutputQuery, MoneroTxConfig, MoneroTxSet, MoneroMessageSignatureType, - MoneroTxWallet + MoneroTxWallet, MoneroMessageSignatureResult ) from utils import WalletUtils, StringUtils, BaseTestClass @@ -490,7 +490,7 @@ def test_sign_message(self, wallet: MoneroWallet) -> None: wallet.sign_message("", MoneroMessageSignatureType.SIGN_WITH_VIEW_KEY) def test_verify_message(self, wallet: MoneroWallet) -> None: - result = wallet.verify_message("", "", "") + result: MoneroMessageSignatureResult = wallet.verify_message("", "", "") WalletUtils.test_message_signature_result(result, False) @pytest.mark.not_supported diff --git a/tests/test_monero_wallet_keys.py b/tests/test_monero_wallet_keys.py index 43d5e48..f5f7bce 100644 --- a/tests/test_monero_wallet_keys.py +++ b/tests/test_monero_wallet_keys.py @@ -76,7 +76,7 @@ def _create_wallet(self, config: Optional[MoneroWalletConfig]) -> MoneroWalletKe # create wallet if random: - wallet = MoneroWalletKeys.create_wallet_random(config) + wallet: MoneroWalletKeys = MoneroWalletKeys.create_wallet_random(config) elif config.seed is not None and config.seed != "": wallet = MoneroWalletKeys.create_wallet_from_seed(config) elif config.primary_address is not None and config.private_view_key is not None: @@ -585,8 +585,8 @@ def test_create_wallet_random(self) -> None: """ Can create a random wallet. """ - config = MoneroWalletConfig() - wallet = self._create_wallet(config) + config: MoneroWalletConfig = MoneroWalletConfig() + wallet: MoneroWalletKeys = self._create_wallet(config) # validate wallet MoneroUtils.validate_address(wallet.get_primary_address(), Utils.NETWORK_TYPE) @@ -610,12 +610,12 @@ def test_create_wallet_random(self) -> None: @override def test_create_wallet_from_seed(self, wallet: MoneroWallet, test_config: BaseTestMoneroWallet.Config) -> None: # save for comparison - primary_address = wallet.get_primary_address() - private_view_key = wallet.get_private_view_key() - private_spend_key = wallet.get_private_spend_key() + primary_address: str = wallet.get_primary_address() + private_view_key: str = wallet.get_private_view_key() + private_spend_key: str = wallet.get_private_spend_key() # recreate test wallet from seed - config = MoneroWalletConfig() + config: MoneroWalletConfig = MoneroWalletConfig() config.seed = Utils.SEED w: MoneroWallet = self._create_wallet(config) @@ -638,7 +638,7 @@ def test_create_wallet_from_seed(self, wallet: MoneroWallet, test_config: BaseTe @override def test_create_wallet_from_seed_with_offset(self) -> None: # create test wallet with offset - config = MoneroWalletConfig() + config: MoneroWalletConfig = MoneroWalletConfig() config.seed = Utils.SEED config.seed_offset = "my secret offset!" wallet: MoneroWallet = self._create_wallet(config) @@ -654,12 +654,12 @@ def test_create_wallet_from_seed_with_offset(self) -> None: @override def test_create_wallet_from_keys(self, daemon: MoneroDaemonRpc, wallet: MoneroWallet) -> None: # save for comparison - primary_address = wallet.get_primary_address() - private_view_key = wallet.get_private_view_key() - private_spend_key = wallet.get_private_spend_key() + primary_address: str = wallet.get_primary_address() + private_view_key: str = wallet.get_private_view_key() + private_spend_key: str = wallet.get_private_spend_key() # recreate test wallet from keys - config = MoneroWalletConfig() + config: MoneroWalletConfig = MoneroWalletConfig() config.primary_address = primary_address config.private_view_key = private_view_key config.private_spend_key = private_spend_key @@ -697,7 +697,7 @@ def test_create_wallet_from_keys_no_keys(self) -> None: """ create_wallet_from_keys() must require at least one of the private spend/view keys. """ - config = MoneroWalletConfig() + config: MoneroWalletConfig = MoneroWalletConfig() config.network_type = Utils.NETWORK_TYPE MoneroWalletKeys.create_wallet_from_keys(config) @@ -707,7 +707,7 @@ def test_create_wallet_from_keys_invalid_spend_key(self) -> None: """ create_wallet_from_keys() must fail to parse a malformed private spend key. """ - config = MoneroWalletConfig() + config: MoneroWalletConfig = MoneroWalletConfig() config.network_type = Utils.NETWORK_TYPE config.private_spend_key = "not-a-valid-hex-secret-key" MoneroWalletKeys.create_wallet_from_keys(config) @@ -718,7 +718,7 @@ def test_create_wallet_from_keys_invalid_view_key(self) -> None: """ create_wallet_from_keys() must fail to parse a malformed private view key. """ - config = MoneroWalletConfig() + config: MoneroWalletConfig = MoneroWalletConfig() config.network_type = Utils.NETWORK_TYPE config.primary_address = Utils.ADDRESS config.private_view_key = "not-a-valid-hex-secret-key" @@ -734,7 +734,7 @@ def test_create_wallet_from_keys_view_key_without_address(self) -> None: Non-deterministic in-process (observed locally as RuntimeError with varying messages 'std::bad_alloc' or 'failed to parse address'). """ - script = ( + script: str = ( "import monero, sys\n" "config = monero.MoneroWalletConfig()\n" f"config.network_type = monero.MoneroNetworkType.{Utils.NETWORK_TYPE.name}\n" @@ -745,7 +745,7 @@ def test_create_wallet_from_keys_view_key_without_address(self) -> None: "except RuntimeError as e:\n" " sys.exit(0 if str(e) == 'must provide address if providing private view key' else f'wrong message: {e}')\n" ) - result = subprocess.run([sys.executable, "-c", script], capture_output=True, text=True, timeout=30) + result: subprocess.CompletedProcess[str] = subprocess.run([sys.executable, "-c", script], capture_output=True, text=True, timeout=30) logger.debug(f"subprocess exit code: {result.returncode}, stderr: {result.stderr.strip()}") assert result.returncode == 0, ( f"create_wallet_from_keys() did not cleanly raise 'must provide address if providing " @@ -756,7 +756,7 @@ def test_create_wallet_from_keys_view_key_without_address(self) -> None: @override def test_get_subaddress_address(self, wallet: MoneroWallet) -> None: assert wallet.get_primary_address() == (wallet.get_address(0, 0)) - accounts = self._get_test_accounts(wallet, True) + accounts: list[MoneroAccount] = self._get_test_accounts(wallet, True) for account in accounts: assert account is not None @@ -772,24 +772,24 @@ def test_get_subaddress_address(self, wallet: MoneroWallet) -> None: @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") @override def test_get_subaddress_address_out_of_range(self, wallet: MoneroWallet) -> None: - accounts = self._get_test_accounts(wallet, True) - account_idx = len(accounts) - 1 - subaddress_idx = len(accounts[account_idx].subaddresses) - address = wallet.get_address(account_idx, subaddress_idx) + accounts: list[MoneroAccount] = self._get_test_accounts(wallet, True) + account_idx: int = len(accounts) - 1 + subaddress_idx: int = len(accounts[account_idx].subaddresses) + address: str = wallet.get_address(account_idx, subaddress_idx) assert address is not None assert len(address) > 0 @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") @override def test_get_account(self, wallet: MoneroWallet) -> None: - accounts = self._get_test_accounts(wallet) + accounts: list[MoneroAccount] = self._get_test_accounts(wallet) assert len(accounts) > 0 for account in accounts: WalletUtils.test_account(account, Utils.NETWORK_TYPE, False) # test without subaddresses assert account.index is not None - retrieved = wallet.get_account(account.index) + retrieved: MoneroAccount = wallet.get_account(account.index) assert len(retrieved.subaddresses) == 0 # test with subaddresses @@ -798,7 +798,7 @@ def test_get_account(self, wallet: MoneroWallet) -> None: @override def test_get_accounts_without_subaddresses(self, wallet: MoneroWallet) -> None: - accounts = self._get_test_accounts(wallet) + accounts: list[MoneroAccount] = self._get_test_accounts(wallet) assert len(accounts) > 0 for account in accounts: WalletUtils.test_account(account, Utils.NETWORK_TYPE, False) @@ -806,7 +806,7 @@ def test_get_accounts_without_subaddresses(self, wallet: MoneroWallet) -> None: @override def test_get_accounts_with_subaddresses(self, wallet: MoneroWallet) -> None: - accounts = self._get_test_accounts(wallet, True) + accounts: list[MoneroAccount] = self._get_test_accounts(wallet, True) assert len(accounts) > 0 for account in accounts: WalletUtils.test_account(account, Utils.NETWORK_TYPE, False) @@ -815,11 +815,11 @@ def test_get_accounts_with_subaddresses(self, wallet: MoneroWallet) -> None: @override def test_get_subaddresses(self, wallet: MoneroWallet) -> None: wallet = wallet - accounts = self._get_test_accounts(wallet) + accounts: list[MoneroAccount] = self._get_test_accounts(wallet) assert len(accounts) > 0 for account in accounts: assert account.index is not None - subaddresses = wallet.get_subaddresses(account.index, self._subaddress_indices) + subaddresses: list[MoneroSubaddress] = wallet.get_subaddresses(account.index, self._subaddress_indices) assert len(subaddresses) > 0 for subaddress in subaddresses: WalletUtils.test_subaddress(subaddress, False) @@ -827,11 +827,11 @@ def test_get_subaddresses(self, wallet: MoneroWallet) -> None: @override def test_get_subaddress_by_index(self, wallet: MoneroWallet) -> None: - accounts = self._get_test_accounts(wallet) + accounts: list[MoneroAccount] = self._get_test_accounts(wallet) assert len(accounts) > 0 for account in accounts: assert account.index is not None - subaddresses = wallet.get_subaddresses(account.index, self._subaddress_indices) + subaddresses: list[MoneroSubaddress] = wallet.get_subaddresses(account.index, self._subaddress_indices) assert len(subaddresses) > 0 for subaddress in subaddresses: @@ -849,7 +849,7 @@ def test_get_subaddress_by_index(self, wallet: MoneroWallet) -> None: def _get_subaddress(self, wallet: MoneroWallet, account_idx: int, subaddress_idx: int) -> Optional[MoneroSubaddress]: subaddress_indices: list[int] = [subaddress_idx] - subaddresses = wallet.get_subaddresses(account_idx, subaddress_indices) + subaddresses: list[MoneroSubaddress] = wallet.get_subaddresses(account_idx, subaddress_indices) if len(subaddresses) == 0: return None @@ -857,11 +857,11 @@ def _get_subaddress(self, wallet: MoneroWallet, account_idx: int, subaddress_idx return subaddresses[0] def _get_test_accounts(self, wallet: MoneroWallet, include_subaddresses: bool = False) -> list[MoneroAccount]: - account_indices = self._account_indices - subaddress_indices = self._subaddress_indices + account_indices: list[int] = self._account_indices + subaddress_indices: list[int] = self._subaddress_indices accounts: list[MoneroAccount] = [] for account_idx in account_indices: - account = wallet.get_account(account_idx) + account: MoneroAccount = wallet.get_account(account_idx) if include_subaddresses: account.subaddresses = wallet.get_subaddresses(account_idx, subaddress_indices) diff --git a/tests/test_monero_wallet_model.py b/tests/test_monero_wallet_model.py index 2eeacac..2751477 100644 --- a/tests/test_monero_wallet_model.py +++ b/tests/test_monero_wallet_model.py @@ -28,7 +28,7 @@ class TestMoneroWalletModel(BaseTestClass): # Test output query expected behaviour def test_output_query(self) -> None: - output_query = MoneroOutputQuery() + output_query: MoneroOutputQuery = MoneroOutputQuery() tx_query: MoneroTxQuery = MoneroTxQuery() # test tx query property assign @@ -60,7 +60,7 @@ def test_output_query(self) -> None: # Test input query expected behaviour def test_input_query(self) -> None: - input_query = MoneroOutputQuery() + input_query: MoneroOutputQuery = MoneroOutputQuery() tx_query: MoneroTxQuery = MoneroTxQuery() # assign tx query to input query @@ -104,7 +104,7 @@ def test_transfer_query(self) -> None: assert tx_query.transfer_query is None - transfer_query = MoneroTransferQuery() + transfer_query: MoneroTransferQuery = MoneroTransferQuery() transfer_query.tx_query = MoneroTxQuery() # check incoming/outgoing @@ -214,7 +214,7 @@ def test_tx_config(self) -> None: #region Serialize/deserialize integrity def test_subaddress_deserialize(self) -> None: - subaddress = MoneroSubaddress() + subaddress: MoneroSubaddress = MoneroSubaddress() subaddress.account_index = 0 subaddress.index = 1 subaddress.address = TestUtils.ADDRESS @@ -227,7 +227,7 @@ def test_subaddress_deserialize(self) -> None: AssertUtils.assert_serialization_integrity(subaddress) def test_account_deserialize(self) -> None: - account = MoneroAccount() + account: MoneroAccount = MoneroAccount() account.index = 0 account.balance = 1000000 account.unlocked_balance = 900000 @@ -237,19 +237,19 @@ def test_account_deserialize(self) -> None: # reads it back (see test below) AssertUtils.assert_serialization_integrity(account) - @pytest.mark.xfail(reason="monero_account::from_property_tree() never reads back \"subaddresses\" even though to_rapidjson_val() emits it; fixed upstream in the local everoddandeven/monero-cpp checkout, pending a submodule bump", strict=True) + @pytest.mark.xfail(reason="monero_account::from_property_tree() never reads back \"subaddresses\" even though to_rapidjson_val() emits it", strict=True) def test_account_subaddresses_deserialize(self) -> None: - account = MoneroAccount() + account: MoneroAccount = MoneroAccount() account.subaddresses = [MoneroSubaddress()] - json_str = account.serialize() + json_str: str = account.serialize() logger.debug(f"Serialized account: {json_str}") assert "subaddresses" in json_str - restored = MoneroAccount.deserialize(json_str) + restored: MoneroAccount = MoneroAccount.deserialize(json_str) logger.debug(f"Deserialized account re-serialized: {restored.serialize()}") assert len(restored.subaddresses) == len(account.subaddresses) def test_transfer_query_deserialize(self) -> None: - query = MoneroTransferQuery() + query: MoneroTransferQuery = MoneroTransferQuery() query.amount = 500000 query.account_index = 0 query.incoming = True @@ -270,10 +270,10 @@ def test_transfer_query_unimplemented_fields(self, json_fragment: str) -> None: MoneroTransferQuery.deserialize(json_fragment) def test_output_wallet_deserialize(self) -> None: - output_wallet = MoneroOutputWallet() + output_wallet: MoneroOutputWallet = MoneroOutputWallet() output_wallet.amount = 1000000 output_wallet.index = 2 - key_image = MoneroKeyImage() + key_image: MoneroKeyImage = MoneroKeyImage() key_image.hex = "a" * 64 key_image.signature = "b" * 128 output_wallet.key_image = key_image @@ -284,7 +284,7 @@ def test_output_wallet_deserialize(self) -> None: AssertUtils.assert_serialization_integrity(output_wallet) def test_output_query_deserialize(self) -> None: - query = MoneroOutputQuery() + query: MoneroOutputQuery = MoneroOutputQuery() query.amount = 1000000 query.index = 2 query.account_index = 0 @@ -297,7 +297,7 @@ def test_output_query_deserialize(self) -> None: AssertUtils.assert_serialization_integrity(query) def test_tx_wallet_deserialize(self) -> None: - tx_wallet = MoneroTxWallet() + tx_wallet: MoneroTxWallet = MoneroTxWallet() tx_wallet.hash = "a" * 64 tx_wallet.is_miner_tx = False tx_wallet.fee = 7500000 @@ -333,7 +333,7 @@ def test_tx_wallet_unimplemented_fields(self, json_fragment: str) -> None: MoneroTxWallet.deserialize(json_fragment) def test_tx_query_deserialize(self) -> None: - tx_query = MoneroTxQuery() + tx_query: MoneroTxQuery = MoneroTxQuery() tx_query.hash = "a" * 64 tx_query.is_confirmed = True tx_query.hashes = ["a" * 64, "b" * 64] @@ -348,12 +348,12 @@ def test_tx_query_deserialize(self) -> None: # deserializing populates both copies at once (see test below) AssertUtils.assert_serialization_integrity(tx_query) - @pytest.mark.xfail(reason="monero_tx_query declares its own m_is_incoming/m_is_outgoing shadowing monero_tx_wallet's fields of the same name, so from_property_tree() double-populates them and a re-serialize duplicates the JSON keys", strict=True) + @pytest.mark.xfail(reason="monero_tx_query has monero_tx_wallet's fields of the same name, so from_property_tree() double-populates them", strict=True) def test_tx_query_is_incoming_deserialize_not_duplicated(self) -> None: - tx_query = MoneroTxQuery() + tx_query: MoneroTxQuery = MoneroTxQuery() tx_query.is_incoming = True tx_query.is_outgoing = False - json_str = tx_query.serialize() + json_str: str = tx_query.serialize() logger.debug(f"Serialized tx query: {json_str}") assert json_str.count("isIncoming") == 1 assert json_str.count("isOutgoing") == 1 @@ -362,9 +362,9 @@ def test_tx_query_is_incoming_deserialize_not_duplicated(self) -> None: assert restored.is_incoming == tx_query.is_incoming assert restored.is_outgoing == tx_query.is_outgoing - restored_json = restored.serialize() - incoming_count = restored_json.count("isIncoming") - outgoing_count = restored_json.count("isOutgoing") + restored_json: str = restored.serialize() + incoming_count: int = restored_json.count("isIncoming") + outgoing_count: int = restored_json.count("isOutgoing") logger.debug(f"Deserialized tx query re-serialized: {restored_json}") logger.debug(f"'isIncoming' occurs {incoming_count} time(s), 'isOutgoing' occurs {outgoing_count} time(s) (expected 1 each -- >1 means duplicated, i.e. malformed)") assert incoming_count == 1 @@ -373,15 +373,15 @@ def test_tx_query_is_incoming_deserialize_not_duplicated(self) -> None: def test_tx_query_nested_transfer_query_deserialize(self) -> None: # transfer_query is a nested sub-object that to_rapidjson_val() and # from_property_tree() both handle recursively - tx_query = MoneroTxQuery() + tx_query: MoneroTxQuery = MoneroTxQuery() tx_query.height = 3000000 - transfer_query = MoneroTransferQuery() + transfer_query: MoneroTransferQuery = MoneroTransferQuery() transfer_query.incoming = True transfer_query.amount = 500000 transfer_query.account_index = 0 tx_query.transfer_query = transfer_query - json_str = tx_query.serialize() + json_str: str = tx_query.serialize() logger.debug(f"Serialized nested tx query: {json_str}") assert "transferQuery" in json_str @@ -404,9 +404,9 @@ def test_tx_query_input_and_output_query_deserialize(self) -> None: assert tx_query.output_query.amount == 7 assert tx_query.output_query.index == 2 - @pytest.mark.xfail(reason="monero_tx_query::to_rapidjson_val() never serialized input_query/output_query even though from_property_tree() can read them back (see test above), so a plain serialize()+deserialize() round trip lost them; fixed upstream in the local everoddandeven/monero-cpp checkout, pending a submodule bump", strict=True) + @pytest.mark.xfail(reason="monero_tx_query::to_rapidjson_val() never serialized input_query/output_query", strict=True) def test_tx_query_input_and_output_query_serialize_round_trip(self) -> None: - tx_query = MoneroTxQuery() + tx_query: MoneroTxQuery = MoneroTxQuery() tx_query.input_query = MoneroOutputQuery() tx_query.input_query.amount = 5 tx_query.input_query.index = 1 @@ -414,7 +414,7 @@ def test_tx_query_input_and_output_query_serialize_round_trip(self) -> None: tx_query.output_query.amount = 7 tx_query.output_query.index = 2 - json_str = tx_query.serialize() + json_str: str = tx_query.serialize() logger.debug(f"Serialized tx query with input/output query: {json_str}") assert "inputQuery" in json_str assert "outputQuery" in json_str @@ -428,21 +428,21 @@ def test_tx_query_input_and_output_query_serialize_round_trip(self) -> None: assert restored.output_query.index == 2 def test_integrated_address_deserialize(self) -> None: - address = MoneroIntegratedAddress() + address: MoneroIntegratedAddress = MoneroIntegratedAddress() address.standard_address = TestUtils.ADDRESS address.payment_id = "d" * 16 address.integrated_address = TestUtils.ADDRESS AssertUtils.assert_serialization_integrity(address) def test_key_image_import_result_deserialize(self) -> None: - result = MoneroKeyImageImportResult() + result: MoneroKeyImageImportResult = MoneroKeyImageImportResult() result.height = 3000000 result.spent_amount = 500000 result.unspent_amount = 1500000 AssertUtils.assert_serialization_integrity(result) def test_message_signature_result_deserialize(self) -> None: - result = MoneroMessageSignatureResult() + result: MoneroMessageSignatureResult = MoneroMessageSignatureResult() result.is_good = True result.is_old = False result.version = 2 @@ -450,7 +450,7 @@ def test_message_signature_result_deserialize(self) -> None: AssertUtils.assert_serialization_integrity(result) def test_check_tx_deserialize(self) -> None: - check = MoneroCheckTx() + check: MoneroCheckTx = MoneroCheckTx() check.is_good = True check.in_tx_pool = False check.num_confirmations = 10 @@ -458,14 +458,14 @@ def test_check_tx_deserialize(self) -> None: AssertUtils.assert_serialization_integrity(check) def test_check_reserve_deserialize(self) -> None: - check = MoneroCheckReserve() + check: MoneroCheckReserve = MoneroCheckReserve() check.is_good = True check.total_amount = 1000000 check.unconfirmed_spent_amount = 0 AssertUtils.assert_serialization_integrity(check) def test_multisig_info_deserialize(self) -> None: - info = MoneroMultisigInfo() + info: MoneroMultisigInfo = MoneroMultisigInfo() info.is_multisig = True info.is_ready = True info.threshold = 2 @@ -473,19 +473,19 @@ def test_multisig_info_deserialize(self) -> None: AssertUtils.assert_serialization_integrity(info) def test_multisig_init_result_deserialize(self) -> None: - result = MoneroMultisigInitResult() + result: MoneroMultisigInitResult = MoneroMultisigInitResult() result.address = TestUtils.ADDRESS result.multisig_hex = "deadbeef" AssertUtils.assert_serialization_integrity(result) def test_multisig_sign_result_deserialize(self) -> None: - result = MoneroMultisigSignResult() + result: MoneroMultisigSignResult = MoneroMultisigSignResult() result.signed_multisig_tx_hex = "deadbeef" result.tx_hashes = ["a" * 64, "b" * 64] AssertUtils.assert_serialization_integrity(result) def test_address_book_entry_deserialize(self) -> None: - entry = MoneroAddressBookEntry() + entry: MoneroAddressBookEntry = MoneroAddressBookEntry() entry.index = 0 entry.address = TestUtils.ADDRESS entry.description = "friend" @@ -493,21 +493,21 @@ def test_address_book_entry_deserialize(self) -> None: AssertUtils.assert_serialization_integrity(entry) def test_account_tag_deserialize(self) -> None: - tag = MoneroAccountTag() + tag: MoneroAccountTag = MoneroAccountTag() tag.tag = "savings" tag.label = "Savings accounts" tag.account_indices = [0, 1, 2] AssertUtils.assert_serialization_integrity(tag) def test_tx_set_deserialize(self) -> None: - tx_set = MoneroTxSet() + tx_set: MoneroTxSet = MoneroTxSet() tx_set.unsigned_tx_hex = "deadbeef" tx_set.multisig_tx_hex = "beefdead" AssertUtils.assert_serialization_integrity(tx_set) - @pytest.mark.xfail(reason="monero_tx_set::deserialize() -- the static JSON-string entry point MoneroWalletFull.describeTxSet() uses to send a tx set to native code -- never handled \"signedTxHex\" and rejects any unrecognized key outright, so describing an already-signed tx set always raised \"field 'signedTxHex' not supported\" even though monero_tx_set::to_rapidjson_val() always wrote it; fixed upstream in the local everoddandeven/monero-cpp checkout, pending a submodule bump", strict=True) + @pytest.mark.xfail(reason="monero_tx_set::deserialize() bug", strict=True) def test_tx_set_signed_tx_hex_deserialize(self) -> None: - tx_set = MoneroTxSet() + tx_set: MoneroTxSet = MoneroTxSet() tx_set.signed_tx_hex = "deadbeef" AssertUtils.assert_serialization_integrity(tx_set) @@ -516,25 +516,25 @@ def test_tx_set_signed_tx_hex_deserialize(self) -> None: #region Copy / merge / comparators def test_incoming_transfer_copy(self) -> None: - transfer = MoneroIncomingTransfer() + transfer: MoneroIncomingTransfer = MoneroIncomingTransfer() transfer.amount = 500000 transfer.account_index = 0 transfer.subaddress_index = 1 transfer.address = TestUtils.ADDRESS transfer.num_suggested_confirmations = 10 - copy = transfer.copy() + copy: MoneroIncomingTransfer = transfer.copy() assert copy is not transfer assert copy.serialize() == transfer.serialize() def test_incoming_transfer_merge(self) -> None: - a = MoneroIncomingTransfer() + a: MoneroIncomingTransfer = MoneroIncomingTransfer() a.amount = 500000 a.account_index = 0 a.subaddress_index = 1 # a.tx is left unset on both sides so merge() won't recurse into tx merge - b = a.copy() + b: MoneroIncomingTransfer = a.copy() b.address = TestUtils.ADDRESS # a.address is unset -> merge fills the gap a.merge(b) assert a.address == TestUtils.ADDRESS @@ -553,7 +553,7 @@ def test_tx_wallet_merge_incoming_transfers_with_unset_indices_are_kept_distinct (NDEBUG/ODR ambiguity between monero-cpp and monero-python's own compiled units), which a plain in-process assertion can't survive. """ - script = ( + script: str = ( "import monero, sys\n" "tx_a = monero.MoneroTxWallet()\n" "tx_a.hash = 'a' * 64\n" @@ -571,7 +571,7 @@ def test_tx_wallet_merge_incoming_transfers_with_unset_indices_are_kept_distinct "n = len(tx_a.incoming_transfers) if tx_a.incoming_transfers else 0\n" "sys.exit(0 if n == 2 else f'incoming_transfers not kept distinct: len={n}')\n" ) - result = subprocess.run([sys.executable, "-c", script], capture_output=True, text=True, timeout=30) + result: subprocess.CompletedProcess[str] = subprocess.run([sys.executable, "-c", script], capture_output=True, text=True, timeout=30) logger.debug(f"subprocess exit code: {result.returncode}, stderr: {result.stderr.strip()}") assert result.returncode == 0, ( f"TxWallet.merge() did not keep unset-index incoming transfers distinct " @@ -579,12 +579,12 @@ def test_tx_wallet_merge_incoming_transfers_with_unset_indices_are_kept_distinct ) def test_incoming_transfer_lt_comparator(self) -> None: - t1 = MoneroIncomingTransfer() + t1: MoneroIncomingTransfer = MoneroIncomingTransfer() t1.tx = MoneroTxWallet() t1.account_index = 0 t1.subaddress_index = 0 - t2 = MoneroIncomingTransfer() + t2: MoneroIncomingTransfer = MoneroIncomingTransfer() t2.tx = MoneroTxWallet() t2.account_index = 0 t2.subaddress_index = 1 @@ -594,30 +594,30 @@ def test_incoming_transfer_lt_comparator(self) -> None: assert IncomingTransferComparator.compare(t1, t2) assert not IncomingTransferComparator.compare(t2, t1) - transfers = [t2, t1] + transfers: list[MoneroIncomingTransfer] = [t2, t1] transfers.sort() assert transfers[0] is t1 assert transfers[1] is t2 def test_outgoing_transfer_copy(self) -> None: - transfer = MoneroOutgoingTransfer() + transfer: MoneroOutgoingTransfer = MoneroOutgoingTransfer() transfer.amount = 500000 transfer.account_index = 0 transfer.addresses = [TestUtils.ADDRESS] transfer.subaddress_indices = [0] transfer.destinations = [MoneroDestination(TestUtils.ADDRESS, 500000)] - copy = transfer.copy() + copy: MoneroOutgoingTransfer = transfer.copy() assert copy is not transfer assert copy.serialize() == transfer.serialize() def test_outgoing_transfer_merge(self) -> None: - a = MoneroOutgoingTransfer() + a: MoneroOutgoingTransfer = MoneroOutgoingTransfer() a.amount = 500000 a.account_index = 0 # a.addresses/subaddress_indices/destinations left empty on both sides so far - b = a.copy() + b: MoneroOutgoingTransfer = a.copy() b.addresses = [TestUtils.ADDRESS] b.subaddress_indices = [0] b.destinations = [MoneroDestination(TestUtils.ADDRESS, 500000)] @@ -626,9 +626,9 @@ def test_outgoing_transfer_merge(self) -> None: assert a.subaddress_indices == [0] assert len(a.destinations) == 1 - @pytest.mark.xfail(reason="monero_outgoing_transfer::merge() dereferences destination address/amount unconditionally (boost::optional UB when unset) and segfaults the interpreter", strict=True) + @pytest.mark.xfail(reason="monero_outgoing_transfer::merge() dereferences destination address/amount unconditionally", strict=True) def test_outgoing_transfer_merge_destinations_with_unset_fields(self) -> None: - script = ( + script: str = ( "import monero, sys\n" # dirty the heap first: a clean freshly-started interpreter doesn't reliably # reproduce the crash, but a heap with realistic allocation churn (much closer to @@ -652,7 +652,7 @@ def test_outgoing_transfer_merge_destinations_with_unset_fields(self) -> None: "except RuntimeError as e:\n" " sys.exit(0 if str(e) == 'Destination vectors are different' else f'wrong message: {e}')\n" ) - result = subprocess.run([sys.executable, "-c", script], capture_output=True, text=True, timeout=30) + result: subprocess.CompletedProcess[str] = subprocess.run([sys.executable, "-c", script], capture_output=True, text=True, timeout=30) logger.debug(f"subprocess exit code: {result.returncode}, stderr: {result.stderr.strip()}") assert result.returncode == 0, ( f"outgoing_transfer.merge() did not cleanly raise 'Destination vectors are different' " @@ -660,7 +660,7 @@ def test_outgoing_transfer_merge_destinations_with_unset_fields(self) -> None: ) def test_output_wallet_copy(self) -> None: - output = MoneroOutputWallet() + output: MoneroOutputWallet = MoneroOutputWallet() output.amount = 1000000 output.index = 2 output.account_index = 0 @@ -668,25 +668,25 @@ def test_output_wallet_copy(self) -> None: output.is_spent = False output.is_frozen = False - copy = output.copy() + copy: MoneroOutputWallet = output.copy() assert copy is not output assert copy.serialize() == output.serialize() def test_output_wallet_merge(self) -> None: - a = MoneroOutputWallet() + a: MoneroOutputWallet = MoneroOutputWallet() a.amount = 1000000 a.index = 2 a.account_index = 0 a.subaddress_index = 1 # a.tx is left unset on both sides so merge() won't recurse into tx merge - b = a.copy() + b: MoneroOutputWallet = a.copy() b.is_spent = True # a.is_spent is unset -> merge fills the gap a.merge(b) assert a.is_spent is True def test_output_wallet_lt_comparator(self) -> None: - o1 = MoneroOutputWallet() + o1: MoneroOutputWallet = MoneroOutputWallet() o1.tx = MoneroTx() o1.account_index = 0 o1.subaddress_index = 0 @@ -694,7 +694,7 @@ def test_output_wallet_lt_comparator(self) -> None: o1.key_image = MoneroKeyImage() o1.key_image.hex = "a" * 64 - o2 = MoneroOutputWallet() + o2: MoneroOutputWallet = MoneroOutputWallet() o2.tx = MoneroTx() o2.account_index = 0 o2.subaddress_index = 0 @@ -707,47 +707,47 @@ def test_output_wallet_lt_comparator(self) -> None: assert OutputComparator.compare(o1, o2) assert not OutputComparator.compare(o2, o1) - outputs = [o2, o1] + outputs: list[MoneroOutputWallet] = [o2, o1] outputs.sort() assert outputs[0] is o1 assert outputs[1] is o2 def test_tx_wallet_copy(self) -> None: - tx = MoneroTxWallet() + tx: MoneroTxWallet = MoneroTxWallet() tx.hash = "a" * 64 tx.is_confirmed = True tx.note = "hello" - copy = tx.copy() + copy: MoneroTxWallet = tx.copy() assert copy is not tx assert copy.serialize() == tx.serialize() def test_tx_wallet_merge(self) -> None: - a = MoneroTxWallet() + a: MoneroTxWallet = MoneroTxWallet() a.hash = "a" * 64 a.is_confirmed = True # required: base monero_tx::merge() dereferences is_confirmed directly - b = a.copy() + b: MoneroTxWallet = a.copy() b.note = "hello" # a.note is unset -> merge fills the gap a.merge(b) assert a.note == "hello" @pytest.mark.xfail(reason="gen_utils::reconcile()'s bug", strict=True) def test_tx_wallet_merge_is_locked_can_become_false(self) -> None: - a = MoneroTxWallet() + a: MoneroTxWallet = MoneroTxWallet() a.hash = "a" * 64 a.is_confirmed = True a.is_locked = False # self: already unlocked - b = a.copy() + b: MoneroTxWallet = a.copy() b.is_locked = True # other: still locked a.merge(b) assert a.is_locked is False @pytest.mark.xfail(reason="TODO monero-cpp bug", strict=True) def test_tx_wallet_outputs_deserialize_as_output_wallet(self) -> None: - tx = MoneroTxWallet() + tx: MoneroTxWallet = MoneroTxWallet() tx.hash = "a" * 64 - output = MoneroOutputWallet() + output: MoneroOutputWallet = MoneroOutputWallet() output.amount = 500000 output.index = 3 output.account_index = 2 @@ -756,11 +756,11 @@ def test_tx_wallet_outputs_deserialize_as_output_wallet(self) -> None: output.is_frozen = False tx.outputs = [output] - json_str = tx.serialize() + json_str: str = tx.serialize() assert "accountIndex" in json_str assert "isSpent" in json_str - restored = MoneroTxWallet.deserialize(json_str) + restored: MoneroTxWallet = MoneroTxWallet.deserialize(json_str) assert len(restored.outputs) == 1 assert isinstance(restored.outputs[0], MoneroOutputWallet) assert restored.outputs[0].account_index == 2 @@ -770,61 +770,62 @@ def test_tx_wallet_outputs_deserialize_as_output_wallet(self) -> None: @pytest.mark.xfail(reason="TODO monero-cpp bug", strict=True) def test_tx_wallet_get_outputs_wallet_after_deserialize(self) -> None: - tx = MoneroTxWallet() + tx: MoneroTxWallet = MoneroTxWallet() tx.hash = "a" * 64 - output = MoneroOutputWallet() + output: MoneroOutputWallet = MoneroOutputWallet() output.amount = 500000 output.index = 3 tx.outputs = [output] - restored = MoneroTxWallet.deserialize(tx.serialize()) - outputs_wallet = restored.get_outputs_wallet() # once deserialize works: raises "nullptr given to monero_output_query::meets_criteria()" + restored: MoneroTxWallet = MoneroTxWallet.deserialize(tx.serialize()) + # once deserialize works: raises "nullptr given to monero_output_query::meets_criteria()" + outputs_wallet: list[MoneroOutputWallet] = restored.get_outputs_wallet() assert len(outputs_wallet) == 1 assert outputs_wallet[0].amount == 500000 - @pytest.mark.xfail(reason="monero_output::copy() is not virtual, so monero_tx::copy()'s inputs/outputs loop always copies through it regardless of the actual dynamic type, silently downgrading a MoneroOutputWallet to a plain MoneroOutput and dropping its wallet-only fields (this is independent of deserialize -- it reproduces on an in-memory tx too); fixed upstream in the local everoddandeven/monero-cpp checkout, pending a submodule bump", strict=True) + @pytest.mark.xfail(reason="TODO monero-cpp fix monero_tx::copy()", strict=True) def test_tx_wallet_copy_preserves_output_wallet_type(self) -> None: - tx = MoneroTxWallet() + tx: MoneroTxWallet = MoneroTxWallet() tx.hash = "a" * 64 - output = MoneroOutputWallet() + output: MoneroOutputWallet = MoneroOutputWallet() output.amount = 500000 output.account_index = 2 output.is_spent = True tx.outputs = [output] - copy = tx.copy() + copy: MoneroTxWallet = tx.copy() assert len(copy.outputs) == 1 assert isinstance(copy.outputs[0], MoneroOutputWallet) assert copy.outputs[0].account_index == 2 assert copy.outputs[0].is_spent is True def test_transfer_query_copy(self) -> None: - query = MoneroTransferQuery() + query: MoneroTransferQuery = MoneroTransferQuery() query.amount = 500000 query.incoming = True query.address = TestUtils.ADDRESS - copy = query.copy() + copy: MoneroTransferQuery = query.copy() assert copy is not query assert copy.serialize() == query.serialize() def test_output_query_copy(self) -> None: - query = MoneroOutputQuery() + query: MoneroOutputQuery = MoneroOutputQuery() query.amount = 1000000 query.min_amount = 100000 query.max_amount = 2000000 - copy = query.copy() + copy: MoneroOutputQuery = query.copy() assert copy is not query assert copy.serialize() == query.serialize() def test_tx_query_copy(self) -> None: - query = MoneroTxQuery() + query: MoneroTxQuery = MoneroTxQuery() query.hash = "a" * 64 query.height = 3000000 query.is_confirmed = True - copy = query.copy() + copy: MoneroTxQuery = query.copy() assert copy is not query assert copy.serialize() == query.serialize() diff --git a/tests/test_monero_wallet_rpc.py b/tests/test_monero_wallet_rpc.py index b090365..402f669 100644 --- a/tests/test_monero_wallet_rpc.py +++ b/tests/test_monero_wallet_rpc.py @@ -118,7 +118,7 @@ def test_get_subaddress_address_out_of_range(self, wallet: MoneroWallet) -> None accounts: list[MoneroAccount] = wallet.get_accounts(True) account_idx: int = len(accounts) - 1 subaddress_idx: int = len(accounts[account_idx].subaddresses) - address = wallet.get_address(account_idx, subaddress_idx) + address: str = wallet.get_address(account_idx, subaddress_idx) assert address is None or len(address) == 0 # Can create a wallet with a randomly generated seed diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py index fee56bb..3711aa3 100644 --- a/tests/utils/__init__.py +++ b/tests/utils/__init__.py @@ -16,7 +16,7 @@ from .wallet_sync_printer import WalletSyncPrinter from .address_book import AddressBook from .keys_book import KeysBook -from .context import TestContext, BinaryBlockContext, TxContext +from .context import SerializableContext, TestContext, BinaryBlockContext, TxContext from .string_utils import StringUtils from .wallet_equality_utils import WalletEqualityUtils from .wallet_tx_tracker import WalletTxTracker @@ -66,6 +66,7 @@ 'WalletSyncPrinter', 'AddressBook', 'KeysBook', + 'SerializableContext', 'TestContext', 'TxContext', 'BinaryBlockContext', diff --git a/tests/utils/address_book.py b/tests/utils/address_book.py index a40f834..adf36e9 100644 --- a/tests/utils/address_book.py +++ b/tests/utils/address_book.py @@ -8,7 +8,9 @@ class AddressBook: """Address book to use in tests.""" + network_type: MoneroNetworkType = MoneroNetworkType.MAINNET + """Address book's network type.""" primary_address_1: str = "" """First test primary address.""" primary_address_2: str = "" @@ -26,9 +28,13 @@ class AddressBook: subaddress_4: str = "" """Fourth test subaddress.""" integrated_1: str = "" + """First integrated subaddress.""" integrated_2: str = "" + """Second integrated subaddress.""" integrated_3: str = "" + """Third integrated subaddress.""" integrated_4: str = "" + """Fourth integrated subaddress.""" invalid_1: str = "" """First invalid address.""" invalid_2: str = "" @@ -47,7 +53,7 @@ def parse(cls, parser: ConfigParser, section: str) -> AddressBook: if not parser.has_section(section): raise Exception(f"Cannot parse address book entry, invalid section '{section}'") - entry = cls() + entry: AddressBook = cls() entry.primary_address_1 = parser.get(section, 'primary_address_1') entry.primary_address_2 = parser.get(section, 'primary_address_2') entry.primary_address_3 = parser.get(section, 'primary_address_3') diff --git a/tests/utils/assert_utils.py b/tests/utils/assert_utils.py index eff6eab..478b335 100644 --- a/tests/utils/assert_utils.py +++ b/tests/utils/assert_utils.py @@ -26,28 +26,43 @@ def assert_equals(cls, expr1: Any, expr2: Any, message: str = "assertion failed" assert expr1.priority == expr2.priority assert expr1.timeout_ms == expr2.timeout_ms elif isinstance(expr1, SerializableStruct) and isinstance(expr2, SerializableStruct): - str1 = expr1.serialize() - str2 = expr2.serialize() + str1: str = expr1.serialize() + str2: str = expr2.serialize() assert str1 == str2, f"{message}: {str1} == {str2}" else: assert expr1 == expr2, f"{message}: {expr1} == {expr2}" @classmethod - def assert_list_equals(cls, expr1: list[Any], expr2: list[Any], message: str = "lists doesn't equal") -> None: - size1: int = len(expr1) - size2: int = len(expr2) - assert size1 == size2, f"{size1} = {size2}" - for i, elem1 in enumerate(expr1): - elem2: Any = expr2[i] + def assert_list_equals[T](cls, list_1: list[T] | None, list_2: list[T] | None, message: str = "lists doesn't equal") -> None: + """Check for lists equality. + + :param list[T] | None list_1: first list to assert equality with `list_2`. + :param list[T] | None list_2: second list to assert equality with `list_1`. + :param str message: failure message. + :raises AssertionError: raises if `list_1` is not equal to `list_2`. + """ + if list_1 == list_2: + return + elif list_1 is None and list_2 is None: + # TODO raise AssertError? + return + + assert list_1 is not None, "list_1 is None" + assert list_2 is not None, "list_2 is None" + size1: int = len(list_1) + size2: int = len(list_2) + assert size1 == size2, f"Lists size mismatch: (list_1) {size1} != (list_2) {size2}" + for i, elem1 in enumerate(list_1): + elem2: T = list_2[i] cls.assert_equals(elem1, elem2, message) @classmethod - def assert_serialization_integrity(cls, obj: Any) -> Any: + def assert_serialization_integrity[T: SerializableStruct](cls, obj: T) -> T: """Serialize obj, deserialize it back through the model's own from_property_tree binding, and assert the result matches the original field for field. - :param Any obj: object to verity serialization integrity. - :return Any: new deserialized object. + :param T obj: object to verity serialization integrity. + :return T: new deserialized object. """ cls = type(obj) # type: ignore json_str: str = obj.serialize() diff --git a/tests/utils/base_test_class.py b/tests/utils/base_test_class.py index 12e0862..894b8c2 100644 --- a/tests/utils/base_test_class.py +++ b/tests/utils/base_test_class.py @@ -14,7 +14,11 @@ class BaseTestClass(ABC): # Setup and teardown of test class @pytest.fixture(scope="class", autouse=True) def global_setup_and_teardown(self) -> Generator[None, Any, None]: - """Executed once before all tests.""" + """Executed once before all tests. + + :returns Generator[None, Any, None]: yields control to the test class after setup, then + runs teardown once all tests in the class have completed. + """ self.before_all() yield self.after_all() @@ -22,7 +26,12 @@ def global_setup_and_teardown(self) -> Generator[None, Any, None]: # Setup and teardown of each test @pytest.fixture(autouse=True) def setup_and_teardown(self, request: pytest.FixtureRequest) -> Generator[None, Any, None]: - """Executed before each test.""" + """Executed before each test. + + :param pytest.FixtureRequest request: the pytest request fixture for the running test. + :returns Generator[None, Any, None]: yields control to the test after setup, then runs + teardown once the test has completed. + """ self.before_each(request) yield self.after_each(request) @@ -45,7 +54,7 @@ def after_all(self) -> None: def before_each(self, request: pytest.FixtureRequest) -> None: """Executed before each test. - :param pytest.FixtureRequest: Request fixture. + :param pytest.FixtureRequest request: Request fixture. """ msg: str = f"Before {request.node.name}" # type: ignore MoneroUtils.log_info(msg) @@ -55,7 +64,7 @@ def before_each(self, request: pytest.FixtureRequest) -> None: def after_each(self, request: pytest.FixtureRequest) -> None: """Executed after each test. - :param pytest.FixtureRequest: Request fixture. + :param pytest.FixtureRequest request: Request fixture. """ msg: str = f"After {request.node.name}" # type: ignore MoneroUtils.log_info(msg) diff --git a/tests/utils/block_utils.py b/tests/utils/block_utils.py index 64a07fe..29450e0 100644 --- a/tests/utils/block_utils.py +++ b/tests/utils/block_utils.py @@ -24,6 +24,7 @@ def test_block_header(cls, header: MoneroBlockHeader, is_full: Optional[bool], d :param MoneroBlockHeader header: header to test. :param bool | None is_full: check full header. + :param bool debug: enable logging debug messages (default to `True`). """ if debug: logger.debug(f"Testing block header: {header.serialize()}") @@ -107,7 +108,7 @@ def test_block(cls, block: MoneroBlock, ctx: TestContext) -> None: :param MoneroBlock | None block: block to test. :param TestContext ctx: test context. """ - logger.debug(f"Testing block: {block.serialize()}") + logger.debug(f"Testing block: {block.serialize()}. Context: {ctx.serialize()}") # test required fields assert block.miner_tx is not None, "Expected block miner tx" @@ -160,7 +161,7 @@ def test_get_blocks_range( :param int | None end_height: range end height. :param int chain_height: blockchain height. :param bool chunked: get blocks range chunked. - :param BinaryBlockContext: binary block test context. + :param BinaryBlockContext block_ctx: binary block test context. """ # fetch blocks by range real_start_height: int = 0 if start_height is None else start_height @@ -187,6 +188,7 @@ def is_tx_in_block(cls, tx_hash: str | None, block: MoneroBlock) -> bool: :param str | None tx_hash: tx's hash to check if included in block. :param MoneroBlock block: block to check if `tx` is included in. + :returns bool: `True` if `tx_hash` is included in `block`, `False` otherwise. """ # validate tx hash assert tx_hash is not None diff --git a/tests/utils/blockchain_utils.py b/tests/utils/blockchain_utils.py index 9fa0a86..4cc9024 100644 --- a/tests/utils/blockchain_utils.py +++ b/tests/utils/blockchain_utils.py @@ -2,7 +2,7 @@ from abc import ABC from time import sleep -from monero import MoneroNetworkType, MoneroGenerateBlocksResult +from monero import MoneroNetworkType, MoneroGenerateBlocksResult, MoneroDaemonRpc, MoneroBlockHeader from .string_utils import StringUtils from .test_utils import TestUtils as Utils @@ -12,9 +12,8 @@ logger: logging.Logger = logging.getLogger("BlockchainUtils") -# Blockchain utilities to be used in integration tests class BlockchainUtils(ABC): - """Blockchain utilities.""" + """Blockchain utilities to be used in integration tests.""" CHECK_BLOCK_TIMEOUT_SECONDS: int = 5 """Timeout in seconds to check blockchain mining progress.""" @@ -52,7 +51,7 @@ def wait_for_height(cls, height: int) -> int: :param int height: height to wait for. :returns int: blockchain height. """ - daemon = MiningUtils.get_daemon() + daemon: MoneroDaemonRpc = MiningUtils.get_daemon() current_height: int = daemon.get_height() # check if already reached height if height <= current_height: @@ -67,9 +66,9 @@ def wait_for_height(cls, height: int) -> int: # wait until blockchain reaches desired height while current_height < height: - p = StringUtils.get_percentage(current_height, height) + p: str = StringUtils.get_percentage(current_height, height) logger.info(f"[{p}] Waiting for blockchain height ({current_height}/{height})") - block = daemon.wait_for_next_block_header() + block: MoneroBlockHeader = daemon.wait_for_next_block_header() assert block.height is not None current_height = block.height sleep(cls.CHECK_BLOCK_TIMEOUT_SECONDS) @@ -147,14 +146,14 @@ def reorg_blockchain(cls, num_blocks: int = 3) -> int: """ assert num_blocks >= 1, f"Invalid number of blocks to reorg: {num_blocks}" - daemon = MiningUtils.get_daemon() + daemon: MoneroDaemonRpc = MiningUtils.get_daemon() # mining must be stopped so it doesn't race with the forced reorg stop_mining: bool = MiningUtils.try_stop_mining() try: # mark the fork point: the last common block between both chains - fork_header = daemon.get_last_block_header() + fork_header: MoneroBlockHeader = daemon.get_last_block_header() assert fork_header.hash is not None and fork_header.height is not None fork_hash: str = fork_header.hash fork_height: int = fork_header.height @@ -184,7 +183,7 @@ def reorg_blockchain(cls, num_blocks: int = 3) -> int: assert active_hash in alt.block_hashes, \ "Reorg failed: active chain block is not part of the generated alternate chain" - original_header = daemon.get_block_header_by_hash(original_hash) + original_header: MoneroBlockHeader = daemon.get_block_header_by_hash(original_hash) assert original_header.orphan_status is True, \ "Reorg failed: original chain block was not marked as orphaned" diff --git a/tests/utils/context/__init__.py b/tests/utils/context/__init__.py index 8171ae4..c8b95ad 100644 --- a/tests/utils/context/__init__.py +++ b/tests/utils/context/__init__.py @@ -1,8 +1,10 @@ +from .serializable_context import SerializableContext from .test_context import TestContext from .binary_block_context import BinaryBlockContext from .tx_context import TxContext __all__ = [ + 'SerializableContext', 'TestContext', 'BinaryBlockContext', 'TxContext' diff --git a/tests/utils/context/serializable_context.py b/tests/utils/context/serializable_context.py new file mode 100644 index 0000000..fad791e --- /dev/null +++ b/tests/utils/context/serializable_context.py @@ -0,0 +1,71 @@ +from __future__ import annotations + +import json + +from abc import ABC, abstractmethod +from typing import Any, TypeVar + +T = TypeVar("T", bound="SerializableContext") + + +class SerializableContext(ABC): + """Base class for test contexts that can be serialized to JSON. + + Mirrors monero-cpp's ``serializable_struct``: :meth:`serialize` emits a + compact JSON string holding only the fields that are defined (i.e. not + ``None``), while :meth:`deserialize` rebuilds an instance from such a string + through the subclass' :meth:`from_dict`. + """ + + __test__ = False + + def serialize(self) -> str: + """Serialize this context to a compact JSON string. + + :returns str: the context serialized to a JSON string. + """ + return json.dumps(self.to_dict(), separators=(",", ":")) + + @abstractmethod + def to_dict(self) -> dict[str, Any]: + """Build the JSON object for this context, adding only defined fields. + + Analogous to monero-cpp's ``to_rapidjson_val``. + + :returns dict[str, Any]: the context as a JSON-serializable dict. + """ + ... + + @classmethod + def deserialize(cls: type[T], context_json: str) -> T: + """Deserialize a context from a JSON string. + + :param str context_json: context in JSON format. + :returns SerializableContext: deserialized instance. + """ + ctx: T = cls() + cls.from_dict(json.loads(context_json), ctx) + return ctx + + @staticmethod + @abstractmethod + def from_dict(node: dict[str, Any], ctx: Any) -> None: + """Populate ``ctx`` from a parsed JSON object. + + Analogous to monero-cpp's ``from_property_tree``. + + :param dict[str, Any] node: parsed JSON object. + :param SerializableContext ctx: instance to populate. + """ + ... + + @staticmethod + def _put(root: dict[str, Any], key: str, value: Any) -> None: + """Add ``key`` -> ``value`` to ``root`` only when ``value`` is defined. + + :param dict[str, Any] root: JSON object being built. + :param str key: member name. + :param Any value: member value, skipped when ``None``. + """ + if value is not None: + root[key] = value diff --git a/tests/utils/context/test_context.py b/tests/utils/context/test_context.py index 53878c8..dda5686 100644 --- a/tests/utils/context/test_context.py +++ b/tests/utils/context/test_context.py @@ -1,9 +1,11 @@ from __future__ import annotations -from typing import Optional +from typing import Any, Optional +from .serializable_context import SerializableContext -class TestContext: + +class TestContext(SerializableContext): """Provides context or configuration for test methods to test a type.""" __test__ = False @@ -54,3 +56,61 @@ def __init__(self, ctx: Optional[TestContext] = None) -> None: self.has_txs = ctx.has_txs self.header_is_full = ctx.header_is_full self.tx_context = ctx.tx_context + + def to_dict(self) -> dict[str, Any]: + """Build the JSON object for this test context (defined fields only). + + :returns dict[str, Any]: the context as a JSON-serializable dict. + """ + root: dict[str, Any] = {} + self._put(root, "hasJson", self.has_json) + self._put(root, "isPruned", self.is_pruned) + self._put(root, "isFull", self.is_full) + self._put(root, "isConfirmed", self.is_confirmed) + self._put(root, "isMinerTx", self.is_miner_tx) + self._put(root, "fromGetTxPool", self.from_get_tx_pool) + self._put(root, "fromBinaryBlock", self.from_binary_block) + self._put(root, "hasOutputIndices", self.has_output_indices) + self._put(root, "doNotTestCopy", self.do_not_test_copy) + self._put(root, "hasTxs", self.has_txs) + self._put(root, "hasHex", self.has_hex) + self._put(root, "headerIsFull", self.header_is_full) + if self.tx_context is not None: + root["txContext"] = self.tx_context.to_dict() + return root + + @staticmethod + def from_dict(node: dict[str, Any], ctx: TestContext) -> None: + """Populate ``ctx`` from a parsed JSON object. + + :param dict[str, Any] node: parsed JSON object. + :param TestContext ctx: instance to populate. + """ + for key, value in node.items(): + if key == "hasJson": + ctx.has_json = value + elif key == "isPruned": + ctx.is_pruned = value + elif key == "isFull": + ctx.is_full = value + elif key == "isConfirmed": + ctx.is_confirmed = value + elif key == "isMinerTx": + ctx.is_miner_tx = value + elif key == "fromGetTxPool": + ctx.from_get_tx_pool = value + elif key == "fromBinaryBlock": + ctx.from_binary_block = value + elif key == "hasOutputIndices": + ctx.has_output_indices = value + elif key == "doNotTestCopy": + ctx.do_not_test_copy = value + elif key == "hasTxs": + ctx.has_txs = value + elif key == "hasHex": + ctx.has_hex = value + elif key == "headerIsFull": + ctx.header_is_full = value + elif key == "txContext": + ctx.tx_context = TestContext() + TestContext.from_dict(value, ctx.tx_context) diff --git a/tests/utils/context/tx_context.py b/tests/utils/context/tx_context.py index 02f2a54..be1f028 100644 --- a/tests/utils/context/tx_context.py +++ b/tests/utils/context/tx_context.py @@ -1,10 +1,14 @@ from __future__ import annotations -from typing import Optional +import json + +from typing import Any, Optional from monero import MoneroWallet, MoneroTxConfig +from .serializable_context import SerializableContext + -class TxContext: +class TxContext(SerializableContext): """Provides context or configuration for test methods to test a type.""" wallet: Optional[MoneroWallet] = None @@ -46,3 +50,55 @@ def __init__(self, ctx: Optional[TxContext] = None) -> None: self.is_send_response = ctx.is_send_response self.is_sweep_response = ctx.is_sweep_response self.is_sweep_output_response = ctx.is_sweep_output_response + + def to_dict(self) -> dict[str, Any]: + """Build the JSON object for this tx context (defined fields only). + + ``wallet`` is a live wallet handle rather than serializable data, so it + is only reflected by the ``hasWallet`` flag and is not restored on + :meth:`deserialize`. + + :returns dict[str, Any]: the context as a JSON-serializable dict. + """ + root: dict[str, Any] = {} + if self.wallet is not None: + root["hasWallet"] = True + if self.config is not None: + root["config"] = json.loads(self.config.serialize()) + self._put(root, "hasOutgoingTransfer", self.has_outgoing_transfer) + self._put(root, "hasIncomingTransfers", self.has_incoming_transfers) + self._put(root, "hasDestinations", self.has_destinations) + self._put(root, "isCopy", self.is_copy) + self._put(root, "includeOutputs", self.include_outputs) + self._put(root, "isSendResponse", self.is_send_response) + self._put(root, "isSweepResponse", self.is_sweep_response) + self._put(root, "isSweepOutputResponse", self.is_sweep_output_response) + return root + + @staticmethod + def from_dict(node: dict[str, Any], ctx: TxContext) -> None: + """Populate ``ctx`` from a parsed JSON object. + + :param dict[str, Any] node: parsed JSON object. + :param TxContext ctx: instance to populate. + """ + for key, value in node.items(): + if key == "config": + ctx.config = MoneroTxConfig.deserialize(json.dumps(value)) + elif key == "hasOutgoingTransfer": + ctx.has_outgoing_transfer = value + elif key == "hasIncomingTransfers": + ctx.has_incoming_transfers = value + elif key == "hasDestinations": + ctx.has_destinations = value + elif key == "isCopy": + ctx.is_copy = value + elif key == "includeOutputs": + ctx.include_outputs = value + elif key == "isSendResponse": + ctx.is_send_response = value + elif key == "isSweepResponse": + ctx.is_sweep_response = value + elif key == "isSweepOutputResponse": + ctx.is_sweep_output_response = value + # "hasWallet" is informational only; the live wallet handle cannot be restored diff --git a/tests/utils/daemon_notification_collector.py b/tests/utils/daemon_notification_collector.py index 2639bd9..2b139d3 100644 --- a/tests/utils/daemon_notification_collector.py +++ b/tests/utils/daemon_notification_collector.py @@ -27,14 +27,27 @@ class DaemonNotificationCollector(MoneroDaemonListener): @property def num_block_headers(self) -> int: + """Number of block headers collected so far. + + :returns int: number of block headers collected so far. + """ return len(self.block_headers) @property def num_block_hashes(self) -> int: + """Number of block hashes collected so far. + + :returns int: number of block hashes collected so far. + """ return len(self.block_hashes) def __init__(self, daemon: MoneroDaemonRpc, auto_remove: bool = False) -> None: - """Initialize a new wallet notification collector.""" + """Initialize a new wallet notification collector. + + :param MoneroDaemonRpc daemon: daemon instance to collect block headers from. + :param bool auto_remove: if `True`, self remove this listener from the daemon after the + first block notification (default `False`). + """ super().__init__() self.listening = True self.daemon = daemon @@ -46,6 +59,10 @@ def __init__(self, daemon: MoneroDaemonRpc, auto_remove: bool = False) -> None: @override def on_block_header(self, header: MoneroBlockHeader) -> None: + """Invoked when the daemon receives a new block header. + + :param MoneroBlockHeader header: the new block header received. + """ try: logger.debug(f"Collecting block header: {header.serialize()}") assert header.hash is not None diff --git a/tests/utils/daemon_utils.py b/tests/utils/daemon_utils.py index 72ed472..4781944 100644 --- a/tests/utils/daemon_utils.py +++ b/tests/utils/daemon_utils.py @@ -27,7 +27,7 @@ def is_regtest(cls, network_type_str: str | None) -> bool: """ if network_type_str is None: return False - nettype = network_type_str.lower() + nettype: str = network_type_str.lower() return nettype == "regtest" or nettype == "reg" @classmethod @@ -37,7 +37,7 @@ def parse_network_type(cls, nettype: str) -> MoneroNetworkType: :param str nettype: network type in string format. :returns MoneroNetworkType: parsed network type. """ - net = nettype.lower() + net: str = nettype.lower() if net == "mainnet" or net == "main" or cls.is_regtest(net): return MoneroNetworkType.MAINNET elif net == "testnet" or net == "test": @@ -55,6 +55,7 @@ def test_known_peer(cls, peer: MoneroPeer, from_connection: bool, debug: bool = :param MoneroPeer peer: daemon peer to test. :param bool from_connection: indicates if `peer` is obtained from daemon connections. + :param bool debug: log peer details before testing (default `True`). """ if debug: logger.debug(f"Testing known peer: {peer.serialize()}") @@ -280,6 +281,7 @@ def test_update_check_result(cls, result: MoneroDaemonUpdateCheckResult, debug: """Test daemon update check result. :param MoneroDaemonUpdateCheckResult result: daemon update check result to test. + :param bool debug: log the result before testing (default `True`). """ if debug: logger.debug(f"Testing update check result: {result.serialize()}") @@ -443,7 +445,7 @@ def get_confirmed_tx_hashes(cls, daemon: MoneroDaemon) -> list[str]: height: int = daemon.get_height() while len(hashes) < 5 and height > 0: height -= 1 - block = daemon.get_block_by_height(height) + block: MoneroBlock = daemon.get_block_by_height(height) for tx_hash in block.tx_hashes: hashes.append(tx_hash) return hashes diff --git a/tests/utils/docker_wallet_rpc_manager.py b/tests/utils/docker_wallet_rpc_manager.py index 9f89cd1..080f510 100644 --- a/tests/utils/docker_wallet_rpc_manager.py +++ b/tests/utils/docker_wallet_rpc_manager.py @@ -45,22 +45,34 @@ class DockerWalletRpcManager: @property def used_slots(self) -> int: - """Number of docker slots used.""" + """Number of docker slots used. + + :returns int: number of docker slots used. + """ return len(self._wallets) @property def free_slots(self) -> int: - """Number of docker slots not used.""" + """Number of docker slots not used. + + :returns int: number of docker slots not used. + """ return self.MAX_SLOTS - self.used_slots @property def no_slot_left(self) -> bool: - """Indicates if no docker slot is left.""" + """Indicates if no docker slot is left. + + :returns bool: `True` if no docker slot is left, `False` otherwise. + """ return self.free_slots == 0 @property def first_free_slot(self) -> int: - """The first free docker slot index (-1 for none).""" + """The first free docker slot index (-1 for none). + + :returns int: the first free docker slot index, or -1 if none is free. + """ slot_idxs: list[int] = list(self._wallets.keys()) slot_range: list[int] = list(range(self.MAX_SLOTS)) for slot_idx in slot_range: @@ -124,7 +136,7 @@ def setup_create_wallet_config(self, config: MoneroWalletConfig) -> MoneroWallet :param MoneroWalletConfig config: configuration to setup for wallet creation. :returns MoneroWalletConfig: setup config. """ - random = config.seed is None and config.primary_address is None + random: bool = config.seed is None and config.primary_address is None if config.path is None: # set random wallet path @@ -141,9 +153,11 @@ def setup_wallet_config(self, c: MoneroWalletConfig | None, create: bool, in_con :param MoneroWalletConfig | None c: wallet configuration to setup (optional). :param bool create: setup wallet creation configuration. + :param bool in_container: `True` if the daemon connection should target the containerized + daemon (`node_2`) instead of the default one. :returns MoneroWalletConfig: setup configuration. """ - config = c if c is not None else MoneroWalletConfig() + config: MoneroWalletConfig = c if c is not None else MoneroWalletConfig() # assign defaults if config.password is None: @@ -185,6 +199,8 @@ def setup_wallet(self, c: MoneroWalletConfig | None, create: bool, in_container: :param MoneroWalletConfig | None c: wallet configuration. :param bool create: create the wallet. + :param bool in_container: `True` if the daemon connection should target the containerized + daemon (`node_2`) instead of the default one. :returns MoneroWalletRpc: wallet rpc client. """ if self.no_slot_left: @@ -218,6 +234,8 @@ def create_wallet(self, c: MoneroWalletConfig | None, in_container: bool) -> Mon """Create a rpc wallet. :param MoneroWalletConfig | None c: wallet configuration. + :param bool in_container: `True` if the daemon connection should target the containerized + daemon (`node_2`) instead of the default one. :returns MoneroWalletRpc: wallet rpc client. """ return self.setup_wallet(c, True, in_container) @@ -225,7 +243,9 @@ def create_wallet(self, c: MoneroWalletConfig | None, in_container: bool) -> Mon def open_wallet(self, c: MoneroWalletConfig | None, in_container: bool) -> MoneroWalletRpc: """Open a rpc wallet. - :param MoneroWalletConfig | None: wallet configuration. + :param MoneroWalletConfig | None c: wallet configuration. + :param bool in_container: `True` if the daemon connection should target the containerized + daemon (`node_2`) instead of the default one. :returns MoneroWalletRpc: wallet rpc client. """ return self.setup_wallet(c, False, in_container) @@ -272,7 +292,7 @@ def clear(self, save: bool = False) -> None: """ for wallet in self._wallets.values(): if not wallet.is_closed(): - rpc_connection = wallet.get_rpc_connection() + rpc_connection: MoneroRpcConnection | None = wallet.get_rpc_connection() try: wallet.close(save) except Exception as e: diff --git a/tests/utils/from_multiple_tx_sender.py b/tests/utils/from_multiple_tx_sender.py index a6b8496..3560e81 100644 --- a/tests/utils/from_multiple_tx_sender.py +++ b/tests/utils/from_multiple_tx_sender.py @@ -54,8 +54,8 @@ def _get_src_account(self) -> MoneroAccount: assert len(self._accounts) >= 2, "This test requires at least 2 accounts; run send-to-multiple tests" # prefer first account instead of primary # TODO why this is needed? - primary_account = self._accounts[0] - first_account = self._accounts[1] + primary_account: MoneroAccount = self._accounts[0] + first_account: MoneroAccount = self._accounts[1] self._accounts[0] = first_account self._accounts[1] = primary_account diff --git a/tests/utils/gen_utils.py b/tests/utils/gen_utils.py index f6537b5..7ce2604 100644 --- a/tests/utils/gen_utils.py +++ b/tests/utils/gen_utils.py @@ -30,6 +30,11 @@ def wait_for(cls, milliseconds: int) -> None: @classmethod def is_empty(cls, value: Union[str, list[Any], None]) -> bool: + """Check if a string or list is empty. + + :param str | list[Any] | None value: value to check. + :returns bool: `True` if `value` is an empty string, `False` otherwise. + """ return value == "" @classmethod @@ -74,6 +79,12 @@ def current_date_time_str(cls, fmt: str = "%Y-%m-%d_%H-%M-%S") -> str: @classmethod def has_key(cls, key: Optional[str], dictionary: dict[str, Any]) -> bool: + """Check if a dictionary has a given key. + + :param str | None key: key to look for. + :param dict[str, Any] dictionary: dictionary to search. + :returns bool: `True` if `key` is a key of `dictionary`, `False` otherwise. + """ assert key is not None, "Key is None" for k in dictionary: if k == key: @@ -82,6 +93,11 @@ def has_key(cls, key: Optional[str], dictionary: dict[str, Any]) -> bool: @classmethod def count_num_instances(cls, instances: list[int]) -> dict[int, int]: + """Count the number of occurrences of each value in a list. + + :param list[int] instances: values to count occurrences of. + :returns dict[int, int]: map of each value to its number of occurrences in `instances`. + """ height_counts: dict[int, int] = {} for inst in instances: count: Optional[int] = height_counts.get(inst, None) @@ -90,6 +106,12 @@ def count_num_instances(cls, instances: list[int]) -> dict[int, int]: @classmethod def get_modes(cls, counts: dict[int, int]) -> set[int]: + """Get the value(s) with the highest occurrence count. + + :param dict[int, int] counts: map of each value to its number of occurrences, as returned + by `count_num_instances`. + :returns set[int]: the value(s) sharing the highest occurrence count. + """ modes: set[int] = set() max_count: Optional[int] = None for cnt in counts.values(): diff --git a/tests/utils/integration_test_utils.py b/tests/utils/integration_test_utils.py index e78314d..863d50c 100644 --- a/tests/utils/integration_test_utils.py +++ b/tests/utils/integration_test_utils.py @@ -47,7 +47,7 @@ def setup(cls, wallet_type: WalletType) -> None: wallet_txs: list[MoneroTxWallet] = wallet.get_txs() num_wallet_txs: int = len(wallet_txs) # fund wallet with mined coins and wait for unlocked balance - txs = cls.fund_wallet_and_wait_for_unlocked(wallet) + txs: list[MoneroTxWallet] = cls.fund_wallet_and_wait_for_unlocked(wallet) # setup first receive height tx: MoneroTxWallet = txs[0] if num_wallet_txs == 0 else wallet_txs[0] diff --git a/tests/utils/keys_book.py b/tests/utils/keys_book.py index 08cfef7..d71c2ff 100644 --- a/tests/utils/keys_book.py +++ b/tests/utils/keys_book.py @@ -36,7 +36,7 @@ def parse(cls, parser: ConfigParser) -> KeysBook: if not parser.has_section('keys'): raise Exception("Section [keys] not found") # load configuration - book = cls() + book: KeysBook = cls() book.private_view_key = parser.get('keys', 'private_view_key') book.public_view_key = parser.get('keys', 'public_view_key') book.private_spend_key = parser.get('keys', 'private_spend_key') diff --git a/tests/utils/mining_utils.py b/tests/utils/mining_utils.py index bfc53a6..a88be5a 100644 --- a/tests/utils/mining_utils.py +++ b/tests/utils/mining_utils.py @@ -1,6 +1,6 @@ import logging -from monero import MoneroDaemonRpc, MoneroGenerateBlocksResult +from monero import MoneroDaemonRpc, MoneroGenerateBlocksResult, MoneroMiningStatus from .test_utils import TestUtils as Utils @@ -27,6 +27,7 @@ def generate_blocks(cls, address: str, num_blocks: int, d: MoneroDaemonRpc | Non :param str address: is the address of the wallet to receive miner transactions if block is successfully mined. :param int num_blocks: is the number of blocks to generate. + :param MoneroDaemonRpc | None d: daemon to generate blocks with (default internal daemon). :returns MoneroGenerateBlocksResult: the result of generating blocks; height is the height of the last block generated. """ assert Utils.REGTEST, "Generating blocks is supported only on regtest." @@ -41,10 +42,10 @@ def is_mining(cls, d: MoneroDaemonRpc | None = None) -> bool: :returns bool: `True` if mining is enabled, `False` otherwise. """ # max tries 3 - daemon = cls.get_daemon() if d is None else d + daemon: MoneroDaemonRpc = cls.get_daemon() if d is None else d for i in range(3): try: - status = daemon.get_mining_status() + status: MoneroMiningStatus = daemon.get_mining_status() return status.is_active is True except Exception: @@ -62,7 +63,7 @@ def start_mining(cls, d: MoneroDaemonRpc | None = None) -> None: if cls.is_mining(): raise Exception("Mining already started") - daemon = cls.get_daemon() if d is None else d + daemon: MoneroDaemonRpc = cls.get_daemon() if d is None else d daemon.start_mining(Utils.MINING_ADDRESS, 1, False, False) @classmethod @@ -75,7 +76,7 @@ def stop_mining(cls, d: MoneroDaemonRpc | None = None) -> None: if not cls.is_mining(): raise Exception("Mining already stopped") - daemon = cls.get_daemon() if d is None else d + daemon: MoneroDaemonRpc = cls.get_daemon() if d is None else d daemon.stop_mining() @classmethod diff --git a/tests/utils/multisig_sample_code_tester.py b/tests/utils/multisig_sample_code_tester.py index 5548dec..1e09e8e 100644 --- a/tests/utils/multisig_sample_code_tester.py +++ b/tests/utils/multisig_sample_code_tester.py @@ -35,6 +35,10 @@ def __init__(self, m: int, participants: list[MoneroWallet]) -> None: self._disposed = False def make_multisig_wallets(self) -> list[str]: + """Prepare and make each participant wallet multisig. + + :returns list[str]: the multisig hex produced by each participant wallet. + """ # prepare and collect multisig hex from each participant prepared_multisig_hexes: list[str] = [] diff --git a/tests/utils/output_utils.py b/tests/utils/output_utils.py index 9220c6f..e5cba03 100644 --- a/tests/utils/output_utils.py +++ b/tests/utils/output_utils.py @@ -4,7 +4,7 @@ from typing import Optional from monero import ( - MoneroWallet, MoneroOutputQuery, + MoneroWallet, MoneroOutputQuery, MoneroTx, MoneroOutput, MoneroKeyImage, MoneroOutputWallet, MoneroOutputDistributionEntry, MoneroOutputHistogramEntry ) @@ -66,14 +66,14 @@ def test_output(cls, output: Optional[MoneroOutput], context: Optional[TestConte """Test monero output. :param MoneroOutput | None output: output to test. - :param TestContext | None: test context (default `None`). + :param TestContext | None context: test context (default `None`). """ assert output is not None GenUtils.test_unsigned_big_integer(output.amount) if context is None: return assert output.tx is not None - ctx = TestContext(context) + ctx: TestContext = TestContext(context) if output.tx.in_tx_pool or ctx.has_output_indices is False: assert output.index is None else: @@ -86,7 +86,7 @@ def test_output(cls, output: Optional[MoneroOutput], context: Optional[TestConte def test_input(cls, xmr_input: Optional[MoneroOutput], ctx: Optional[TestContext]) -> None: """Test monero input. - :param MoneroOutput | None zmr_input: input to test. + :param MoneroOutput | None xmr_input: input to test. :param TestContext | None ctx: test context (default `None`). """ assert xmr_input is not None @@ -129,7 +129,7 @@ def test_output_wallet(cls, output: Optional[MoneroOutputWallet]) -> None: GenUtils.test_unsigned_big_integer(output.amount, True) # output has circular reference to its transaction which has some initialized fields - tx = output.tx + tx: MoneroTx = output.tx assert tx is not None assert output in tx.outputs assert tx.hash is not None @@ -139,12 +139,12 @@ def test_output_wallet(cls, output: Optional[MoneroOutputWallet]) -> None: assert tx.is_confirmed is True assert tx.is_relayed is True assert tx.is_failed is False - tx_height = tx.get_height() + tx_height: int | None = tx.get_height() assert tx_height is not None assert tx_height > 0 # test copying - copy = output.copy() + copy: MoneroOutputWallet = output.copy() assert copy != output AssertUtils.assert_equals(copy, output) # TODO: should output copy do deep copy of tx so models are graph instead of tree? Would need to work out circular references @@ -160,10 +160,11 @@ def get_and_test_outputs(cls, wallet: MoneroWallet, query: Optional[MoneroOutput :param MoneroWallet wallet: wallet to get outputs from. :param MoneroOutputQuery | None query: output query. :param bool | None is_expected: expected non-empty outputs. + :returns list[MoneroOutputWallet]: the fetched, tested outputs. """ - copy = query.copy() if query is not None else None - outputs = wallet.get_outputs(query) if query is not None else wallet.get_outputs(MoneroOutputQuery()) + copy: MoneroOutputQuery | None = query.copy() if query is not None else None + outputs: list[MoneroOutputWallet] = wallet.get_outputs(query) if query is not None else wallet.get_outputs(MoneroOutputQuery()) AssertUtils.assert_equals(copy, query) if is_expected is False: diff --git a/tests/utils/send_and_update_txs_tester.py b/tests/utils/send_and_update_txs_tester.py index dbd5da1..3c919aa 100644 --- a/tests/utils/send_and_update_txs_tester.py +++ b/tests/utils/send_and_update_txs_tester.py @@ -185,7 +185,7 @@ def wait_for_confirmations(self, sent_txs: list[MoneroTxWallet], num_confirmatio self.test_out_in_pairs(updated_txs, False) # update confirmations in order to exit loop - fetched_tx = fetched_txs[0] + fetched_tx: MoneroTxWallet = fetched_txs[0] assert fetched_tx.num_confirmations is not None self.num_confirmations = fetched_tx.num_confirmations diff --git a/tests/utils/single_tx_sender.py b/tests/utils/single_tx_sender.py index bcca428..9abe4d9 100644 --- a/tests/utils/single_tx_sender.py +++ b/tests/utils/single_tx_sender.py @@ -38,30 +38,46 @@ class SingleTxSender: @property def tracker(self) -> TxTracker: - """Wallet transaction tracker.""" + """Wallet transaction tracker. + + :returns TxTracker: the wallet transaction tracker. + """ return TestUtils.WALLET_TX_TRACKER @property def balance_before(self) -> int: - """Wallet balance before sending.""" - balance = self._from_subaddress.balance if self._from_subaddress is not None else 0 + """Wallet balance before sending. + + :returns int: the sending subaddress' balance before sending, or 0 if not yet selected. + """ + balance: int | None = self._from_subaddress.balance if self._from_subaddress is not None else 0 return balance if balance is not None else 0 @property def unlocked_balance_before(self) -> int: - """Wallet unlocked balance before sending.""" - balance = self._from_subaddress.unlocked_balance if self._from_subaddress is not None else 0 + """Wallet unlocked balance before sending. + + :returns int: the sending subaddress' unlocked balance before sending, or 0 if not yet + selected. + """ + balance: int | None = self._from_subaddress.unlocked_balance if self._from_subaddress is not None else 0 return balance if balance is not None else 0 @property def send_amount(self) -> int: - """Amount to send.""" - b = self.unlocked_balance_before + """Amount to send. + + :returns int: the amount to send, derived from the unlocked balance before sending. + """ + b: int = self.unlocked_balance_before return int((b - TxWalletUtils.MAX_FEE) / self.SEND_DIVISOR) @property def address(self) -> str: - """Primary wallet address.""" + """Primary wallet address. + + :returns str: the wallet's primary address. + """ return self._wallet.get_primary_address() def __init__(self, wallet: MoneroWallet, config: Optional[MoneroTxConfig]) -> None: @@ -96,9 +112,9 @@ def _get_locked_txs(self) -> list[MoneroTxWallet]: :returns list[MoneroTxWallet]: locked txs. """ # query locked txs - query = MoneroTxQuery() + query: MoneroTxQuery = MoneroTxQuery() query.is_locked = True - locked_txs = WalletTxsUtils.get_and_test_txs(self._wallet, query, None, True, TestUtils.REGTEST) + locked_txs: list[MoneroTxWallet] = WalletTxsUtils.get_and_test_txs(self._wallet, query, None, True, TestUtils.REGTEST) for locked_tx in locked_txs: assert locked_tx.is_locked, "Expected locked tx" @@ -110,7 +126,7 @@ def _check_balance(self) -> None: # wait for wallet to clear unconfirmed txs self.tracker.wait_for_txs_to_clear_pool([self._wallet]) sufficient_balance: bool = False - accounts = self._wallet.get_accounts(True) + accounts: list[MoneroAccount] = self._wallet.get_accounts(True) # iterate over all wallet addresses for account in accounts: for i, subaddress in enumerate(account.subaddresses): @@ -138,7 +154,7 @@ def _check_balance_decreased(self) -> None: assert self._from_subaddress is not None assert self._from_account.index is not None assert self._from_subaddress.index is not None - subaddress = self._wallet.get_subaddress(self._from_account.index, self._from_subaddress.index) + subaddress: MoneroSubaddress = self._wallet.get_subaddress(self._from_account.index, self._from_subaddress.index) assert subaddress.balance is not None assert subaddress.balance < self.balance_before, f"Expected {subaddress.balance} < {self.balance_before}" assert subaddress.unlocked_balance is not None @@ -187,7 +203,7 @@ def _send_to_self(self, config: MoneroTxConfig) -> list[MoneroTxWallet]: :param MoneroTxConfig config: tx configuration. :returns list[MoneroTxWallet]: created txs. """ - txs = self._wallet.create_txs(config) + txs: list[MoneroTxWallet] = self._wallet.create_txs(config) if config.can_split is False: # must have exactly one tx if no split @@ -206,7 +222,7 @@ def _handle_non_relayed_tx(self, txs: list[MoneroTxWallet], config: MoneroTxConf return txs # build test context - ctx = TxContext() + ctx: TxContext = TxContext() ctx.wallet = self._wallet ctx.config = config ctx.is_send_response = True @@ -238,7 +254,7 @@ def _handle_non_relayed_tx(self, txs: list[MoneroTxWallet], config: MoneroTxConf assert len(tx_hash) == 64 # fetch txs for testing - query = MoneroTxQuery() + query: MoneroTxQuery = MoneroTxQuery() query.hashes = tx_hashes return self._wallet.get_txs(query) @@ -253,14 +269,14 @@ def send(self) -> None: assert self._from_account is not None # init tx config - config = self._build_tx_config() - config_copy = config.copy() + config: MoneroTxConfig = self._build_tx_config() + config_copy: MoneroTxConfig = config.copy() # test sending to invalid address self._send_to_invalid(config) # test send to self - txs = self._send_to_self(config) + txs: list[MoneroTxWallet] = self._send_to_self(config) logger.debug(f"Created {len(txs)} txs") @@ -279,10 +295,10 @@ def send(self) -> None: # test that balance and unlocked balance decreased self._check_balance_decreased() - locked_txs = self._get_locked_txs() + locked_txs: list[MoneroTxWallet] = self._get_locked_txs() # build test context - ctx = TxContext() + ctx: TxContext = TxContext() ctx.wallet = self._wallet ctx.config = config ctx.is_send_response = config.relay is True @@ -300,7 +316,7 @@ def send(self) -> None: assert config.payment_id == tx.payment_id # test outgoing destinations - dest_count = len(tx.outgoing_transfer.destinations) + dest_count: int = len(tx.outgoing_transfer.destinations) if dest_count > 0: assert dest_count == 1 for dest in tx.outgoing_transfer.destinations: diff --git a/tests/utils/string_utils.py b/tests/utils/string_utils.py index cc599b4..db83f84 100644 --- a/tests/utils/string_utils.py +++ b/tests/utils/string_utils.py @@ -43,5 +43,10 @@ def get_random_string(cls, n: int = 25) -> str: @classmethod def prettify(cls, json_str: str) -> str: + """Pretty-print a JSON string with indentation. + + :param str json_str: JSON string to pretty-print. + :returns str: the pretty-printed JSON string. + """ parsed_obj: Any = loads(json_str) return dumps(parsed_obj, indent=1) diff --git a/tests/utils/sync_progress_tester.py b/tests/utils/sync_progress_tester.py index 439b7cc..bb109f6 100644 --- a/tests/utils/sync_progress_tester.py +++ b/tests/utils/sync_progress_tester.py @@ -53,6 +53,14 @@ def __init__(self, wallet: MoneroWalletFull, start_height: int, end_height: int) @override def on_sync_progress(self, height: int, start_height: int, end_height: int, percent_done: float, message: str) -> None: + """Invoked on wallet sync progress. + + :param int height: current blockchain height. + :param int start_height: sync start height. + :param int end_height: sync end height. + :param float percent_done: sync percentage progress. + :param str message: sync progress message. + """ super().on_sync_progress(height, start_height, end_height, percent_done, message) # registered wallet listeners will continue to get sync notifications after the wallet's initial sync diff --git a/tests/utils/sync_seed_tester.py b/tests/utils/sync_seed_tester.py index 853d837..6cfad78 100644 --- a/tests/utils/sync_seed_tester.py +++ b/tests/utils/sync_seed_tester.py @@ -66,6 +66,13 @@ def __init__( self.test_post_sync_notifications = test_post_sync_notifications def test_post_sync(self, wallet: MoneroWalletFull, wallet_sync_tester: WalletSyncTester) -> None: + """Test that a registered wallet listener keeps receiving sync notifications after the + wallet's initial sync has completed. + + :param MoneroWalletFull wallet: wallet to start syncing and test. + :param WalletSyncTester wallet_sync_tester: listener whose post-completion notifications + are asserted. + """ # start automatic syncing wallet.start_syncing(TestUtils.SYNC_PERIOD_IN_MS) @@ -142,7 +149,7 @@ def test_notifications(self, wallet: MoneroWalletFull, start_height_expected: in # compare with ground truth if not self.skip_gt_comparison: - wallet_gt = TestUtils.create_wallet_ground_truth(TestUtils.NETWORK_TYPE, wallet.get_seed(), self.start_height, self.restore_height) + wallet_gt: MoneroWalletFull = TestUtils.create_wallet_ground_truth(TestUtils.NETWORK_TYPE, wallet.get_seed(), self.start_height, self.restore_height) WalletEqualityUtils.test_wallet_full_equality_on_chain(wallet_gt, wallet) # if testing post-sync notifications, wait for a block to be added to the chain diff --git a/tests/utils/sync_with_pool_submit_tester.py b/tests/utils/sync_with_pool_submit_tester.py index 858f9a0..c41c96b 100644 --- a/tests/utils/sync_with_pool_submit_tester.py +++ b/tests/utils/sync_with_pool_submit_tester.py @@ -78,7 +78,7 @@ def run_failing_core_code(self, config_no_relay: MoneroTxConfig) -> None: assert result2.is_good self.wallet.sync() # wallet is aware of tx2 - fetched = self.wallet.get_tx(tx2.hash) + fetched: MoneroTxWallet | None = self.wallet.get_tx(tx2.hash) assert fetched is not None and fetched.is_failed is False, "Submitted tx should not be null or failed" finally: self.daemon.flush_tx_pool(tx2.hash) @@ -117,7 +117,7 @@ def flush_tx(self, tx_hash: str) -> None: TestUtils.WALLET_TX_TRACKER.wait_for_txs_to_clear_wallets([self.wallet]) # wallet should see failed state - fetched = self.wallet.get_tx(tx_hash) + fetched: MoneroTxWallet | None = self.wallet.get_tx(tx_hash) if fetched is not None: assert fetched.is_failed, "Flushed tx should be failed" assert not fetched.in_tx_pool, "Flushed tx should not be in pool" @@ -158,7 +158,7 @@ def test(self) -> None: assert tx.hash is not None # create tx using same config which is double spend - tx_double_spend = self.wallet.create_tx(config_no_relay) + tx_double_spend: MoneroTxWallet = self.wallet.create_tx(config_no_relay) assert tx_double_spend.hash is not None assert tx_double_spend.full_hex is not None diff --git a/tests/utils/test_utils.py b/tests/utils/test_utils.py index 1fc4ae5..ab2bb16 100644 --- a/tests/utils/test_utils.py +++ b/tests/utils/test_utils.py @@ -177,7 +177,7 @@ def load_config(cls) -> None: assert parser.has_section("wallet") # parse general config - nettype_str = parser.get('general', 'network_type') + nettype_str: str = parser.get('general', 'network_type') cls.TEST_NON_RELAYS = parser.getboolean('general', 'test_non_relays') cls.TEST_RELAYS = parser.getboolean('general', 'test_relays') cls.TEST_NOTIFICATIONS = parser.getboolean('general', 'test_notifications') @@ -223,7 +223,7 @@ def load_config(cls) -> None: cls.WALLET_RPC_URI = cls.WALLET_RPC_DOMAIN + ":" + str(cls.WALLET_RPC_PORT_START) cls.WALLET_RPC_ZMQ_URI = "tcp:#" + cls.WALLET_RPC_ZMQ_DOMAIN + ":" + str(cls.WALLET_RPC_ZMQ_PORT_START) cls.SYNC_PERIOD_IN_MS = parser.getint('wallet', 'sync_period_in_ms') - in_container = getenv("IN_CONTAINER", "true") + in_container: str = getenv("IN_CONTAINER", "true") cls.IN_CONTAINER = in_container.lower() == "true" or in_container == "1" # parse mining wallet config @@ -326,7 +326,7 @@ def get_wallet_keys_config(cls) -> MoneroWalletConfig: :returns MoneroWalletConfig: new test wallet keys configuration. """ - config = MoneroWalletConfig() + config: MoneroWalletConfig = MoneroWalletConfig() config.network_type = cls.NETWORK_TYPE config.seed = cls.SEED return config @@ -338,7 +338,7 @@ def get_wallet_keys(cls) -> MoneroWalletKeys: :returns MoneroWalletKeys: keys-only test wallet. """ if cls._WALLET_KEYS is None: - config = cls.get_wallet_keys_config() + config: MoneroWalletConfig = cls.get_wallet_keys_config() cls._WALLET_KEYS = MoneroWalletKeys.create_wallet_from_seed(config) return cls._WALLET_KEYS @@ -350,7 +350,7 @@ def get_wallet_full_config(cls, daemon_connection: MoneroRpcConnection) -> Moner :param MoneroRpcConnection daemon_connection: rpc daemon connection. :returns MoneroWalletConfig: full wallet test configuration. """ - config = MoneroWalletConfig() + config: MoneroWalletConfig = MoneroWalletConfig() config.path = cls.WALLET_FULL_PATH config.password = cls.WALLET_PASSWORD config.network_type = cls.NETWORK_TYPE @@ -372,10 +372,10 @@ def get_wallet_full(cls) -> MoneroWalletFull: # create wallet from seed if it doesn't exist if not MoneroWalletFull.wallet_exists(cls.WALLET_FULL_PATH): # create wallet with connection - daemon_connection = MoneroRpcConnection( + daemon_connection: MoneroRpcConnection = MoneroRpcConnection( cls.DAEMON_RPC_URI, cls.DAEMON_RPC_USERNAME, cls.DAEMON_RPC_PASSWORD ) - config = cls.get_wallet_full_config(daemon_connection) + config: MoneroWalletConfig = cls.get_wallet_full_config(daemon_connection) logger.debug("Creating full wallet...") cls._WALLET_FULL = MoneroWalletFull.create_wallet(config) logger.debug(f"Created full wallet at path '{cls.WALLET_FULL_PATH}'") @@ -395,7 +395,7 @@ def get_wallet_full(cls) -> MoneroWalletFull: # sync and save wallet if cls._WALLET_FULL.is_connected_to_daemon(): logger.debug("Wallet full is connected to daemon") - listener = WalletSyncPrinter(0.25) + listener: WalletSyncPrinter = WalletSyncPrinter(0.25) cls._WALLET_FULL.sync(listener) logger.debug("Synced full wallet") cls._WALLET_FULL.save() @@ -416,12 +416,12 @@ def get_mining_wallet_config(cls) -> MoneroWalletConfig: :returns MoneroWalletConfig: mining wallet configuration. """ - connection = MoneroRpcConnection( + connection: MoneroRpcConnection = MoneroRpcConnection( cls.DAEMON_RPC_URI, cls.DAEMON_RPC_USERNAME, cls.DAEMON_RPC_PASSWORD ) - config = cls.get_wallet_full_config(connection) + config: MoneroWalletConfig = cls.get_wallet_full_config(connection) config.path = cls.MINING_WALLET_FULL_PATH config.password = cls.MINING_WALLET_PASSWORD config.seed = cls.MINING_SEED @@ -443,7 +443,7 @@ def get_mining_wallet(cls) -> MoneroWalletFull: if not MoneroWalletFull.wallet_exists(cls.MINING_WALLET_FULL_PATH): logger.debug("Creating mining wallet...") - wallet = MoneroWalletFull.create_wallet(cls.get_mining_wallet_config()) + wallet: MoneroWalletFull = MoneroWalletFull.create_wallet(cls.get_mining_wallet_config()) logger.debug("Mining wallet created") else: logger.debug("Opening mining wallet...") @@ -481,7 +481,7 @@ def get_wallet_rpc(cls) -> MoneroWalletRpc: if cls._WALLET_RPC is None: # construct wallet rpc instance with daemon connection - rpc = cls.get_wallet_rpc_connection() + rpc: MoneroRpcConnection = cls.get_wallet_rpc_connection() cls._WALLET_RPC = MoneroWalletRpc(rpc) # attempt to open test wallet @@ -516,7 +516,7 @@ def get_wallet_rpc(cls) -> MoneroWalletRpc: def open_wallet_rpc(cls, c: Optional[MoneroWalletConfig]) -> MoneroWalletRpc: """Open a rpc wallet. - :params MoneroWalletConfig | None c: rpc wallet configuration. + :param MoneroWalletConfig | None c: rpc wallet configuration. :returns MoneroWalletRpc: opened rpc wallet. """ return cls.RPC_WALLET_MANAGER.open_wallet(c, cls.IN_CONTAINER) @@ -580,9 +580,9 @@ def create_wallet_ground_truth( """ # create ground truth wallet - daemon_connection = MoneroRpcConnection(cls.DAEMON_RPC_URI, cls.DAEMON_RPC_USERNAME, cls.DAEMON_RPC_PASSWORD) - path = cls.TEST_WALLETS_DIR + "/gt_wallet_" + GenUtils.current_timestamp_str() - config = MoneroWalletConfig() + daemon_connection: MoneroRpcConnection = MoneroRpcConnection(cls.DAEMON_RPC_URI, cls.DAEMON_RPC_USERNAME, cls.DAEMON_RPC_PASSWORD) + path: str = cls.TEST_WALLETS_DIR + "/gt_wallet_" + GenUtils.current_timestamp_str() + config: MoneroWalletConfig = MoneroWalletConfig() config.path = path config.password = cls.WALLET_PASSWORD config.network_type = network_type @@ -594,7 +594,7 @@ def create_wallet_ground_truth( if start_height is None: start_height = 0 if restore_height is None else restore_height - gt_wallet = MoneroWalletFull.create_wallet(config) + gt_wallet: MoneroWalletFull = MoneroWalletFull.create_wallet(config) assert restore_height == gt_wallet.get_restore_height() gt_wallet.sync(start_height, WalletSyncPrinter(0.25)) gt_wallet.start_syncing(cls.SYNC_PERIOD_IN_MS) @@ -606,7 +606,7 @@ def create_wallet_ground_truth( @classmethod def clear_wallet_full_txs_pool(cls) -> None: """Clear full wallet txs pool and save.""" - wallet_full = cls.get_wallet_full() + wallet_full: MoneroWalletFull = cls.get_wallet_full() cls.WALLET_TX_TRACKER.wait_for_txs_to_clear_pool(wallet_full) wallet_full.close(True) diff --git a/tests/utils/to_multiple_tx_sender.py b/tests/utils/to_multiple_tx_sender.py index 1738832..03424e0 100644 --- a/tests/utils/to_multiple_tx_sender.py +++ b/tests/utils/to_multiple_tx_sender.py @@ -37,12 +37,18 @@ class ToMultipleTxSender: @property def total_subaddresses(self) -> int: - """Total num of subaddresses to send txs to.""" + """Total num of subaddresses to send txs to. + + :returns int: total number of subaddresses to send txs to. + """ return self._num_accounts * self._num_subaddresses_per_account @property def min_account_amount(self) -> int: - """Minimum account unlocked balance needed.""" + """Minimum account unlocked balance needed. + + :returns int: minimum account unlocked balance needed to fulfill the send configuration. + """ fee: int = TxWalletUtils.MAX_FEE # 75000000000 # compute the minimum account unlocked balance needed in order to fulfill the config if self._send_amount_per_subaddress is not None: diff --git a/tests/utils/transfer_utils.py b/tests/utils/transfer_utils.py index 55d4243..b71b8f4 100644 --- a/tests/utils/transfer_utils.py +++ b/tests/utils/transfer_utils.py @@ -36,6 +36,7 @@ def test_incoming_transfer(cls, transfer: Optional[MoneroIncomingTransfer]) -> N :param MoneroIncomingTransfer | None transfer: transfer to test. """ assert transfer is not None + logger.debug(f"Testing incoming transfer: {transfer.serialize()}") assert transfer.is_incoming() is True assert transfer.is_outgoing() is False assert transfer.address is not None @@ -52,6 +53,7 @@ def test_outgoing_transfer(cls, transfer: Optional[MoneroOutgoingTransfer], ctx: :param TxContext ctx: test context. """ assert transfer is not None + logger.debug(f"Testing outgoing transfer: {transfer.serialize()}, with context: {ctx.serialize()}") assert transfer.is_incoming() is False assert transfer.is_outgoing() is True if ctx.is_send_response is not True: @@ -80,9 +82,9 @@ def test_transfer(cls, transfer: Optional[MoneroTransfer], context: Optional[TxC """Test monero transfer. :param MoneroTransfer | None transfer: transfer to test. - :param TxContext | None: test context. + :param TxContext | None context: test context. """ - ctx = context if context is not None else TxContext() + ctx: TxContext = TxContext(context) assert transfer is not None GenUtils.test_unsigned_big_integer(transfer.amount) if ctx.is_sweep_output_response is not True: diff --git a/tests/utils/tx_spammer.py b/tests/utils/tx_spammer.py index 6169804..c65cf01 100644 --- a/tests/utils/tx_spammer.py +++ b/tests/utils/tx_spammer.py @@ -27,14 +27,13 @@ def __init__(self, network_type: MoneroNetworkType) -> None: def create_spam_wallets(self, n: int = 10) -> list[MoneroWallet]: """Create random wallet used as spam destinations. - :param MoneroNetworkType network_type: Network type. :param int n: number of wallets to create. - :returns list[MoneroWalletKeys]: random wallets created. + :returns list[MoneroWallet]: random wallets created. """ assert n >= 0, "n must be >= 0" wallets: list[MoneroWallet] = [] # setup basic wallet config - config = MoneroWalletConfig() + config: MoneroWalletConfig = MoneroWalletConfig() config.network_type = self._network_type # create n random wallets for i in range(n): diff --git a/tests/utils/tx_tester.py b/tests/utils/tx_tester.py index 4ddfc56..d2e2b27 100644 --- a/tests/utils/tx_tester.py +++ b/tests/utils/tx_tester.py @@ -1,5 +1,5 @@ -from monero import MoneroTx +from monero import MoneroTx, MoneroBlock from .context import TestContext from .gen_utils import GenUtils @@ -73,7 +73,7 @@ def _test_confirmed(self) -> None: # test confirmed if self.tx.is_confirmed is True: - block = self.tx.block + block: MoneroBlock | None = self.tx.block assert block is not None assert self.tx in block.txs assert block.height is not None @@ -120,6 +120,7 @@ def _test_in_tx_pool(self) -> None: assert self.tx.last_relayed_timestamp is None def _test_failed(self) -> None: + """Test transaction failure details.""" # test failed # TODO what else to test associated with failed if self.tx.is_failed: diff --git a/tests/utils/tx_utils.py b/tests/utils/tx_utils.py index 13ab1e6..2a8c9cc 100644 --- a/tests/utils/tx_utils.py +++ b/tests/utils/tx_utils.py @@ -2,7 +2,7 @@ from abc import ABC from typing import Optional -from monero import MoneroTx +from monero import MoneroTx, MoneroBlock from .context import TestContext from .assert_utils import AssertUtils @@ -25,11 +25,11 @@ def test_tx_copy(cls, tx: Optional[MoneroTx], context: Optional[TestContext]) -> """ # copy tx and assert deep equality assert tx is not None - copy = tx.copy() + copy: MoneroTx = tx.copy() assert isinstance(copy, MoneroTx) assert copy.block is None if tx.block is not None: - block_copy = tx.block.copy() + block_copy: MoneroBlock = tx.block.copy() block_copy.txs = [copy] AssertUtils.assert_equals(tx, copy) @@ -44,7 +44,7 @@ def test_tx_copy(cls, tx: Optional[MoneroTx], context: Optional[TestContext]) -> assert tx.outputs[i].amount == output.amount # test copied tx - ctx = TestContext(context) + ctx: TestContext = TestContext(context) ctx.do_not_test_copy = True # to prevent infinite recursion if tx.block is not None: block_copy = tx.block.copy() @@ -54,7 +54,7 @@ def test_tx_copy(cls, tx: Optional[MoneroTx], context: Optional[TestContext]) -> cls.test_tx(copy, ctx) # test merging with copy - merged = copy + merged: MoneroTx = copy merged.merge(copy.copy()) assert str(tx) == str(merged) @@ -66,6 +66,7 @@ def test_tx(cls, tx: MoneroTx | None, ctx: TestContext) -> None: :param TestContext ctx: test context. """ assert tx is not None, "No tx provided" + logger.debug(f"Testing tx: {tx.serialize()}. Context: {ctx.serialize()}") tester: TxTester = TxTester(tx, ctx) tester.run() diff --git a/tests/utils/tx_wallet_tester.py b/tests/utils/tx_wallet_tester.py index b01f821..d25f81a 100644 --- a/tests/utils/tx_wallet_tester.py +++ b/tests/utils/tx_wallet_tester.py @@ -21,6 +21,11 @@ class TxWalletTester: ctx: TxContext def __init__(self, tx: MoneroTxWallet, context: Optional[TxContext]) -> None: + """Initialize a new tx wallet tester. + + :param MoneroTxWallet tx: transaction to test. + :param TxContext | None context: test context. + """ self.tx = tx # validate / sanitize inputs self.ctx = TxContext(context) @@ -30,7 +35,10 @@ def __init__(self, tx: MoneroTxWallet, context: Optional[TxContext]) -> None: assert self.ctx.is_send_response is None, "if either send_request or is_send_response is defined, they must both be defined" assert self.ctx.config is None, "if either send_request or is_send_response is defined, they must both be defined" + logger.debug(f"Initialized TxWalletTester with tx: {tx.serialize()}, and context: {self.ctx.serialize()}") + def _test_common(self) -> None: + """Test common tx field types.""" # test common field types assert self.tx.hash is not None assert self.tx.is_confirmed is not None @@ -53,6 +61,7 @@ def _test_common(self) -> None: assert self.tx.received_timestamp is None # TODO monero-wallet-rpc: return received timestamp (asked to file issue if wanted) def _test_send(self) -> None: + """Test send tx fields.""" # test send tx if self.ctx.is_send_response is True: assert self.tx.weight is not None @@ -65,6 +74,7 @@ def _test_send(self) -> None: assert len(self.tx.inputs) == 0 def _test_pool_status(self) -> None: + """Test confirmed and tx pool status fields.""" # test confirmed if self.tx.is_confirmed: assert self.tx.block is not None @@ -101,6 +111,7 @@ def _test_pool_status(self) -> None: assert self.tx.last_relayed_timestamp is None def _test_status(self) -> None: + """Test miner, failure, and relay status fields.""" # test miner tx if self.tx.is_miner_tx: assert self.tx.fee is not None @@ -134,6 +145,7 @@ def _test_status(self) -> None: assert (not self.tx.is_relayed) is True def _test_outgoing_transfer(self) -> None: + """Test the tx's outgoing transfer, if any, against the test context.""" # test outgoing transfer per configuration if self.ctx.has_outgoing_transfer is False: assert self.tx.outgoing_transfer is None @@ -158,6 +170,7 @@ def _test_outgoing_transfer(self) -> None: assert self.tx.key is None def _test_incoming_transfers(self) -> None: + """Test the tx's incoming transfers, if any.""" # test incoming transfers if len(self.tx.incoming_transfers) > 0: assert self.tx.is_incoming is True @@ -173,7 +186,7 @@ def _test_incoming_transfers(self) -> None: assert transfer.amount is not None transfer_sum += transfer.amount if self.ctx.wallet is not None: - addr = self.ctx.wallet.get_address(transfer.account_index, transfer.subaddress_index) + addr: str = self.ctx.wallet.get_address(transfer.account_index, transfer.subaddress_index) assert transfer.address == addr # TODO special case: transfer amount of 0 @@ -185,6 +198,10 @@ def _test_incoming_transfers(self) -> None: assert len(self.tx.incoming_transfers) == 0 def _test_relay(self, config: MoneroTxConfig) -> None: + """Test tx relay fields against the send configuration. + + :param MoneroTxConfig config: send configuration the tx was created with. + """ if config.relay is True: # test relayed txs assert self.tx.in_tx_pool is True @@ -202,6 +219,7 @@ def _test_relay(self, config: MoneroTxConfig) -> None: assert self.tx.is_double_spend_seen is None def _test_send_response(self) -> None: + """Test tx fields specific to a send response.""" # test tx set assert self.tx.tx_set is not None found: bool = False @@ -259,6 +277,7 @@ def _test_send_response(self) -> None: self._test_relay(config) def _test_inputs_and_outputs(self) -> None: + """Test the tx's wallet inputs and outputs.""" # test inputs if self.tx.is_outgoing is True and self.ctx.is_send_response is True: assert len(self.tx.inputs) > 0 diff --git a/tests/utils/tx_wallet_utils.py b/tests/utils/tx_wallet_utils.py index d2aa04f..701b05c 100644 --- a/tests/utils/tx_wallet_utils.py +++ b/tests/utils/tx_wallet_utils.py @@ -5,7 +5,7 @@ from monero import ( - MoneroTxWallet, MoneroUtils, + MoneroTxWallet, MoneroUtils, MoneroBlock, MoneroTxSet, MoneroTxQuery, MoneroNetworkType, MoneroCheckTx, MoneroCheckReserve @@ -219,7 +219,7 @@ def set_block_copy(cls, copy: MoneroTxWallet, tx: MoneroTxWallet) -> None: # copy block assert tx.block is not None - block = tx.block.copy() + block: MoneroBlock = tx.block.copy() # set copy tx in block copy block.txs = [copy] @@ -237,8 +237,8 @@ def txs_mergeable(cls, tx1: MoneroTxWallet, tx2: MoneroTxWallet) -> bool: """ try: # copy txs - copy1 = tx1.copy() - copy2 = tx2.copy() + copy1: MoneroTxWallet = tx1.copy() + copy2: MoneroTxWallet = tx2.copy() # set block copies cls.set_block_copy(copy1, tx1) cls.set_block_copy(copy2, tx2) diff --git a/tests/utils/txs_structure_tester.py b/tests/utils/txs_structure_tester.py index 15a7224..568c40c 100644 --- a/tests/utils/txs_structure_tester.py +++ b/tests/utils/txs_structure_tester.py @@ -28,17 +28,26 @@ class TxsStructureTester: @property def num_txs(self) -> int: - """Number of transactions to test.""" + """Number of transactions to test. + + :returns int: number of transactions to test. + """ return len(self.txs) @property def num_unconfirmed_txs(self) -> int: - """Number of unconfirmed txs to test.""" + """Number of unconfirmed txs to test. + + :returns int: number of unconfirmed txs to test. + """ return len(self.unconfirmed_txs) @property def num_tx_hashes(self) -> int: - """Number of tx hashes set in tx query.""" + """Number of tx hashes set in tx query. + + :returns int: number of tx hashes set in tx query. + """ return len(self.query.hashes) def __init__(self, txs: list[MoneroTxWallet], query: Optional[MoneroTxQuery], regtest: bool) -> None: @@ -66,15 +75,21 @@ def __init__(self, txs: list[MoneroTxWallet], query: Optional[MoneroTxQuery], re self.blocks.append(tx.block) def _test_block_txs_order(self, tx: MoneroTx, block: MoneroBlock, index: int) -> None: + """Test that `tx` is at the expected position within `block`'s tx order. + + :param MoneroTx tx: transaction to test. + :param MoneroBlock block: block `tx` belongs to. + :param int index: expected position of `tx` in `self.txs`. + """ assert tx.block == block if self.num_tx_hashes == 0: - other = self.txs[index] + other: MoneroTxWallet = self.txs[index] if not self.regtest: assert other.hash == tx.hash, "Txs in block are not in order" # verify tx order is self-consistent with blocks unless txs manually re-ordered by querying by hash assert other == tx else: - # TODO regtest wallet2 has inconsinstent txs order betwenn + # TODO regtest wallet2 has inconsinstent txs order between calls assert other in block.txs, "Tx not found in block" def _test_txs_order(self) -> None: @@ -95,7 +110,7 @@ def _test_txs_order(self) -> None: prev_block_height = block.height elif self.num_tx_hashes == 0: assert block.height is not None - msg = f"Blocks are not in order of heights: {prev_block_height} vs {block.height}" + msg: str = f"Blocks are not in order of heights: {prev_block_height} vs {block.height}" assert block.height > prev_block_height, msg for tx in block.txs: diff --git a/tests/utils/view_only_and_offline_wallet_tester.py b/tests/utils/view_only_and_offline_wallet_tester.py index 21d3316..df77532 100644 --- a/tests/utils/view_only_and_offline_wallet_tester.py +++ b/tests/utils/view_only_and_offline_wallet_tester.py @@ -98,7 +98,7 @@ def _test_offline_wallet(self) -> None: query: MoneroTxQuery = MoneroTxQuery() query.in_tx_pool = False - txs = self._offline_wallet.get_txs(query) + txs: list[MoneroTxWallet] = self._offline_wallet.get_txs(query) assert len(txs) == 0 #endregion @@ -142,7 +142,7 @@ def test(self) -> None: assert signed_tx_set.signed_tx_hex is not None assert len(signed_tx_set.signed_tx_hex) > 0 assert len(signed_tx_set.txs) == 1 - tx_from_set = signed_tx_set.txs[0] + tx_from_set: MoneroTxWallet = signed_tx_set.txs[0] assert tx_from_set.hash is not None assert len(tx_from_set.hash) > 0 diff --git a/tests/utils/wallet_equality_utils.py b/tests/utils/wallet_equality_utils.py index 1adc12c..16e596f 100644 --- a/tests/utils/wallet_equality_utils.py +++ b/tests/utils/wallet_equality_utils.py @@ -54,11 +54,11 @@ def test_wallet_equality_on_chain(cls, w1: MoneroWallet, w2: MoneroWallet) -> No cls.test_accounts_equal_on_chain(w1.get_accounts(True), w2.get_accounts(True)) assert w1.get_balance() == w2.get_balance() assert w1.get_unlocked_balance() == w2.get_unlocked_balance() - transfer_query = MoneroTransferQuery() + transfer_query: MoneroTransferQuery = MoneroTransferQuery() transfer_query.tx_query = MoneroTxQuery() transfer_query.tx_query.is_confirmed = True cls.test_transfers_equal_on_chain(w1.get_transfers(transfer_query), w2.get_transfers(transfer_query)) - output_query = MoneroOutputQuery() + output_query: MoneroOutputQuery = MoneroOutputQuery() output_query.set_tx_query(MoneroTxQuery(), True) assert output_query.tx_query is not None output_query.tx_query.is_confirmed = True @@ -83,6 +83,12 @@ def test_wallet_full_equality_on_chain(cls, wallet1: MoneroWalletFull, wallet2: @classmethod def test_account(cls, accounts: list[MoneroAccount], j: int, size: int) -> None: + """Test that accounts past a common index are unused (zero balance, no used subaddresses). + + :param list[MoneroAccount] accounts: accounts to test. + :param int j: index to start testing from. + :param int size: number of accounts, i.e. the index to test up to (exclusive). + """ while j < size: assert 0 == accounts[j].balance assert len(accounts[j].subaddresses) >= 1 @@ -94,8 +100,8 @@ def test_account(cls, accounts: list[MoneroAccount], j: int, size: int) -> None: def test_accounts_equal_on_chain(cls, accounts1: list[MoneroAccount], accounts2: list[MoneroAccount]) -> None: """Test account lists equality based on on-chain data. - :param list[MoneroAccount] account1: first account list to compare on-chain data. - :param list[MoneroAccount] account2: second account list to compare on-chain data. + :param list[MoneroAccount] accounts1: first account list to compare on-chain data. + :param list[MoneroAccount] accounts2: second account list to compare on-chain data. """ accounts1_size: int = len(accounts1) accounts2_size: int = len(accounts2) @@ -177,6 +183,11 @@ def test_subaddress_equal_on_chain(cls, subaddress1: MoneroSubaddress, subaddres @classmethod def test_txs_wallet_equality(cls, txs1: list[MoneroTxWallet], txs2: list[MoneroTxWallet]) -> None: + """Test that every tx in `txs1` matches its counterpart (by hash) in `txs2`. + + :param list[MoneroTxWallet] txs1: first tx list to compare. + :param list[MoneroTxWallet] txs2: second tx list to compare. + """ for tx1 in txs1: found: bool = False for tx2 in txs2: @@ -267,15 +278,20 @@ def transfer_cached_info(cls, src: MoneroTxWallet, tgt: MoneroTxWallet) -> None: @classmethod def compare_transfers(cls, txs_transfers_1: dict[str, list[MoneroTransfer]], txs_transfers_2: dict[str, list[MoneroTransfer]]) -> None: + """Compare transfers collected per tx hash for equality. + + :param dict[str, list[MoneroTransfer]] txs_transfers_1: first collection of transfers, keyed by tx hash. + :param dict[str, list[MoneroTransfer]] txs_transfers_2: second collection of transfers, keyed by tx hash. + """ # compare collected transfers per tx for equality for tx_hash in txs_transfers_1: - tx_transfers1 = txs_transfers_1[tx_hash] - tx_transfers2 = txs_transfers_2[tx_hash] + tx_transfers1: list[MoneroTransfer] = txs_transfers_1[tx_hash] + tx_transfers2: list[MoneroTransfer] = txs_transfers_2[tx_hash] assert len(tx_transfers1) == len(tx_transfers2) # normalize and compare transfers for i, transfer1 in enumerate(tx_transfers1): - transfer2 = tx_transfers2[i] + transfer2: MoneroTransfer = tx_transfers2[i] # normalize outgoing transfers if isinstance(transfer1, MoneroOutgoingTransfer): @@ -319,7 +335,7 @@ def test_transfers_equal_on_chain(cls, transfers1: list[MoneroTransfer], transfe last_tx2: Optional[MoneroTxWallet] = None for i, transfer1 in enumerate(transfers1): - transfer2 = transfers2[i] + transfer2: MoneroTransfer = transfers2[i] # transfers must have same height even if they don't belong to same tx # (because tx ordering within blocks is not currently provided by wallet2) @@ -329,7 +345,7 @@ def test_transfers_equal_on_chain(cls, transfers1: list[MoneroTransfer], transfe if last_height is None: last_height = transfer1.tx.get_height() else: - transfer_height = transfer1.tx.get_height() + transfer_height: int | None = transfer1.tx.get_height() assert transfer_height is not None assert last_height <= transfer_height @@ -348,7 +364,7 @@ def test_transfers_equal_on_chain(cls, transfers1: list[MoneroTransfer], transfe last_tx2 = transfer2.tx # collect tx1 transfer - tx_transfers1 = txs_transfers_1.get(transfer1.tx.hash) + tx_transfers1: list[MoneroTransfer] | None = txs_transfers_1.get(transfer1.tx.hash) if tx_transfers1 is None: tx_transfers1 = [] txs_transfers_1[transfer1.tx.hash] = tx_transfers1 @@ -356,7 +372,7 @@ def test_transfers_equal_on_chain(cls, transfers1: list[MoneroTransfer], transfe tx_transfers1.append(transfer1) # collect tx2 transfer - tx_transfers2 = txs_transfers_2.get(transfer2.tx.hash) + tx_transfers2: list[MoneroTransfer] | None = txs_transfers_2.get(transfer2.tx.hash) if tx_transfers2 is None: tx_transfers2 = [] txs_transfers_2[transfer2.tx.hash] = tx_transfers2 @@ -367,10 +383,15 @@ def test_transfers_equal_on_chain(cls, transfers1: list[MoneroTransfer], transfe @classmethod def compare_outputs(cls, txs_outputs1: dict[str, list[MoneroOutputWallet]], txs_outputs2: dict[str, list[MoneroOutputWallet]]) -> None: + """Compare outputs collected per tx hash for equality. + + :param dict[str, list[MoneroOutputWallet]] txs_outputs1: first collection of outputs, keyed by tx hash. + :param dict[str, list[MoneroOutputWallet]] txs_outputs2: second collection of outputs, keyed by tx hash. + """ # compare collected outputs per tx for equality for tx_hash in txs_outputs2: - tx_outputs1 = txs_outputs1[tx_hash] - tx_outputs2 = txs_outputs2[tx_hash] + tx_outputs1: list[MoneroOutputWallet] = txs_outputs1[tx_hash] + tx_outputs2: list[MoneroOutputWallet] = txs_outputs2[tx_hash] assert len(tx_outputs1) == len(tx_outputs2) # normalize and compare outputs @@ -405,7 +426,7 @@ def test_output_wallets_equal_on_chain(cls, outputs1: list[MoneroOutputWallet], if last_height is None: last_height = output1.tx.get_height() else: - output_height = output1.tx.get_height() + output_height: int | None = output1.tx.get_height() assert output_height is not None assert last_height <= output_height diff --git a/tests/utils/wallet_error_utils.py b/tests/utils/wallet_error_utils.py index cd38b2e..f60a440 100644 --- a/tests/utils/wallet_error_utils.py +++ b/tests/utils/wallet_error_utils.py @@ -16,6 +16,8 @@ def test_invalid_address_error(cls, ex: Exception, address: str | None = None) - """Test exception is invalid address. :param Exception ex: exception to test. + :param str | None address: the invalid address expected to appear in the error message, + if any (default `None`). """ msg: str = str(ex) err_msg: str = "Invalid address" @@ -88,11 +90,19 @@ def test_wallet_is_closed_error(cls, error: Exception) -> None: @classmethod def test_wallet_is_not_connected_error(cls, error: Exception) -> None: + """Test exception is wallet-not-connected-to-daemon error. + + :param Exception error: error to test. + """ err_msg: str = str(error) # TODO normalize Network error message? assert err_msg == "Wallet is not connected to daemon" or err_msg == RpcConnectionUtils.NETWORK_ERROR_MSG, err_msg @classmethod def test_deprecated_payment_id_error(cls, error: Exception) -> None: + """Test exception is deprecated-standalone-payment-id error. + + :param Exception error: error to test. + """ err_msg: str = str(error) assert err_msg == "Standalone payment id deprecated, use integrated address instead", err_msg diff --git a/tests/utils/wallet_notification_collector.py b/tests/utils/wallet_notification_collector.py index 0e8ce7e..ac8a262 100644 --- a/tests/utils/wallet_notification_collector.py +++ b/tests/utils/wallet_notification_collector.py @@ -38,6 +38,10 @@ def __init__(self) -> None: @override def on_new_block(self, height: int) -> None: + """Invoked when the wallet detects a new block. + + :param int height: height of the new block. + """ try: assert self.listening num_block_notifications: int = len(self.block_notifications) @@ -56,6 +60,11 @@ def on_new_block(self, height: int) -> None: @override def on_balances_changed(self, new_balance: int, new_unlocked_balance: int) -> None: + """Invoked when the wallet's balance changes. + + :param int new_balance: the wallet's new balance. + :param int new_unlocked_balance: the wallet's new unlocked balance. + """ try: assert self.listening num_balance_notifications: int = len(self.balance_notifications) @@ -74,6 +83,10 @@ def on_balances_changed(self, new_balance: int, new_unlocked_balance: int) -> No @override def on_output_received(self, output: MoneroOutputWallet) -> None: + """Invoked when the wallet receives a new output. + + :param MoneroOutputWallet output: the output received. + """ try: assert self.listening # collect received output @@ -85,6 +98,10 @@ def on_output_received(self, output: MoneroOutputWallet) -> None: @override def on_output_spent(self, output: MoneroOutputWallet) -> None: + """Invoked when one of the wallet's outputs is spent. + + :param MoneroOutputWallet output: the output spent. + """ try: assert self.listening # collect spent output diff --git a/tests/utils/wallet_send_utils.py b/tests/utils/wallet_send_utils.py index a523c35..11abf01 100644 --- a/tests/utils/wallet_send_utils.py +++ b/tests/utils/wallet_send_utils.py @@ -24,11 +24,11 @@ def test_send_to_single(cls, wallet: MoneroWallet, can_split: bool, relay: Optio :param bool | None relay: Relay created transaction(s). :param str | None payment_id: Transaction payment id. """ - config = MoneroTxConfig() + config: MoneroTxConfig = MoneroTxConfig() config.can_split = can_split config.relay = relay config.payment_id = payment_id - sender = SingleTxSender(wallet, config) + sender: SingleTxSender = SingleTxSender(wallet, config) sender.send() # Convenience method for sending funds from multiple sources @@ -53,6 +53,17 @@ def test_send_to_multiple( send_amount_per_subaddress: Optional[int] = None, subtract_fee_from_destinations: bool = False ) -> None: + """Test send multiple txs from wallet to multiple accounts and subaddresses. + + :param MoneroWallet wallet: test wallet to send txs from. + :param int num_accounts: number of accounts to send to. + :param int num_subaddresses_per_account: number of subaddresses per account to send to. + :param bool can_split: can split wallet txs. + :param int | None send_amount_per_subaddress: amount to send to each subaddress, or + `None` to compute it from the account's unlocked balance (default `None`). + :param bool subtract_fee_from_destinations: subtract the tx fee from destination amounts + instead of the sender's balance (default `False`). + """ sender: ToMultipleTxSender = ToMultipleTxSender( wallet, num_accounts, num_subaddresses_per_account, can_split, send_amount_per_subaddress, subtract_fee_from_destinations) @@ -70,10 +81,22 @@ def test_sweep_wallet(cls, wallet: MoneroWallet, sweep_each_subaddress: Optional @classmethod def test_send_and_update_txs(cls, daemon: MoneroDaemon, wallet: MoneroWallet, config: MoneroTxConfig) -> None: + """Test sending a tx and observing its status update as it confirms. + + :param MoneroDaemon daemon: daemon to test against. + :param MoneroWallet wallet: test wallet to send the tx from. + :param MoneroTxConfig config: tx configuration to send with. + """ tester: SendAndUpdateTxsTester = SendAndUpdateTxsTester(daemon, wallet, config) tester.test() @classmethod def test_sync_with_pool_submit(cls, daemon: MoneroDaemon, wallet: MoneroWallet, config: MoneroTxConfig) -> None: + """Test that syncing with the pool detects a tx submitted directly to the daemon. + + :param MoneroDaemon daemon: daemon to test against. + :param MoneroWallet wallet: test wallet to sync and submit the tx with. + :param MoneroTxConfig config: tx configuration to send with. + """ tester: SyncWithPoolSubmitTester = SyncWithPoolSubmitTester(daemon, wallet, config) tester.test() diff --git a/tests/utils/wallet_sync_printer.py b/tests/utils/wallet_sync_printer.py index 2ac4c9e..d141d3e 100644 --- a/tests/utils/wallet_sync_printer.py +++ b/tests/utils/wallet_sync_printer.py @@ -34,6 +34,6 @@ def on_sync_progress(self, height: int, start_height: int, end_height: int, perc :param str message: sync progress message. """ if percent_done == 1.0 or percent_done >= self.next_increment: - msg = f"on_sync_progress({height}, {start_height}, {end_height}, {percent_done}, {message})" + msg: str = f"on_sync_progress({height}, {start_height}, {end_height}, {percent_done}, {message})" logger.info(msg) self.next_increment += self.sync_resolution diff --git a/tests/utils/wallet_sync_tester.py b/tests/utils/wallet_sync_tester.py index 9420298..6477a3f 100644 --- a/tests/utils/wallet_sync_tester.py +++ b/tests/utils/wallet_sync_tester.py @@ -54,6 +54,10 @@ def __init__(self, wallet: MoneroWalletFull, start_height: int, end_height: int) @override def on_new_block(self, height: int) -> None: + """Invoked when the wallet detects a new block. + + :param int height: height of the new block. + """ if self.is_done: assert self in self.wallet.get_listeners(), "Listener has completed and is not registered so should not be called again" self.on_new_block_after_done = True @@ -65,6 +69,11 @@ def on_new_block(self, height: int) -> None: @override def on_balances_changed(self, new_balance: int, new_unlocked_balance: int) -> None: + """Invoked when the wallet's balance changes. + + :param int new_balance: the wallet's new balance. + :param int new_unlocked_balance: the wallet's new unlocked balance. + """ if self.prev_balance is not None: assert new_balance != self.prev_balance or new_unlocked_balance != self.prev_unlocked_balance self.prev_balance = new_balance @@ -125,14 +134,26 @@ def test_output(self, output: MoneroOutputWallet, received: bool) -> None: @override def on_output_received(self, output: MoneroOutputWallet) -> None: + """Invoked when the wallet receives a new output. + + :param MoneroOutputWallet output: the output received. + """ self.test_output(output, True) @override def on_output_spent(self, output: MoneroOutputWallet) -> None: + """Invoked when one of the wallet's outputs is spent. + + :param MoneroOutputWallet output: the output spent. + """ self.test_output(output, False) @override def on_done(self, chain_height: int) -> None: + """Invoked when the wallet's initial sync completes. + + :param int chain_height: blockchain height the wallet finished syncing to. + """ super().on_done(chain_height) assert self.wallet_tester_prev_height is not None diff --git a/tests/utils/wallet_test_utils.py b/tests/utils/wallet_test_utils.py index 8688228..1752ba8 100644 --- a/tests/utils/wallet_test_utils.py +++ b/tests/utils/wallet_test_utils.py @@ -94,7 +94,7 @@ def is_wallet_funded(cls, wallet: MoneroWallet, xmr_amount_per_address: float, n for account in accounts: for subaddress in account.subaddresses: - balance = subaddress.unlocked_balance + balance: int | None = subaddress.unlocked_balance assert balance is not None if balance >= amount_per_address: subaddresses_found += 1 @@ -110,6 +110,16 @@ def build_tx_config( amount_per_address: int, supports_get_accounts: bool ) -> MoneroTxConfig: + """Build a tx configuration that funds every subaddress across the given accounts up to + `amount_per_address`, creating accounts/subaddresses as needed. + + :param MoneroWallet wallet: wallet to build the tx configuration for. + :param int num_accounts: number of accounts to fund. + :param int num_subaddresses: number of subaddresses per account to fund. + :param int amount_per_address: minimum unlocked balance each subaddress should end up with. + :param bool supports_get_accounts: `True` if the wallet supports listing/creating accounts. + :returns MoneroTxConfig: tx configuration with one destination per underfunded subaddress. + """ tx_config: MoneroTxConfig = MoneroTxConfig() tx_config.account_index = 0 tx_config.relay = True @@ -133,7 +143,7 @@ def build_tx_config( continue assert address.address is not None - dest = MoneroDestination(address.address, amount_per_address) + dest: MoneroDestination = MoneroDestination(address.address, amount_per_address) tx_config.destinations.append(dest) return tx_config @@ -152,6 +162,7 @@ def fund_wallet( :param float xmr_amount_per_address: XMR amount to fund each address. :param int num_accounts: number of accounts to fund. :param int num_subaddresses: number of subaddress to fund for each account. + :param bool close_mining_wallet: close the mining wallet after funding (default `False`). :returns list[MoneroTxWallet]: Funding transactions created from mining wallet. """ primary_addr: str = wallet.get_primary_address() diff --git a/tests/utils/wallet_transfers_utils.py b/tests/utils/wallet_transfers_utils.py index 3abfd48..29959b6 100644 --- a/tests/utils/wallet_transfers_utils.py +++ b/tests/utils/wallet_transfers_utils.py @@ -28,9 +28,15 @@ def get_and_test_transfers( :param MoneroTransferQuery | None query: filter wallet transfers by query if defined. :param TxContext | None ctx: transaction context. :param bool | None is_expected: expects empty/non-empty transfers. + :returns list[MoneroTransfer]: the fetched, tested transfers. """ copy: Optional[MoneroTransferQuery] = query.copy() if query is not None else None - transfers = wallet.get_transfers(query) if query is not None else wallet.get_transfers(MoneroTransferQuery()) + + transfers: list[MoneroTransfer] + if query is not None: + transfers = wallet.get_transfers(query) + else: + transfers = wallet.get_transfers(MoneroTransferQuery()) if is_expected is False: assert len(transfers) == 0 diff --git a/tests/utils/wallet_tx_tracker.py b/tests/utils/wallet_tx_tracker.py index 8c05835..3fa5d0c 100644 --- a/tests/utils/wallet_tx_tracker.py +++ b/tests/utils/wallet_tx_tracker.py @@ -3,7 +3,7 @@ from time import sleep from monero import ( MoneroDaemon, MoneroWallet, MoneroTxQuery, MoneroSyncResult, - MoneroTxWallet + MoneroTxWallet, MoneroMiningStatus ) logger: logging.Logger = logging.getLogger("WalletTxTracker") @@ -28,7 +28,10 @@ class WalletTxTracker: @property def sync_period(self) -> float: - """Sync period in seconds.""" + """Sync period in seconds. + + :returns float: sync period in seconds. + """ return self._sync_period_ms / 1000 def __init__(self, daemon: MoneroDaemon, sync_period_ms: int, mining_address: str) -> None: @@ -43,6 +46,7 @@ def __init__(self, daemon: MoneroDaemon, sync_period_ms: int, mining_address: st self._mining_address = mining_address def _sleep(self) -> None: + """Sleep for one sync period.""" sleep(self.sync_period) def _wait_for_txs_to_clear(self, clear_from_wallet: bool, wallets: list[MoneroWallet]) -> None: @@ -69,7 +73,7 @@ def _wait_for_txs_to_clear(self, clear_from_wallet: bool, wallets: list[MoneroWa assert result.num_blocks_fetched is not None if result.num_blocks_fetched > 0: logger.debug(f"Synced wallet {i + 1}, blocks fetched {result.num_blocks_fetched}") - query = MoneroTxQuery() + query: MoneroTxQuery = MoneroTxQuery() query.in_tx_pool = True pool_txs: list[MoneroTxWallet] = wallet.get_txs(query) for tx in pool_txs: @@ -114,7 +118,7 @@ def _wait_for_txs_to_clear(self, clear_from_wallet: bool, wallets: list[MoneroWa if is_first: is_first = False logger.info(f"Waiting for wallet txs to clear from the pool in order to fully sync and avoid double spend attempts: {tx_hashes_pool}") - mining_status = self._daemon.get_mining_status() + mining_status: MoneroMiningStatus = self._daemon.get_mining_status() if mining_status.is_active is not True: try: self._daemon.start_mining(self._mining_address, 1, False, False) @@ -188,6 +192,7 @@ def wait_for_unlocked_balance( raise err # check if wallet has unlocked balance + unlocked_balance: int if subaddress_index is not None: unlocked_balance = wallet.get_unlocked_balance(account_index, subaddress_index) else: diff --git a/tests/utils/wallet_txs_utils.py b/tests/utils/wallet_txs_utils.py index 4e36235..024691e 100644 --- a/tests/utils/wallet_txs_utils.py +++ b/tests/utils/wallet_txs_utils.py @@ -29,9 +29,11 @@ def get_and_test_txs( :param MoneroTxQuery | None query: filter wallet txs by query if defined. :param TxContext | None ctx: transaction context. :param bool | None is_expected: expects empty/non-empty txs. + :param bool regtest: indicates if running test on regtest network. + :returns list[MoneroTxWallet]: the fetched, tested txs. """ copy: Optional[MoneroTxQuery] = query.copy() if query is not None else None - txs = wallet.get_txs(query) if query is not None else wallet.get_txs() + txs: list[MoneroTxWallet] = wallet.get_txs(query) if query is not None else wallet.get_txs() assert txs is not None if is_expected is False: @@ -58,12 +60,13 @@ def get_random_transactions( ) -> list[MoneroTxWallet]: """Get random transaction from wallet. - :param Wallet wallet: wallet to get random txs from. - :param MoneroTxQuery | None: filter txs by query (default `None`). + :param MoneroWallet wallet: wallet to get random txs from. + :param MoneroTxQuery | None query: filter txs by query (default `None`). :param int | None min_txs: minimum number of txs to get (default `None`). :param int | None max_txs: maximum number of txs to get (default `None`). + :returns list[MoneroTxWallet]: the fetched random txs. """ - txs = wallet.get_txs(query if query is not None else MoneroTxQuery()) + txs: list[MoneroTxWallet] = wallet.get_txs(query if query is not None else MoneroTxQuery()) if min_txs is not None: assert len(txs) >= min_txs, f"{len(txs)}/{min_txs} transactions found with the query" @@ -92,12 +95,12 @@ def get_unrelayed_tx(cls, wallet: MoneroWallet, account_idx: int) -> MoneroTxWal """ # TODO monero-project assert account_idx > 0, "Txs sent from/to same account are not properly synced from the pool" - config = MoneroTxConfig() + config: MoneroTxConfig = MoneroTxConfig() config.account_index = account_idx config.address = wallet.get_primary_address() config.amount = TxWalletUtils.MAX_FEE - tx = wallet.create_tx(config) + tx: MoneroTxWallet = wallet.create_tx(config) assert (tx.full_hex is None or tx.full_hex == "") is False assert tx.relay is False, f"Expected tx.relay to be False, got {tx.relay}" return tx @@ -114,7 +117,7 @@ def test_scan_txs(cls, wallet: MoneroWallet, scan_wallet: MoneroWallet) -> None: txs: list[MoneroTxWallet] = wallet.get_txs() assert len(txs) > 2, "Not enough txs to scan" for i in range(1, 3): - tx_hash = txs[i].hash + tx_hash: str | None = txs[i].hash assert tx_hash is not None tx_hashes.append(tx_hash) diff --git a/tests/utils/wallet_utils.py b/tests/utils/wallet_utils.py index 1d49221..197ac43 100644 --- a/tests/utils/wallet_utils.py +++ b/tests/utils/wallet_utils.py @@ -119,7 +119,7 @@ def test_account(cls, account: Optional[MoneroAccount], network_type: MoneroNetw """Test a monero wallet account. :param MoneroAccount | None account: wallet account to test. - :param MoneroNetworkType: wallet network type. + :param MoneroNetworkType network_type: wallet network type. :param bool full: validates also `balance`, `unlocked_balance` and `subaddresses` (default `True`). """ # test account @@ -143,7 +143,7 @@ def test_account(cls, account: Optional[MoneroAccount], network_type: MoneroNetw cls.test_subaddress(account.subaddresses[i]) assert account.index == account.subaddresses[i].account_index assert i == account.subaddresses[i].index - address_balance = account.subaddresses[i].balance + address_balance: int | None = account.subaddresses[i].balance assert address_balance is not None balance += address_balance address_balance = account.subaddresses[i].unlocked_balance @@ -250,7 +250,12 @@ def test_multisig_info(cls, info: MoneroMultisigInfo, threshold: int, num_partic @classmethod def build_payment_uri_config(cls, address: str) -> MoneroTxConfig: - tx_config = MoneroTxConfig() + """Build a sample tx configuration to test payment URI conversion. + + :param str address: destination address for the tx configuration. + :returns MoneroTxConfig: sample tx configuration for payment URI tests. + """ + tx_config: MoneroTxConfig = MoneroTxConfig() tx_config.address = address tx_config.amount = 250000000000 tx_config.recipient_name = "John Doe"