268 lines
10 KiB
Python
268 lines
10 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Unit tests for cache updater module.
|
|
|
|
Tests background cache update logic.
|
|
"""
|
|
|
|
import pytest
|
|
from unittest.mock import patch, MagicMock
|
|
import pandas as pd
|
|
import time
|
|
|
|
|
|
class TestCacheUpdater:
|
|
"""Test CacheUpdater class."""
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def reset_state(self):
|
|
"""Reset module state before each test."""
|
|
import mes_dashboard.core.redis_client as rc
|
|
rc._REDIS_CLIENT = None
|
|
yield
|
|
rc._REDIS_CLIENT = None
|
|
|
|
def test_updater_starts_when_redis_enabled(self, reset_state):
|
|
"""Test updater starts when Redis is enabled."""
|
|
import mes_dashboard.core.cache_updater as cu
|
|
|
|
mock_client = MagicMock()
|
|
mock_client.ping.return_value = True
|
|
|
|
with patch.object(cu, 'REDIS_ENABLED', True):
|
|
with patch.object(cu, 'redis_available', return_value=True):
|
|
with patch.object(cu, 'read_sql_df', return_value=None):
|
|
updater = cu.CacheUpdater(interval=1)
|
|
try:
|
|
updater.start()
|
|
assert updater._is_running is True
|
|
assert updater._thread is not None
|
|
finally:
|
|
updater.stop()
|
|
time.sleep(0.2)
|
|
|
|
def test_updater_does_not_start_when_redis_disabled(self, reset_state):
|
|
"""Test updater does not start when Redis is disabled."""
|
|
import mes_dashboard.core.cache_updater as cu
|
|
|
|
with patch.object(cu, 'REDIS_ENABLED', False):
|
|
updater = cu.CacheUpdater(interval=1)
|
|
updater.start()
|
|
assert updater._is_running is False
|
|
|
|
def test_updater_stops_gracefully(self, reset_state):
|
|
"""Test updater stops gracefully."""
|
|
import mes_dashboard.core.cache_updater as cu
|
|
|
|
mock_client = MagicMock()
|
|
mock_client.ping.return_value = True
|
|
|
|
with patch.object(cu, 'REDIS_ENABLED', True):
|
|
with patch.object(cu, 'redis_available', return_value=True):
|
|
with patch.object(cu, 'read_sql_df', return_value=None):
|
|
updater = cu.CacheUpdater(interval=1)
|
|
updater.start()
|
|
assert updater._is_running is True
|
|
|
|
updater.stop()
|
|
time.sleep(0.2) # Give thread time to stop
|
|
assert updater._is_running is False
|
|
|
|
|
|
class TestCheckSysDate:
|
|
"""Test SYS_DATE checking logic."""
|
|
|
|
def test_check_sys_date_returns_value(self):
|
|
"""Test _check_sys_date returns correct value."""
|
|
import mes_dashboard.core.cache_updater as cu
|
|
|
|
mock_df = pd.DataFrame({'SYS_DATE': ['2024-01-15 10:30:00']})
|
|
|
|
with patch.object(cu, 'read_sql_df', return_value=mock_df):
|
|
updater = cu.CacheUpdater()
|
|
result = updater._check_sys_date()
|
|
assert result == '2024-01-15 10:30:00'
|
|
|
|
def test_check_sys_date_handles_empty_result(self):
|
|
"""Test _check_sys_date handles empty result."""
|
|
import mes_dashboard.core.cache_updater as cu
|
|
|
|
with patch.object(cu, 'read_sql_df', return_value=pd.DataFrame()):
|
|
updater = cu.CacheUpdater()
|
|
result = updater._check_sys_date()
|
|
assert result is None
|
|
|
|
def test_check_sys_date_handles_none_result(self):
|
|
"""Test _check_sys_date handles None result."""
|
|
import mes_dashboard.core.cache_updater as cu
|
|
|
|
with patch.object(cu, 'read_sql_df', return_value=None):
|
|
updater = cu.CacheUpdater()
|
|
result = updater._check_sys_date()
|
|
assert result is None
|
|
|
|
def test_check_sys_date_handles_exception(self):
|
|
"""Test _check_sys_date handles database exception."""
|
|
import mes_dashboard.core.cache_updater as cu
|
|
|
|
with patch.object(cu, 'read_sql_df', side_effect=Exception("Database error")):
|
|
updater = cu.CacheUpdater()
|
|
result = updater._check_sys_date()
|
|
assert result is None
|
|
|
|
|
|
class TestLoadFullTable:
|
|
"""Test full table loading logic."""
|
|
|
|
def test_load_full_table_success(self):
|
|
"""Test _load_full_table loads data correctly."""
|
|
import mes_dashboard.core.cache_updater as cu
|
|
|
|
test_df = pd.DataFrame({
|
|
'LOTID': ['LOT001', 'LOT002'],
|
|
'QTY': [100, 200],
|
|
'WORKORDER': ['WO001', 'WO002']
|
|
})
|
|
|
|
with patch.object(cu, 'read_sql_df', return_value=test_df):
|
|
updater = cu.CacheUpdater()
|
|
result = updater._load_full_table()
|
|
|
|
assert result is not None
|
|
assert len(result) == 2
|
|
|
|
def test_load_full_table_handles_none(self):
|
|
"""Test _load_full_table handles None result."""
|
|
import mes_dashboard.core.cache_updater as cu
|
|
|
|
with patch.object(cu, 'read_sql_df', return_value=None):
|
|
updater = cu.CacheUpdater()
|
|
result = updater._load_full_table()
|
|
assert result is None
|
|
|
|
def test_load_full_table_handles_exception(self):
|
|
"""Test _load_full_table handles exception."""
|
|
import mes_dashboard.core.cache_updater as cu
|
|
|
|
with patch.object(cu, 'read_sql_df', side_effect=Exception("Database error")):
|
|
updater = cu.CacheUpdater()
|
|
result = updater._load_full_table()
|
|
assert result is None
|
|
|
|
|
|
class TestUpdateRedisCache:
|
|
"""Test Redis cache update logic."""
|
|
|
|
def test_update_redis_cache_success(self):
|
|
"""Test _update_redis_cache updates cache correctly."""
|
|
import mes_dashboard.core.cache_updater as cu
|
|
|
|
mock_client = MagicMock()
|
|
mock_pipeline = MagicMock()
|
|
mock_client.pipeline.return_value = mock_pipeline
|
|
|
|
test_df = pd.DataFrame({
|
|
'LOTID': ['LOT001'],
|
|
'QTY': [100]
|
|
})
|
|
|
|
with patch.object(cu, 'get_redis_client', return_value=mock_client):
|
|
with patch.object(cu, 'get_key', side_effect=lambda k: f'mes_wip:{k}'):
|
|
updater = cu.CacheUpdater()
|
|
result = updater._update_redis_cache(test_df, '2024-01-15 10:30:00')
|
|
|
|
assert result is True
|
|
mock_pipeline.rename.assert_called_once()
|
|
mock_pipeline.execute.assert_called_once()
|
|
assert mock_pipeline.set.call_count == 3
|
|
for call in mock_pipeline.set.call_args_list:
|
|
assert call.kwargs.get("ex") == updater.interval * 3
|
|
|
|
def test_update_redis_cache_no_client(self):
|
|
"""Test _update_redis_cache handles no client."""
|
|
import mes_dashboard.core.cache_updater as cu
|
|
|
|
test_df = pd.DataFrame({'LOTID': ['LOT001']})
|
|
|
|
with patch.object(cu, 'get_redis_client', return_value=None):
|
|
updater = cu.CacheUpdater()
|
|
result = updater._update_redis_cache(test_df, '2024-01-15')
|
|
assert result is False
|
|
|
|
def test_update_redis_cache_cleans_staging_key_on_failure(self):
|
|
"""Failed publish should clean staged key and keep function safe."""
|
|
import mes_dashboard.core.cache_updater as cu
|
|
|
|
mock_client = MagicMock()
|
|
mock_pipeline = MagicMock()
|
|
mock_pipeline.execute.side_effect = RuntimeError("pipeline failed")
|
|
mock_client.pipeline.return_value = mock_pipeline
|
|
|
|
test_df = pd.DataFrame({'LOTID': ['LOT001'], 'QTY': [100]})
|
|
|
|
with patch.object(cu, 'get_redis_client', return_value=mock_client):
|
|
with patch.object(cu, 'get_key', side_effect=lambda k: f'mes_wip:{k}'):
|
|
updater = cu.CacheUpdater()
|
|
result = updater._update_redis_cache(test_df, '2024-01-15 10:30:00')
|
|
|
|
assert result is False
|
|
mock_client.delete.assert_called_once()
|
|
staged_key = mock_client.delete.call_args.args[0]
|
|
assert "staging" in staged_key
|
|
|
|
def test_update_redis_cache_ttl_override(self):
|
|
"""Configured TTL override should apply to all Redis keys."""
|
|
import mes_dashboard.core.cache_updater as cu
|
|
|
|
mock_client = MagicMock()
|
|
mock_pipeline = MagicMock()
|
|
mock_client.pipeline.return_value = mock_pipeline
|
|
test_df = pd.DataFrame({'LOTID': ['LOT001'], 'QTY': [100]})
|
|
|
|
with patch.object(cu, 'WIP_CACHE_TTL_SECONDS', 42):
|
|
with patch.object(cu, 'get_redis_client', return_value=mock_client):
|
|
with patch.object(cu, 'get_key', side_effect=lambda k: f'mes_wip:{k}'):
|
|
updater = cu.CacheUpdater(interval=600)
|
|
result = updater._update_redis_cache(test_df, '2024-01-15 10:30:00')
|
|
|
|
assert result is True
|
|
assert mock_pipeline.set.call_count == 3
|
|
for call in mock_pipeline.set.call_args_list:
|
|
assert call.kwargs.get("ex") == 42
|
|
|
|
|
|
class TestCacheUpdateFlow:
|
|
"""Test complete cache update flow."""
|
|
|
|
def test_no_update_when_sys_date_unchanged(self):
|
|
"""Test cache doesn't update when SYS_DATE unchanged."""
|
|
import mes_dashboard.core.cache_updater as cu
|
|
|
|
mock_df = pd.DataFrame({'SYS_DATE': ['2024-01-15 10:30:00']})
|
|
mock_client = MagicMock()
|
|
mock_client.get.return_value = '2024-01-15 10:30:00'
|
|
|
|
with patch.object(cu, 'read_sql_df', return_value=mock_df):
|
|
with patch.object(cu, 'redis_available', return_value=True):
|
|
with patch.object(cu, 'get_redis_client', return_value=mock_client):
|
|
with patch.object(cu, 'get_key', side_effect=lambda k: f'mes_wip:{k}'):
|
|
updater = cu.CacheUpdater()
|
|
# Simulate already having cached the same date
|
|
result = updater._check_and_update(force=False)
|
|
# No update because dates match
|
|
assert result is False
|
|
|
|
def test_update_when_sys_date_changes(self):
|
|
"""Test cache updates when SYS_DATE changes."""
|
|
import mes_dashboard.core.cache_updater as cu
|
|
|
|
updater = cu.CacheUpdater()
|
|
|
|
mock_df = pd.DataFrame({'SYS_DATE': ['2024-01-15 11:00:00']})
|
|
|
|
with patch.object(cu, 'read_sql_df', return_value=mock_df):
|
|
current_date = updater._check_sys_date()
|
|
old_date = '2024-01-15 10:30:00'
|
|
needs_update = current_date != old_date
|
|
|
|
assert needs_update is True
|