From 9ac3b885c366ab2cc252d9925ba2e0dca5786843 Mon Sep 17 00:00:00 2001 From: Daichi Narushima <1938249+dceoy@users.noreply.github.com> Date: Tue, 23 Jun 2026 21:41:30 +0900 Subject: [PATCH] test: add explicit unit tests for calculate_positions_margin_by_symbol and calculate_positions_margin_safe (#50) (#53) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: add explicit unit tests for calculate_positions_margin_by_symbol and calculate_positions_margin_safe (#50) Covers all acceptance criteria: partial failure with warning log, all-fail, empty symbol list with no-broker-call assertion, duplicate deduplication, successful aggregation with first-seen key order, suppress_errors=False propagation, and three calculate_positions_margin_safe cases (partial skip, all-fail → 0.0, empty list → 0.0). Co-Authored-By: Claude Sonnet 4.6 * test: fix warning log assertion and parametrize suppress_errors=False test - Use record.getMessage() + levelno check instead of record.message, which is only populated after formatting and can return an empty string. - Parametrize test_one_symbol_fails_suppress_errors_false over all three exception types caught by the implementation (Mt5TradingError, Mt5RuntimeError, AttributeError) so any future narrowing of the except tuple would be caught by tests. Co-Authored-By: Claude Sonnet 4.6 * style: shorten docstring to fit 88-char line limit * style: shorten docstring to fit 88-char line limit --------- Co-authored-by: agent Co-authored-by: Claude Sonnet 4.6 --- tests/test_trading.py | 76 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 63 insertions(+), 13 deletions(-) diff --git a/tests/test_trading.py b/tests/test_trading.py index e3ee2c7..8ddec7a 100644 --- a/tests/test_trading.py +++ b/tests/test_trading.py @@ -2,6 +2,7 @@ from __future__ import annotations +import logging from types import SimpleNamespace from typing import Any, cast from unittest.mock import MagicMock @@ -3135,7 +3136,7 @@ class TestCalculatePositionsMarginBySymbol: """Tests for calculate_positions_margin_by_symbol (#50).""" def test_all_symbols_succeed(self, mocker: MockerFixture) -> None: - """Returns one entry per symbol when all margin calls succeed.""" + """Returns one entry per symbol in first-seen order when all calls succeed.""" client = _mock_trade_client() mocker.patch( "mt5cli.trading.calculate_positions_margin", @@ -3146,33 +3147,50 @@ class TestCalculatePositionsMarginBySymbol: client, symbols=["EURUSD", "GBPUSD"] ) - assert result == {"EURUSD": 12.5, "GBPUSD": 30.0} + assert list(result.keys()) == ["EURUSD", "GBPUSD"] + _assert_close(result["EURUSD"], 12.5) + _assert_close(result["GBPUSD"], 30.0) - def test_one_symbol_fails_suppress_errors_true(self, mocker: MockerFixture) -> None: - """Skips the failing symbol and returns the successful one.""" + def test_one_symbol_fails_suppress_errors_true( + self, mocker: MockerFixture, caplog: pytest.LogCaptureFixture + ) -> None: + """Skips the failing symbol, emits a warning, and returns the successful one.""" client = _mock_trade_client() mocker.patch( "mt5cli.trading.calculate_positions_margin", side_effect=[Mt5TradingError("tick unavailable"), 30.0], ) - result = calculate_positions_margin_by_symbol( - client, symbols=["EURUSD", "GBPUSD"], suppress_errors=True - ) + with caplog.at_level(logging.WARNING, logger="mt5cli.trading"): + result = calculate_positions_margin_by_symbol( + client, symbols=["EURUSD", "GBPUSD"], suppress_errors=True + ) assert result == {"GBPUSD": 30.0} + assert any( + "EURUSD" in record.getMessage() and record.levelno == logging.WARNING + for record in caplog.records + ) + @pytest.mark.parametrize( + "exc", + [ + Mt5TradingError("trading error"), + Mt5RuntimeError("runtime error"), + AttributeError("missing attr"), + ], + ) def test_one_symbol_fails_suppress_errors_false( - self, mocker: MockerFixture + self, mocker: MockerFixture, exc: Exception ) -> None: - """Re-raises the first failure when suppress_errors=False.""" + """Re-raises the first failure for each caught exception type.""" client = _mock_trade_client() mocker.patch( "mt5cli.trading.calculate_positions_margin", - side_effect=[Mt5TradingError("tick unavailable"), 30.0], + side_effect=[exc, 30.0], ) - with pytest.raises(Mt5TradingError, match="tick unavailable"): + with pytest.raises(type(exc)): calculate_positions_margin_by_symbol( client, symbols=["EURUSD", "GBPUSD"], suppress_errors=False ) @@ -3191,11 +3209,13 @@ class TestCalculatePositionsMarginBySymbol: assert result == {} - def test_empty_symbol_list(self) -> None: - """Returns an empty dict for an empty input list.""" + def test_empty_symbol_list(self, mocker: MockerFixture) -> None: + """Returns an empty dict for an empty input list without any broker calls.""" client = _mock_trade_client() + mock_calc = mocker.patch("mt5cli.trading.calculate_positions_margin") result = calculate_positions_margin_by_symbol(client, symbols=[]) assert result == {} + mock_calc.assert_not_called() def test_duplicate_symbols_preserve_first_seen_order( self, mocker: MockerFixture @@ -3229,3 +3249,33 @@ class TestCalculatePositionsMarginSafe: total = calculate_positions_margin_safe(client, symbols=["EURUSD", "GBPUSD"]) _assert_close(total, 42.5) + + def test_partial_failure_skips_and_sums(self, mocker: MockerFixture) -> None: + """Sums only successful margins when one symbol raises.""" + client = _mock_trade_client() + mocker.patch( + "mt5cli.trading.calculate_positions_margin", + side_effect=[Mt5TradingError("tick unavailable"), 30.0], + ) + + total = calculate_positions_margin_safe(client, symbols=["EURUSD", "GBPUSD"]) + + _assert_close(total, 30.0) + + def test_all_symbols_fail_returns_zero(self, mocker: MockerFixture) -> None: + """Returns 0.0 when every symbol raises.""" + client = _mock_trade_client() + mocker.patch( + "mt5cli.trading.calculate_positions_margin", + side_effect=[Mt5TradingError("err1"), Mt5RuntimeError("err2")], + ) + + total = calculate_positions_margin_safe(client, symbols=["EURUSD", "GBPUSD"]) + + _assert_close(total, 0.0) + + def test_empty_symbols_returns_zero(self) -> None: + """Returns 0.0 for an empty symbol list.""" + client = _mock_trade_client() + total = calculate_positions_margin_safe(client, symbols=[]) + _assert_close(total, 0.0)