a5c2aa8e8f
* test: consolidate duplicate test cases into parameterized tests - Combine test_symbol_info, test_symbol_info_tick, test_market_book into test_symbol_command with parametrize - Combine test_order_check variants into test_order_request with parametrize - Combine test_order_send variants into test_order_request with parametrize - Consolidate grafana view skip tests into test_grafana_view_skipped_when_required_cols_missing - Consolidate grafana index skip tests into test_index_skipped_when_cols_missing - Consolidate _NoOp method tests into test_method_is_noop - Consolidate record_*_update noop tests into test_record_update_noop_before_configure This improves test maintainability by reducing duplication while maintaining coverage. * test: parametrize remaining duplicated cases * test: parameterize remaining duplicate test cases * test: parameterize remaining duplicate test cases * test: parameterize remaining duplicate test cases * test: parameterize remaining duplicate test cases * test: parameterize count and start_pos validation tests * test: parameterize remaining duplicate test cases - tests/test_utils.py: merge valid string/integer cases for parse_timeframe and parse_tick_flags. - tests/test_cli.py: consolidate collect-history --if-exists append/fail tests and drop the duplicate collect-history --dataset ticks default flags=-1 assertion. - tests/test_trading.py: parametrize buy/sell projected margin ratio, invalid place_market_order mode validation, core calculate_volume_by_margin boundary cases, and zero-ratio calculate_margin_and_volume sizing cases. * test: parameterize remaining duplicate test cases Consolidate the last high-value duplicate test cases outside the already-changed areas while keeping semantics and 100% coverage. - tests/test_sdk.py:TestMt5CliClient - merge copy_rates_range, copy_ticks_from, history_orders, and latest_rates delegation/ normalization tests into test_method_delegates_with_normalization parameterized by call/expected_method/expected_kwargs with readable ids for each normalization path. - tests/test_sdk.py:TestSubstituteEnvPlaceholders - merge brace/whole-dollar/plain/partial substitution cases into test_substitute_env_placeholders, and missing-env cases into test_substitute_env_placeholders_raises_on_missing_env. - tests/test_trading.py:TestVolumeAndExecution - merge buy/sell trailing-stop main cases into test_calculate_trailing_stop_updates_by_side, opposite-side invalid-quote cases into test_calculate_trailing_stop_updates_ignores_opposite_side_price, and the two mixed-positions scenarios into the parametrized test_calculate_trailing_stop_updates_mixed_positions_skip_invalid_side. All ids explicitly describe the side and behavior. * test: parameterize collect-history and telemetry update tests Consolidate duplicate default-vs-ticks collect-history cases in CLI and SDK tests, and merge history/snapshot record_*_update success and failure tests in test_telemetry.py. Co-authored-by: Cursor <cursoragent@cursor.com> * test: parameterize telemetry record_*_state gauge tests Co-authored-by: Cursor <cursoragent@cursor.com> * test: parameterize Grafana example file checks * test: centralize remaining parametrized cases * test: add parametrized replacements for remaining cases * test: fix remaining parametrized lint * Fix Ruff warnings * test: remove duplicate parametrization sweep file * test: remove duplicate-test deselection hook * test: restore conftest formatting * test: add remaining parametrized regression coverage * test: fix lint issues in parametrization completion tests * test: avoid dynamic SQL in parametrization completion tests * test: format parametrization completion tests * test: fix lint and pyright issues in parametrization coverage * Fix a Pyright error * Update .agents/skills/pr-feedback-triage/SKILL.md * test: dissolve parametrize-completion catch-all into owning test files Move the remaining consolidated cases from test_parametrize_completion.py into their owning modules and delete the file: - test_cli.py: merge snapshot/grafana-schema --publish-copy gating into one parametrized test. - test_trading.py: merge calculate_spread_ratio numeric/numeric-string cases; fold non-finite volume_max into test_normalize_order_volume_deterministic; merge estimate_order_margin invalid-volume cases (0.0/nan/inf); merge calculate_positions_margin invalid-row filtering cases. - test_history.py: parametrize resolve_history_tick_flags and resolve_granularity_name. - test_sdk.py: parametrize collect_latest_closed_rates_for_accounts empty-effective-frame cases. - test_grafana.py: merge account/terminal snapshot insert tests inside TestSnapshotInserts. All remaining cases in the removed file were exact duplicates of existing coverage, so no new test scenarios are introduced. 100% coverage and all checks (ruff, pyright, pytest, local-qa) pass. * test: parameterize remaining account-event, snapshot, and CLI edge cases Consolidates six more pairs of duplicate tests flagged during PR #91 review into pytest.mark.parametrize tables (incremental history_deals account-event edge cases, dedup scope column variants, snapshot view status filtering, snapshot kwargs forwarding, injected-client lifecycle, and close-positions --yes gate), preserving explicit ids and 100% coverage. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * test: consolidate build_config login coercion into parametrized cases Fold separate backward-compat and env-expansion tests into named pytest.param rows for clearer failure output. Co-authored-by: Cursor <cursoragent@cursor.com> * test: consolidate deduplication and publish test cases into parametrized variants Merge related test cases into single parametrized tests: - Consolidate collect-history validation tests into a single parametrized case - Merge snapshot-view skip conditions into one test with parametrized inputs - Combine publish_grafana_copy target variations into one parametrized test - Consolidate incremental-start and drop-duplicates error cases - Remove duplicate test_drop_duplicates_rejects_invalid_identifiers Reduces test file duplication while maintaining full coverage. * test: consolidate remaining review-identified parameterized duplicates Merge close-position filter, margin-ratio suppress/reraise, SQLite path validation, and grafana_symbol_pnl schema cases into clearer parametrized tests. Co-authored-by: Cursor <cursoragent@cursor.com> * test: restore default if_exists coverage in SQLite append test The default-append parametrized case now omits if_exists so the API default remains exercised instead of passing IfExists.APPEND explicitly. Co-authored-by: Cursor <cursoragent@cursor.com> * test: tighten trading parametrization cleanup * test: consolidate remaining parameterized cases * test: consolidate additional duplicate cases into parametrized tests Merge overlapping success-path tests in contracts, history, SDK, and trading modules while preserving 100% coverage and explicit test ids. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: agent <agent@localhost> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
512 lines
17 KiB
Python
512 lines
17 KiB
Python
"""Tests for mt5cli.utils module."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sqlite3
|
|
import sys
|
|
from datetime import UTC, datetime
|
|
from typing import TYPE_CHECKING
|
|
|
|
import pandas as pd
|
|
import pytest
|
|
|
|
import mt5cli.utils
|
|
|
|
if TYPE_CHECKING:
|
|
from pathlib import Path
|
|
|
|
from mt5cli.utils import (
|
|
DATETIME_TYPE,
|
|
REQUEST_TYPE,
|
|
TICK_FLAGS_TYPE,
|
|
TIMEFRAME_TYPE,
|
|
Dataset,
|
|
IfExists,
|
|
detect_format,
|
|
export_dataframe,
|
|
export_dataframe_to_sqlite,
|
|
parse_datetime,
|
|
parse_request,
|
|
parse_tick_flags,
|
|
parse_timeframe,
|
|
)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# detect_format
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestDetectFormat:
|
|
"""Tests for detect_format."""
|
|
|
|
def test_explicit_format_returned(self, tmp_path: Path) -> None:
|
|
"""Test that explicit format overrides extension."""
|
|
result = detect_format(tmp_path / "data.txt", explicit_format="csv")
|
|
assert result == "csv"
|
|
|
|
@pytest.mark.parametrize(
|
|
("filename", "expected"),
|
|
[
|
|
("data.csv", "csv"),
|
|
("data.json", "json"),
|
|
("data.parquet", "parquet"),
|
|
("data.pq", "parquet"),
|
|
("data.db", "sqlite3"),
|
|
("data.sqlite", "sqlite3"),
|
|
("data.sqlite3", "sqlite3"),
|
|
("DATA.CSV", "csv"),
|
|
("DATA.JSON", "json"),
|
|
("DATA.PARQUET", "parquet"),
|
|
],
|
|
)
|
|
def test_auto_detect_from_extension(
|
|
self,
|
|
tmp_path: Path,
|
|
filename: str,
|
|
expected: str,
|
|
) -> None:
|
|
"""Test format auto-detection from file extension."""
|
|
result = detect_format(tmp_path / filename)
|
|
assert result == expected
|
|
|
|
def test_unknown_extension_raises(self, tmp_path: Path) -> None:
|
|
"""Test that unknown extension raises ValueError."""
|
|
with pytest.raises(ValueError, match="Cannot detect format"):
|
|
detect_format(tmp_path / "data.xyz")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# export_dataframe
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestExportDataframe:
|
|
"""Tests for export_dataframe."""
|
|
|
|
@pytest.fixture
|
|
def sample_df(self) -> pd.DataFrame:
|
|
"""Create a sample DataFrame for testing."""
|
|
return pd.DataFrame({"a": [1, 2, 3], "b": ["x", "y", "z"]})
|
|
|
|
@pytest.mark.parametrize(
|
|
("filename", "output_format", "reader"),
|
|
[
|
|
("out.csv", "csv", "csv"),
|
|
("out.json", "json", "json"),
|
|
("out.parquet", "parquet", "parquet"),
|
|
("out.db", "sqlite3", "sqlite3"),
|
|
],
|
|
ids=["csv", "json", "parquet", "sqlite3"],
|
|
)
|
|
def test_export_round_trip(
|
|
self,
|
|
tmp_path: Path,
|
|
sample_df: pd.DataFrame,
|
|
filename: str,
|
|
output_format: str,
|
|
reader: str,
|
|
) -> None:
|
|
"""Test CSV/JSON/Parquet/SQLite3 exports round-trip the sample DataFrame."""
|
|
output = tmp_path / filename
|
|
export_dataframe(sample_df, output, output_format, table_name="test_table")
|
|
if reader == "csv":
|
|
result = pd.read_csv(output)
|
|
pd.testing.assert_frame_equal(result, sample_df)
|
|
elif reader == "json":
|
|
with output.open() as f:
|
|
records = json.load(f)
|
|
assert len(records) == 3
|
|
assert records[0]["a"] == 1
|
|
elif reader == "sqlite3":
|
|
with sqlite3.connect(output) as conn:
|
|
result = pd.read_sql( # type: ignore[reportUnknownMemberType]
|
|
"SELECT * FROM test_table",
|
|
conn,
|
|
)
|
|
pd.testing.assert_frame_equal(result, sample_df)
|
|
else:
|
|
result = pd.read_parquet(output)
|
|
pd.testing.assert_frame_equal(result, sample_df)
|
|
|
|
def test_export_parquet_without_pyarrow(
|
|
self,
|
|
tmp_path: Path,
|
|
sample_df: pd.DataFrame,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""Test that a clear error is raised when pyarrow is not installed."""
|
|
monkeypatch.setitem(sys.modules, "pyarrow", None)
|
|
with pytest.raises(ImportError, match="mt5cli\\[parquet\\]"):
|
|
export_dataframe(sample_df, tmp_path / "out.parquet", "parquet")
|
|
|
|
def test_unsupported_format_raises(
|
|
self,
|
|
tmp_path: Path,
|
|
sample_df: pd.DataFrame,
|
|
) -> None:
|
|
"""Test that unsupported format raises ValueError."""
|
|
with pytest.raises(ValueError, match="Unsupported output format"):
|
|
export_dataframe(sample_df, tmp_path / "out.txt", "xml")
|
|
|
|
|
|
class TestExportDataframeToSqlite:
|
|
"""Tests for export_dataframe_to_sqlite."""
|
|
|
|
@pytest.mark.parametrize(
|
|
("first_if_exists", "second_if_exists"),
|
|
[
|
|
pytest.param(IfExists.REPLACE, IfExists.APPEND, id="replace-then-append"),
|
|
pytest.param(None, None, id="default-append"),
|
|
],
|
|
)
|
|
def test_append_preserves_existing_rows(
|
|
self,
|
|
tmp_path: Path,
|
|
first_if_exists: IfExists | None,
|
|
second_if_exists: IfExists | None,
|
|
) -> None:
|
|
"""Test explicit append and default append modes keep prior rows."""
|
|
output = tmp_path / "append.db"
|
|
first = pd.DataFrame({"id": [1], "value": ["a"]})
|
|
second = pd.DataFrame({"id": [2], "value": ["b"]})
|
|
if first_if_exists is None:
|
|
export_dataframe_to_sqlite(first, output, "items")
|
|
else:
|
|
export_dataframe_to_sqlite(
|
|
first,
|
|
output,
|
|
"items",
|
|
if_exists=first_if_exists,
|
|
)
|
|
if second_if_exists is None:
|
|
export_dataframe_to_sqlite(second, output, "items")
|
|
else:
|
|
export_dataframe_to_sqlite(
|
|
second,
|
|
output,
|
|
"items",
|
|
if_exists=second_if_exists,
|
|
)
|
|
with sqlite3.connect(output) as conn:
|
|
result = pd.read_sql( # type: ignore[reportUnknownMemberType]
|
|
"SELECT id, value FROM items ORDER BY id",
|
|
conn,
|
|
)
|
|
pd.testing.assert_frame_equal(
|
|
result,
|
|
pd.DataFrame({"id": [1, 2], "value": ["a", "b"]}),
|
|
)
|
|
|
|
def test_deduplicate_keeps_latest_row(self, tmp_path: Path) -> None:
|
|
"""Test deduplication keeps the latest ROWID for key columns."""
|
|
output = tmp_path / "dedup.db"
|
|
first = pd.DataFrame({
|
|
"symbol": ["EURUSD", "EURUSD"],
|
|
"time": ["2024-01-01", "2024-01-01"],
|
|
"bid": [1.0, 1.1],
|
|
})
|
|
second = pd.DataFrame({
|
|
"symbol": ["EURUSD"],
|
|
"time": ["2024-01-01"],
|
|
"bid": [1.2],
|
|
})
|
|
export_dataframe_to_sqlite(
|
|
first,
|
|
output,
|
|
"ticks",
|
|
if_exists=IfExists.REPLACE,
|
|
deduplicate_on=("symbol", "time"),
|
|
)
|
|
export_dataframe_to_sqlite(
|
|
second,
|
|
output,
|
|
"ticks",
|
|
if_exists=IfExists.APPEND,
|
|
deduplicate_on=("symbol", "time"),
|
|
)
|
|
with sqlite3.connect(output) as conn:
|
|
result = pd.read_sql( # type: ignore[reportUnknownMemberType]
|
|
"SELECT symbol, time, bid FROM ticks",
|
|
conn,
|
|
)
|
|
pd.testing.assert_frame_equal(
|
|
result.reset_index(drop=True),
|
|
pd.DataFrame({
|
|
"symbol": ["EURUSD"],
|
|
"time": ["2024-01-01"],
|
|
"bid": [1.2],
|
|
}),
|
|
)
|
|
|
|
def test_writes_index_with_label(self, tmp_path: Path) -> None:
|
|
"""Test optional index export with a custom label."""
|
|
output = tmp_path / "index.db"
|
|
frame = pd.DataFrame(
|
|
{"value": [1.0]}, index=pd.Index(["EURUSD"], name="symbol")
|
|
)
|
|
export_dataframe_to_sqlite(
|
|
frame,
|
|
output,
|
|
"margins",
|
|
if_exists=IfExists.REPLACE,
|
|
index=True,
|
|
index_label="symbol",
|
|
)
|
|
with sqlite3.connect(output) as conn:
|
|
result = pd.read_sql( # type: ignore[reportUnknownMemberType]
|
|
"SELECT symbol, value FROM margins",
|
|
conn,
|
|
)
|
|
pd.testing.assert_frame_equal(
|
|
result,
|
|
pd.DataFrame({"symbol": ["EURUSD"], "value": [1.0]}),
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Parse helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestParseDatetime:
|
|
"""Tests for parse_datetime."""
|
|
|
|
@pytest.mark.parametrize(
|
|
("value", "expected"),
|
|
[
|
|
("2024-01-15", datetime(2024, 1, 15, tzinfo=UTC)),
|
|
("2024-01-15T12:00:00+00:00", datetime(2024, 1, 15, 12, 0, 0, tzinfo=UTC)),
|
|
],
|
|
ids=["date", "datetime-with-tz"],
|
|
)
|
|
def test_valid_inputs(self, value: str, expected: datetime) -> None:
|
|
"""Test parsing valid date and datetime strings."""
|
|
assert parse_datetime(value) == expected
|
|
|
|
def test_invalid_format_raises(self) -> None:
|
|
"""Test that invalid format raises ValueError."""
|
|
with pytest.raises(ValueError, match="Invalid datetime"):
|
|
parse_datetime("not-a-date")
|
|
|
|
|
|
class TestParseTimeframe:
|
|
"""Tests for parse_timeframe."""
|
|
|
|
@pytest.mark.parametrize(
|
|
("value", "expected"),
|
|
[
|
|
("M1", 1),
|
|
("h1", 16385),
|
|
("D1", 16408),
|
|
("MN1", 49153),
|
|
("1", 1),
|
|
(16385, 16385),
|
|
],
|
|
ids=["M1", "h1", "D1", "MN1", "int-string-1", "int-16385"],
|
|
)
|
|
def test_valid_timeframe(self, value: str | int, expected: int) -> None:
|
|
"""Test parsing valid string and integer timeframe values."""
|
|
assert parse_timeframe(value) == expected
|
|
|
|
@pytest.mark.parametrize(
|
|
"value",
|
|
["42", "INVALID"],
|
|
ids=["unsupported-integer", "invalid-string"],
|
|
)
|
|
def test_invalid_timeframe_raises(self, value: str) -> None:
|
|
"""Test that invalid timeframe values raise ValueError."""
|
|
with pytest.raises(ValueError, match="Invalid timeframe"):
|
|
parse_timeframe(value)
|
|
|
|
|
|
class TestParseTickFlags:
|
|
"""Tests for parse_tick_flags."""
|
|
|
|
@pytest.mark.parametrize(
|
|
("value", "expected"),
|
|
[
|
|
("ALL", -1),
|
|
("info", 1),
|
|
("TRADE", 2),
|
|
("COPY_TICKS_ALL", -1),
|
|
("-1", -1),
|
|
(2, 2),
|
|
],
|
|
ids=["ALL", "info", "TRADE", "COPY_TICKS_ALL", "int-string--1", "int-2"],
|
|
)
|
|
def test_valid_flag(self, value: str | int, expected: int) -> None:
|
|
"""Test parsing valid string and integer tick flag values."""
|
|
assert parse_tick_flags(value) == expected
|
|
|
|
@pytest.mark.parametrize(
|
|
"value",
|
|
["7", "INVALID"],
|
|
ids=["unsupported-integer", "invalid-string"],
|
|
)
|
|
def test_invalid_flag_raises(self, value: str) -> None:
|
|
"""Test that invalid tick flag values raise ValueError."""
|
|
with pytest.raises(ValueError, match="Invalid tick flags"):
|
|
parse_tick_flags(value)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# parse_request
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestParseRequest:
|
|
"""Tests for parse_request."""
|
|
|
|
def test_inline_json(self) -> None:
|
|
"""Test parsing an inline JSON object string."""
|
|
result = parse_request('{"action": 1, "symbol": "EURUSD"}')
|
|
assert result == {"action": 1, "symbol": "EURUSD"}
|
|
|
|
def test_file_reference(self, tmp_path: Path) -> None:
|
|
"""Test parsing JSON from a file via the @path syntax."""
|
|
path = tmp_path / "req.json"
|
|
path.write_text('{"action": 2}', encoding="utf-8")
|
|
result = parse_request(f"@{path}")
|
|
assert result == {"action": 2}
|
|
|
|
@pytest.mark.parametrize(
|
|
("value", "match"),
|
|
[
|
|
pytest.param("not json", "Invalid JSON request", id="invalid-json"),
|
|
pytest.param("[1, 2, 3]", "must be a JSON object", id="non-object"),
|
|
],
|
|
)
|
|
def test_invalid_request_raises(self, value: str, match: str) -> None:
|
|
"""Test that invalid JSON and non-object requests raise ValueError."""
|
|
with pytest.raises(ValueError, match=match):
|
|
parse_request(value)
|
|
|
|
def test_missing_file_raises(self, tmp_path: Path) -> None:
|
|
"""Test that a missing request file raises ValueError."""
|
|
path = tmp_path / "missing.json"
|
|
with pytest.raises(ValueError, match="Failed to read JSON request file"):
|
|
parse_request(f"@{path}")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Constants
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestConstants:
|
|
"""Tests for module constants."""
|
|
|
|
@pytest.mark.parametrize("name", ["TIMEFRAME_MAP", "TICK_FLAG_MAP"])
|
|
def test_private_maps_absent_from_utils(self, name: str) -> None:
|
|
"""Private pdmt5 maps are not exposed as public mt5cli.utils attributes."""
|
|
assert not hasattr(mt5cli.utils, name)
|
|
|
|
@pytest.mark.parametrize(
|
|
("dataset", "expected"),
|
|
[
|
|
(Dataset.rates, "rates"),
|
|
(Dataset.ticks, "ticks"),
|
|
(Dataset.history_orders, "history_orders"),
|
|
(Dataset.history_deals, "history_deals"),
|
|
],
|
|
)
|
|
def test_dataset_table_name(self, dataset: Dataset, expected: str) -> None:
|
|
"""Test dataset SQLite table names."""
|
|
assert dataset.table_name == expected
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Click ParamTypes
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestDateTimeType:
|
|
"""Tests for _DateTimeType."""
|
|
|
|
def test_convert_string(self) -> None:
|
|
"""Test converting a string to datetime."""
|
|
result = DATETIME_TYPE.convert("2024-06-15", None, None)
|
|
assert result == datetime(2024, 6, 15, tzinfo=UTC)
|
|
|
|
def test_convert_datetime_passthrough(self) -> None:
|
|
"""Test that datetime values pass through unchanged."""
|
|
dt = datetime(2024, 1, 1, tzinfo=UTC)
|
|
assert DATETIME_TYPE.convert(dt, None, None) is dt
|
|
|
|
def test_convert_invalid(self) -> None:
|
|
"""Test that invalid values raise BadParameter."""
|
|
with pytest.raises(Exception, match="Invalid datetime"):
|
|
DATETIME_TYPE.convert("bad", None, None)
|
|
|
|
|
|
class TestTimeframeType:
|
|
"""Tests for _TimeframeType."""
|
|
|
|
@pytest.mark.parametrize(
|
|
("value", "expected"),
|
|
[("H1", 16385), (16385, 16385)],
|
|
ids=["string", "int"],
|
|
)
|
|
def test_convert_valid(self, value: str | int, expected: int) -> None:
|
|
"""Test converting valid string and integer timeframe values."""
|
|
assert TIMEFRAME_TYPE.convert(value, None, None) == expected
|
|
|
|
@pytest.mark.parametrize(
|
|
"value",
|
|
[42, "bad"],
|
|
ids=["unsupported-int", "invalid-string"],
|
|
)
|
|
def test_convert_invalid(self, value: object) -> None:
|
|
"""Test that unsupported int and invalid string values raise BadParameter."""
|
|
with pytest.raises(Exception, match="Invalid timeframe"):
|
|
TIMEFRAME_TYPE.convert(value, None, None)
|
|
|
|
@pytest.mark.parametrize("value", [True, False, None, 1.5])
|
|
def test_convert_invalid_types(self, value: object) -> None:
|
|
"""Test that bool, float, and None values raise BadParameter."""
|
|
with pytest.raises(Exception, match="Invalid timeframe"):
|
|
TIMEFRAME_TYPE.convert(value, None, None)
|
|
|
|
|
|
class TestTickFlagsType:
|
|
"""Tests for _TickFlagsType."""
|
|
|
|
@pytest.mark.parametrize(
|
|
("value", "expected"),
|
|
[("ALL", -1), (2, 2)],
|
|
ids=["string", "int"],
|
|
)
|
|
def test_convert_valid(self, value: str | int, expected: int) -> None:
|
|
"""Test converting valid string and integer tick flag values."""
|
|
assert TICK_FLAGS_TYPE.convert(value, None, None) == expected
|
|
|
|
@pytest.mark.parametrize(
|
|
"value",
|
|
[7, "bad"],
|
|
ids=["unsupported-int", "invalid-string"],
|
|
)
|
|
def test_convert_invalid(self, value: object) -> None:
|
|
"""Test that unsupported int and invalid string values raise BadParameter."""
|
|
with pytest.raises(Exception, match="Invalid tick flags"):
|
|
TICK_FLAGS_TYPE.convert(value, None, None)
|
|
|
|
@pytest.mark.parametrize("value", [True, False, None, 1.5])
|
|
def test_convert_invalid_types(self, value: object) -> None:
|
|
"""Test that bool, float, and None values raise BadParameter."""
|
|
with pytest.raises(Exception, match="Invalid tick flags"):
|
|
TICK_FLAGS_TYPE.convert(value, None, None)
|
|
|
|
|
|
class TestRequestType:
|
|
"""Tests for _RequestType."""
|
|
|
|
def test_convert_string(self) -> None:
|
|
"""Test converting a JSON string to a request dictionary."""
|
|
assert REQUEST_TYPE.convert('{"action": 1}', None, None) == {"action": 1}
|
|
|
|
def test_convert_invalid(self) -> None:
|
|
"""Test that invalid values raise BadParameter."""
|
|
with pytest.raises(Exception, match="Invalid JSON request"):
|
|
REQUEST_TYPE.convert("bad", None, None)
|