186 lines
6.7 KiB
Python
186 lines
6.7 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Unit tests for redis_df_store module."""
|
|
|
|
import pytest
|
|
from unittest.mock import patch, MagicMock
|
|
from decimal import Decimal
|
|
|
|
import pandas as pd
|
|
|
|
|
|
class TestRedisStoreDf:
|
|
"""3.1 — round-trip store/load."""
|
|
|
|
def test_round_trip(self):
|
|
"""Store a DF, load it back, verify equality."""
|
|
import mes_dashboard.core.redis_df_store as rds
|
|
|
|
mock_client = MagicMock()
|
|
stored = {}
|
|
|
|
def fake_setex(key, ttl, value):
|
|
stored[key] = value
|
|
|
|
def fake_get(key):
|
|
return stored.get(key)
|
|
|
|
mock_client.setex.side_effect = fake_setex
|
|
mock_client.get.side_effect = fake_get
|
|
|
|
df = pd.DataFrame({"A": [1, 2, 3], "B": ["x", "y", "z"]})
|
|
|
|
with patch.object(rds, "REDIS_ENABLED", True), \
|
|
patch.object(rds, "get_redis_client", return_value=mock_client):
|
|
rds.redis_store_df("test:key", df, ttl=60)
|
|
loaded = rds.redis_load_df("test:key")
|
|
|
|
assert loaded is not None
|
|
pd.testing.assert_frame_equal(loaded, df)
|
|
|
|
def test_store_empty_df(self):
|
|
"""Round-trip with an empty DataFrame preserves schema."""
|
|
import mes_dashboard.core.redis_df_store as rds
|
|
|
|
mock_client = MagicMock()
|
|
stored = {}
|
|
mock_client.setex.side_effect = lambda k, t, v: stored.update({k: v})
|
|
mock_client.get.side_effect = lambda k: stored.get(k)
|
|
|
|
df = pd.DataFrame({"COL": pd.Series([], dtype="int64")})
|
|
|
|
with patch.object(rds, "REDIS_ENABLED", True), \
|
|
patch.object(rds, "get_redis_client", return_value=mock_client):
|
|
rds.redis_store_df("test:empty", df, ttl=60)
|
|
loaded = rds.redis_load_df("test:empty")
|
|
|
|
assert loaded is not None
|
|
assert len(loaded) == 0
|
|
assert list(loaded.columns) == ["COL"]
|
|
|
|
def test_decimal_object_column_round_trip(self):
|
|
"""Mixed-precision Decimal object columns should store without serialization errors."""
|
|
import mes_dashboard.core.redis_df_store as rds
|
|
|
|
mock_client = MagicMock()
|
|
stored = {}
|
|
mock_client.setex.side_effect = lambda k, t, v: stored.update({k: v})
|
|
mock_client.get.side_effect = lambda k: stored.get(k)
|
|
|
|
df = pd.DataFrame(
|
|
{
|
|
"REJECT_SHARE_PCT": [Decimal("12.345"), Decimal("1.2"), None],
|
|
"REJECT_RATE_PCT": [Decimal("0.123456"), Decimal("10.9"), Decimal("9.000001")],
|
|
"LABEL": ["A", "B", "C"],
|
|
}
|
|
)
|
|
|
|
with patch.object(rds, "REDIS_ENABLED", True), \
|
|
patch.object(rds, "get_redis_client", return_value=mock_client):
|
|
assert rds.redis_store_df("test:decimal", df, ttl=60)
|
|
loaded = rds.redis_load_df("test:decimal")
|
|
|
|
assert loaded is not None
|
|
assert loaded["REJECT_SHARE_PCT"].dtype.kind in ("f", "i")
|
|
assert loaded["REJECT_RATE_PCT"].dtype.kind in ("f", "i")
|
|
assert loaded.loc[0, "REJECT_SHARE_PCT"] == pytest.approx(12.345)
|
|
assert loaded.loc[2, "REJECT_RATE_PCT"] == pytest.approx(9.000001)
|
|
|
|
|
|
class TestChunkHelpers:
|
|
"""3.2 — chunk-level helpers round-trip."""
|
|
|
|
def test_chunk_round_trip(self):
|
|
import mes_dashboard.core.redis_df_store as rds
|
|
|
|
mock_client = MagicMock()
|
|
stored = {}
|
|
mock_client.setex.side_effect = lambda k, t, v: stored.update({k: v})
|
|
mock_client.get.side_effect = lambda k: stored.get(k)
|
|
mock_client.exists.side_effect = lambda k: 1 if k in stored else 0
|
|
|
|
df = pd.DataFrame({"X": [10, 20]})
|
|
|
|
with patch.object(rds, "REDIS_ENABLED", True), \
|
|
patch.object(rds, "get_redis_client", return_value=mock_client):
|
|
rds.redis_store_chunk("reject", "abc123", 0, df, ttl=60)
|
|
assert rds.redis_chunk_exists("reject", "abc123", 0)
|
|
loaded = rds.redis_load_chunk("reject", "abc123", 0)
|
|
|
|
assert loaded is not None
|
|
pd.testing.assert_frame_equal(loaded, df)
|
|
|
|
def test_chunk_not_exists(self):
|
|
import mes_dashboard.core.redis_df_store as rds
|
|
|
|
mock_client = MagicMock()
|
|
mock_client.exists.return_value = 0
|
|
|
|
with patch.object(rds, "REDIS_ENABLED", True), \
|
|
patch.object(rds, "get_redis_client", return_value=mock_client):
|
|
assert not rds.redis_chunk_exists("reject", "abc123", 99)
|
|
|
|
def test_clear_batch_removes_chunk_and_meta_keys(self):
|
|
import mes_dashboard.core.redis_df_store as rds
|
|
|
|
mock_client = MagicMock()
|
|
deleted = {"keys": []}
|
|
|
|
mock_client.keys.return_value = [
|
|
"mes-dashboard:batch:reject:q123:chunk:0",
|
|
"mes-dashboard:batch:reject:q123:chunk:1",
|
|
]
|
|
mock_client.delete.side_effect = lambda *keys: deleted["keys"].extend(keys) or len(keys)
|
|
|
|
with patch.object(rds, "REDIS_ENABLED", True), \
|
|
patch.object(rds, "get_redis_client", return_value=mock_client):
|
|
count = rds.redis_clear_batch("reject", "q123")
|
|
|
|
assert count == 3
|
|
assert any("chunk:0" in key for key in deleted["keys"])
|
|
assert any("chunk:1" in key for key in deleted["keys"])
|
|
assert any("meta" in key for key in deleted["keys"])
|
|
|
|
|
|
class TestRedisUnavailable:
|
|
"""3.3 — graceful fallback when Redis is unavailable."""
|
|
|
|
def test_store_no_redis(self):
|
|
"""store returns without error when Redis disabled."""
|
|
import mes_dashboard.core.redis_df_store as rds
|
|
|
|
df = pd.DataFrame({"A": [1]})
|
|
with patch.object(rds, "REDIS_ENABLED", False):
|
|
rds.redis_store_df("key", df) # no exception
|
|
|
|
def test_load_no_redis(self):
|
|
"""load returns None when Redis disabled."""
|
|
import mes_dashboard.core.redis_df_store as rds
|
|
|
|
with patch.object(rds, "REDIS_ENABLED", False):
|
|
result = rds.redis_load_df("key")
|
|
assert result is None
|
|
|
|
def test_chunk_exists_no_redis(self):
|
|
import mes_dashboard.core.redis_df_store as rds
|
|
|
|
with patch.object(rds, "REDIS_ENABLED", False):
|
|
assert not rds.redis_chunk_exists("p", "h", 0)
|
|
|
|
def test_store_client_none(self):
|
|
"""store returns without error when client is None."""
|
|
import mes_dashboard.core.redis_df_store as rds
|
|
|
|
df = pd.DataFrame({"A": [1]})
|
|
with patch.object(rds, "REDIS_ENABLED", True), \
|
|
patch.object(rds, "get_redis_client", return_value=None):
|
|
rds.redis_store_df("key", df) # no exception
|
|
|
|
def test_load_client_none(self):
|
|
"""load returns None when client is None."""
|
|
import mes_dashboard.core.redis_df_store as rds
|
|
|
|
with patch.object(rds, "REDIS_ENABLED", True), \
|
|
patch.object(rds, "get_redis_client", return_value=None):
|
|
result = rds.redis_load_df("key")
|
|
assert result is None
|