Files
DashBoard/tests/test_realtime_equipment_cache.py
egg e2ce75b004 fix(hold): dedup equipment cache, fix portal iframe, improve Hold dashboards
- Equipment cache: add freshness gate so only 1 Oracle query per 5-min cycle
  across 4 gunicorn workers; sync worker waits before first refresh
- Portal: add frame-busting to prevent recursive iframe nesting
- Hold Overview: remove redundant TreeMap, add Product & Future Hold Comment
  columns to LotTable
- Hold History: switch list.sql JOIN from DW_MES_LOT_V (WIP snapshot) to
  DW_MES_CONTAINER (historical master) for reliable Product data; add
  Future Hold Comment column; fix comment truncation with hover tooltip
- Page status: reorganize drawer groupings

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 09:01:02 +08:00

644 lines
26 KiB
Python

# -*- coding: utf-8 -*-
"""Unit tests for realtime_equipment_cache module.
Tests aggregation, status classification, and cache query functionality.
"""
import pytest
from unittest.mock import patch, MagicMock
import json
from datetime import datetime, timedelta
import pandas as pd
class TestClassifyStatus:
"""Test _classify_status function."""
def test_classifies_prd_as_productive(self):
"""Test PRD status is classified as PRODUCTIVE."""
from mes_dashboard.services.realtime_equipment_cache import _classify_status
result = _classify_status('PRD')
assert result == 'PRODUCTIVE'
def test_classifies_sby_as_standby(self):
"""Test SBY status is classified as STANDBY."""
from mes_dashboard.services.realtime_equipment_cache import _classify_status
result = _classify_status('SBY')
assert result == 'STANDBY'
def test_classifies_udt_as_down(self):
"""Test UDT status is classified as DOWN."""
from mes_dashboard.services.realtime_equipment_cache import _classify_status
result = _classify_status('UDT')
assert result == 'DOWN'
def test_classifies_sdt_as_down(self):
"""Test SDT status is classified as DOWN."""
from mes_dashboard.services.realtime_equipment_cache import _classify_status
result = _classify_status('SDT')
assert result == 'DOWN'
def test_classifies_egt_as_engineering(self):
"""Test EGT status is classified as ENGINEERING."""
from mes_dashboard.services.realtime_equipment_cache import _classify_status
result = _classify_status('EGT')
assert result == 'ENGINEERING'
def test_classifies_nst_as_not_scheduled(self):
"""Test NST status is classified as NOT_SCHEDULED."""
from mes_dashboard.services.realtime_equipment_cache import _classify_status
result = _classify_status('NST')
assert result == 'NOT_SCHEDULED'
def test_classifies_scrap_as_inactive(self):
"""Test SCRAP status is classified as INACTIVE."""
from mes_dashboard.services.realtime_equipment_cache import _classify_status
result = _classify_status('SCRAP')
assert result == 'INACTIVE'
def test_classifies_unknown_as_other(self):
"""Test unknown status is classified as OTHER."""
from mes_dashboard.services.realtime_equipment_cache import _classify_status
result = _classify_status('UNKNOWN_STATUS')
assert result == 'OTHER'
def test_handles_none_status(self):
"""Test None status is classified as OTHER."""
from mes_dashboard.services.realtime_equipment_cache import _classify_status
result = _classify_status(None)
assert result == 'OTHER'
def test_handles_empty_status(self):
"""Test empty string status is classified as OTHER."""
from mes_dashboard.services.realtime_equipment_cache import _classify_status
result = _classify_status('')
assert result == 'OTHER'
class TestAggregateByResourceid:
"""Test _aggregate_by_resourceid function."""
def test_aggregates_single_record(self):
"""Test aggregation with single record per resource."""
from mes_dashboard.services.realtime_equipment_cache import _aggregate_by_resourceid
records = [
{
'RESOURCEID': 'R001',
'EQUIPMENTID': 'E001',
'OBJECTCATEGORY': 'ASSEMBLY',
'EQUIPMENTASSETSSTATUS': 'PRD',
'EQUIPMENTASSETSSTATUSREASON': None,
'RUNCARDLOTID': 'LOT001',
'JOBORDER': 'JO001',
'JOBSTATUS': 'RUN',
'SYMPTOMCODE': None,
'CAUSECODE': None,
'REPAIRCODE': None,
'LOTTRACKINQTY_PCS': 100,
'LOTTRACKINTIME': '2024-01-15T10:00:00',
}
]
result = _aggregate_by_resourceid(records)
assert len(result) == 1
assert result[0]['RESOURCEID'] == 'R001'
assert result[0]['LOT_COUNT'] == 1
assert result[0]['TOTAL_TRACKIN_QTY'] == 100
assert result[0]['STATUS_CATEGORY'] == 'PRODUCTIVE'
def test_aggregates_multiple_lots(self):
"""Test aggregation with multiple LOTs per resource (e.g., oven)."""
from mes_dashboard.services.realtime_equipment_cache import _aggregate_by_resourceid
records = [
{
'RESOURCEID': 'R001',
'EQUIPMENTID': 'E001',
'OBJECTCATEGORY': 'ASSEMBLY',
'EQUIPMENTASSETSSTATUS': 'PRD',
'EQUIPMENTASSETSSTATUSREASON': None,
'RUNCARDLOTID': 'LOT001',
'JOBORDER': 'JO001',
'JOBSTATUS': 'RUN',
'SYMPTOMCODE': None,
'CAUSECODE': None,
'REPAIRCODE': None,
'LOTTRACKINQTY_PCS': 100,
'LOTTRACKINTIME': '2024-01-15T10:00:00',
},
{
'RESOURCEID': 'R001',
'EQUIPMENTID': 'E001',
'OBJECTCATEGORY': 'ASSEMBLY',
'EQUIPMENTASSETSSTATUS': 'PRD',
'EQUIPMENTASSETSSTATUSREASON': None,
'RUNCARDLOTID': 'LOT002',
'JOBORDER': 'JO002',
'JOBSTATUS': 'RUN',
'SYMPTOMCODE': None,
'CAUSECODE': None,
'REPAIRCODE': None,
'LOTTRACKINQTY_PCS': 150,
'LOTTRACKINTIME': '2024-01-15T11:00:00',
},
{
'RESOURCEID': 'R001',
'EQUIPMENTID': 'E001',
'OBJECTCATEGORY': 'ASSEMBLY',
'EQUIPMENTASSETSSTATUS': 'PRD',
'EQUIPMENTASSETSSTATUSREASON': None,
'RUNCARDLOTID': 'LOT003',
'JOBORDER': 'JO003',
'JOBSTATUS': 'RUN',
'SYMPTOMCODE': None,
'CAUSECODE': None,
'REPAIRCODE': None,
'LOTTRACKINQTY_PCS': 50,
'LOTTRACKINTIME': '2024-01-15T09:00:00',
},
]
result = _aggregate_by_resourceid(records)
assert len(result) == 1
assert result[0]['RESOURCEID'] == 'R001'
assert result[0]['LOT_COUNT'] == 3
assert result[0]['TOTAL_TRACKIN_QTY'] == 300 # 100 + 150 + 50
assert result[0]['LATEST_TRACKIN_TIME'] == '2024-01-15T11:00:00'
def test_aggregates_multiple_resources(self):
"""Test aggregation with multiple different resources."""
from mes_dashboard.services.realtime_equipment_cache import _aggregate_by_resourceid
records = [
{
'RESOURCEID': 'R001',
'EQUIPMENTID': 'E001',
'OBJECTCATEGORY': 'ASSEMBLY',
'EQUIPMENTASSETSSTATUS': 'PRD',
'EQUIPMENTASSETSSTATUSREASON': None,
'RUNCARDLOTID': 'LOT001',
'JOBORDER': 'JO001',
'JOBSTATUS': 'RUN',
'SYMPTOMCODE': None,
'CAUSECODE': None,
'REPAIRCODE': None,
'LOTTRACKINQTY_PCS': 100,
'LOTTRACKINTIME': '2024-01-15T10:00:00',
},
{
'RESOURCEID': 'R002',
'EQUIPMENTID': 'E002',
'OBJECTCATEGORY': 'WAFERSORT',
'EQUIPMENTASSETSSTATUS': 'SBY',
'EQUIPMENTASSETSSTATUSREASON': 'Waiting',
'RUNCARDLOTID': None,
'JOBORDER': None,
'JOBSTATUS': None,
'SYMPTOMCODE': None,
'CAUSECODE': None,
'REPAIRCODE': None,
'LOTTRACKINQTY_PCS': None,
'LOTTRACKINTIME': None,
},
]
result = _aggregate_by_resourceid(records)
assert len(result) == 2
r1 = next(r for r in result if r['RESOURCEID'] == 'R001')
r2 = next(r for r in result if r['RESOURCEID'] == 'R002')
assert r1['LOT_COUNT'] == 1
assert r1['STATUS_CATEGORY'] == 'PRODUCTIVE'
assert r2['LOT_COUNT'] == 0
assert r2['STATUS_CATEGORY'] == 'STANDBY'
def test_handles_empty_records(self):
"""Test handles empty record list."""
from mes_dashboard.services.realtime_equipment_cache import _aggregate_by_resourceid
result = _aggregate_by_resourceid([])
assert result == []
def test_handles_null_quantities(self):
"""Test handles null quantities gracefully."""
from mes_dashboard.services.realtime_equipment_cache import _aggregate_by_resourceid
records = [
{
'RESOURCEID': 'R001',
'EQUIPMENTID': 'E001',
'OBJECTCATEGORY': 'ASSEMBLY',
'EQUIPMENTASSETSSTATUS': 'SBY',
'EQUIPMENTASSETSSTATUSREASON': None,
'JOBORDER': None,
'JOBSTATUS': None,
'SYMPTOMCODE': None,
'CAUSECODE': None,
'REPAIRCODE': None,
'LOTTRACKINQTY_PCS': None,
'LOTTRACKINTIME': None,
}
]
result = _aggregate_by_resourceid(records)
assert len(result) == 1
assert result[0]['TOTAL_TRACKIN_QTY'] == 0
assert result[0]['LATEST_TRACKIN_TIME'] is None
def test_skips_records_without_resourceid(self):
"""Test skips records without RESOURCEID."""
from mes_dashboard.services.realtime_equipment_cache import _aggregate_by_resourceid
records = [
{
'RESOURCEID': None,
'EQUIPMENTID': 'E001',
'OBJECTCATEGORY': 'ASSEMBLY',
'EQUIPMENTASSETSSTATUS': 'PRD',
'EQUIPMENTASSETSSTATUSREASON': None,
'JOBORDER': None,
'JOBSTATUS': None,
'SYMPTOMCODE': None,
'CAUSECODE': None,
'REPAIRCODE': None,
'LOTTRACKINQTY_PCS': 100,
'LOTTRACKINTIME': '2024-01-15T10:00:00',
},
{
'RESOURCEID': 'R001',
'EQUIPMENTID': 'E001',
'OBJECTCATEGORY': 'ASSEMBLY',
'EQUIPMENTASSETSSTATUS': 'PRD',
'EQUIPMENTASSETSSTATUSREASON': None,
'JOBORDER': None,
'JOBSTATUS': None,
'SYMPTOMCODE': None,
'CAUSECODE': None,
'REPAIRCODE': None,
'LOTTRACKINQTY_PCS': 50,
'LOTTRACKINTIME': '2024-01-15T10:00:00',
},
]
result = _aggregate_by_resourceid(records)
assert len(result) == 1
assert result[0]['RESOURCEID'] == 'R001'
class TestGetEquipmentStatusById:
"""Test get_equipment_status_by_id function."""
@pytest.fixture(autouse=True)
def reset_modules(self):
"""Reset module state before each test."""
import mes_dashboard.core.redis_client as rc
import mes_dashboard.services.realtime_equipment_cache as eq
rc._REDIS_CLIENT = None
eq._equipment_status_cache.invalidate("equipment_status_all")
eq._invalidate_equipment_status_lookup()
yield
rc._REDIS_CLIENT = None
eq._equipment_status_cache.invalidate("equipment_status_all")
eq._invalidate_equipment_status_lookup()
def test_returns_none_when_redis_unavailable(self):
"""Test returns None when Redis client unavailable."""
from mes_dashboard.services.realtime_equipment_cache import get_equipment_status_by_id
with patch('mes_dashboard.services.realtime_equipment_cache.get_redis_client', return_value=None):
result = get_equipment_status_by_id('R001')
assert result is None
def test_returns_none_when_id_not_found(self):
"""Test returns None when resource ID not in index."""
from mes_dashboard.services.realtime_equipment_cache import get_equipment_status_by_id
mock_client = MagicMock()
mock_client.hget.return_value = None
with patch('mes_dashboard.services.realtime_equipment_cache.get_redis_client', return_value=mock_client):
with patch('mes_dashboard.services.realtime_equipment_cache.get_key_prefix', return_value='mes_wip'):
result = get_equipment_status_by_id('R999')
assert result is None
def test_returns_matching_record(self):
"""Test returns matching record from cache."""
from mes_dashboard.services.realtime_equipment_cache import get_equipment_status_by_id
test_data = [
{'RESOURCEID': 'R001', 'STATUS_CATEGORY': 'PRODUCTIVE'},
{'RESOURCEID': 'R002', 'STATUS_CATEGORY': 'STANDBY'},
]
mock_client = MagicMock()
mock_client.hget.return_value = '1' # Index 1 -> R002
mock_client.get.return_value = json.dumps(test_data)
with patch('mes_dashboard.services.realtime_equipment_cache.get_redis_client', return_value=mock_client):
with patch('mes_dashboard.services.realtime_equipment_cache.get_key_prefix', return_value='mes_wip'):
result = get_equipment_status_by_id('R002')
assert result is not None
assert result['RESOURCEID'] == 'R002'
assert result['STATUS_CATEGORY'] == 'STANDBY'
class TestGetEquipmentStatusByIds:
"""Test get_equipment_status_by_ids function."""
@pytest.fixture(autouse=True)
def reset_modules(self):
"""Reset module state before each test."""
import mes_dashboard.core.redis_client as rc
import mes_dashboard.services.realtime_equipment_cache as eq
rc._REDIS_CLIENT = None
eq._equipment_status_cache.invalidate("equipment_status_all")
eq._invalidate_equipment_status_lookup()
yield
rc._REDIS_CLIENT = None
eq._equipment_status_cache.invalidate("equipment_status_all")
eq._invalidate_equipment_status_lookup()
def test_returns_empty_for_empty_input(self):
"""Test returns empty list for empty input."""
from mes_dashboard.services.realtime_equipment_cache import get_equipment_status_by_ids
result = get_equipment_status_by_ids([])
assert result == []
def test_returns_empty_when_redis_unavailable(self):
"""Test returns empty list when Redis unavailable."""
from mes_dashboard.services.realtime_equipment_cache import get_equipment_status_by_ids
with patch('mes_dashboard.services.realtime_equipment_cache.get_redis_client', return_value=None):
result = get_equipment_status_by_ids(['R001', 'R002'])
assert result == []
def test_returns_matching_records(self):
"""Test returns all matching records."""
from mes_dashboard.services.realtime_equipment_cache import get_equipment_status_by_ids
test_data = [
{'RESOURCEID': 'R001', 'STATUS_CATEGORY': 'PRODUCTIVE'},
{'RESOURCEID': 'R002', 'STATUS_CATEGORY': 'STANDBY'},
{'RESOURCEID': 'R003', 'STATUS_CATEGORY': 'DOWN'},
]
mock_client = MagicMock()
mock_client.hmget.return_value = ['0', '2', None] # R001 at idx 0, R003 at idx 2, R999 not found
mock_client.get.return_value = json.dumps(test_data)
with patch('mes_dashboard.services.realtime_equipment_cache.get_redis_client', return_value=mock_client):
with patch('mes_dashboard.services.realtime_equipment_cache.get_key_prefix', return_value='mes_wip'):
result = get_equipment_status_by_ids(['R001', 'R003', 'R999'])
assert len(result) == 2
ids = [r['RESOURCEID'] for r in result]
assert 'R001' in ids
assert 'R003' in ids
assert 'R999' not in ids
class TestGetAllEquipmentStatus:
"""Test get_all_equipment_status function."""
@pytest.fixture(autouse=True)
def reset_modules(self):
"""Reset module state before each test."""
import mes_dashboard.core.redis_client as rc
import mes_dashboard.services.realtime_equipment_cache as eq
rc._REDIS_CLIENT = None
eq._equipment_status_cache.invalidate("equipment_status_all")
eq._invalidate_equipment_status_lookup()
yield
rc._REDIS_CLIENT = None
eq._equipment_status_cache.invalidate("equipment_status_all")
eq._invalidate_equipment_status_lookup()
def test_returns_empty_when_redis_unavailable(self):
"""Test returns empty list when Redis unavailable."""
from mes_dashboard.services.realtime_equipment_cache import get_all_equipment_status
with patch('mes_dashboard.services.realtime_equipment_cache.get_redis_client', return_value=None):
result = get_all_equipment_status()
assert result == []
def test_returns_empty_when_no_data(self):
"""Test returns empty list when no data in cache."""
from mes_dashboard.services.realtime_equipment_cache import get_all_equipment_status
mock_client = MagicMock()
mock_client.get.return_value = None
with patch('mes_dashboard.services.realtime_equipment_cache.get_redis_client', return_value=mock_client):
with patch('mes_dashboard.services.realtime_equipment_cache.get_key_prefix', return_value='mes_wip'):
result = get_all_equipment_status()
assert result == []
def test_returns_all_cached_data(self):
"""Test returns all cached equipment status."""
from mes_dashboard.services.realtime_equipment_cache import get_all_equipment_status
test_data = [
{'RESOURCEID': 'R001', 'STATUS_CATEGORY': 'PRODUCTIVE'},
{'RESOURCEID': 'R002', 'STATUS_CATEGORY': 'STANDBY'},
]
mock_client = MagicMock()
mock_client.get.return_value = json.dumps(test_data)
with patch('mes_dashboard.services.realtime_equipment_cache.get_redis_client', return_value=mock_client):
with patch('mes_dashboard.services.realtime_equipment_cache.get_key_prefix', return_value='mes_wip'):
result = get_all_equipment_status()
assert len(result) == 2
assert result[0]['RESOURCEID'] == 'R001'
assert result[1]['RESOURCEID'] == 'R002'
class TestGetEquipmentStatusCacheStatus:
"""Test get_equipment_status_cache_status function."""
@pytest.fixture
def app(self):
"""Create application for testing."""
from mes_dashboard.app import create_app
import mes_dashboard.core.database as db
db._ENGINE = None
app = create_app('testing')
app.config['TESTING'] = True
return app
def test_returns_disabled_when_cache_disabled(self, app):
"""Test returns disabled status when cache is disabled."""
app.config['REALTIME_EQUIPMENT_CACHE_ENABLED'] = False
with app.app_context():
from mes_dashboard.services.realtime_equipment_cache import get_equipment_status_cache_status
result = get_equipment_status_cache_status()
assert result['enabled'] is False
assert result['loaded'] is False
def test_returns_loaded_status_when_data_exists(self, app):
"""Test returns loaded status when cache has data."""
app.config['REALTIME_EQUIPMENT_CACHE_ENABLED'] = True
mock_client = MagicMock()
mock_client.get.side_effect = lambda key: {
'mes_wip:equipment_status:meta:updated': '2024-01-15T10:30:00',
'mes_wip:equipment_status:meta:count': '1000',
}.get(key)
with app.app_context():
with patch('mes_dashboard.services.realtime_equipment_cache.get_redis_client', return_value=mock_client):
with patch('mes_dashboard.services.realtime_equipment_cache.get_key_prefix', return_value='mes_wip'):
from mes_dashboard.services.realtime_equipment_cache import get_equipment_status_cache_status
result = get_equipment_status_cache_status()
assert result['enabled'] is True
assert result['loaded'] is True
assert result['count'] == 1000
class TestEquipmentProcessLevelCache:
"""Test bounded process-level cache behavior for equipment status."""
def test_lru_eviction_prefers_recent_keys(self):
import mes_dashboard.services.realtime_equipment_cache as eq
cache = eq._ProcessLevelCache(ttl_seconds=60, max_size=2)
cache.set("a", [{"RESOURCEID": "R001"}])
cache.set("b", [{"RESOURCEID": "R002"}])
assert cache.get("a") is not None # refresh recency
cache.set("c", [{"RESOURCEID": "R003"}]) # should evict "b"
assert cache.get("b") is None
assert cache.get("a") is not None
assert cache.get("c") is not None
def test_global_equipment_cache_uses_bounded_config(self):
import mes_dashboard.services.realtime_equipment_cache as eq
assert eq.EQUIPMENT_PROCESS_CACHE_MAX_SIZE >= 1
assert eq._equipment_status_cache.max_size == eq.EQUIPMENT_PROCESS_CACHE_MAX_SIZE
class TestEquipmentRefreshDedup:
"""Test refresh de-dup behavior and sync worker startup timing."""
def test_refresh_skips_when_recently_updated(self):
"""Should skip Oracle query when cache is fresh and force=False."""
import mes_dashboard.services.realtime_equipment_cache as eq
recent_updated = (datetime.now() - timedelta(seconds=10)).isoformat()
mock_client = MagicMock()
mock_client.get.return_value = recent_updated
with patch.object(eq, "_SYNC_INTERVAL", 300):
with patch.object(eq, "try_acquire_lock", return_value=True):
with patch.object(eq, "release_lock") as mock_release_lock:
with patch.object(eq, "get_redis_client", return_value=mock_client):
with patch.object(eq, "get_key_prefix", return_value="mes_wip"):
with patch.object(eq, "_load_equipment_status_from_oracle") as mock_oracle:
with patch.object(eq, "_save_to_redis", return_value=True) as mock_save:
result = eq.refresh_equipment_status_cache(force=False)
assert result is False
mock_oracle.assert_not_called()
mock_save.assert_not_called()
mock_client.get.assert_called_once_with("mes_wip:equipment_status:meta:updated")
mock_release_lock.assert_called_once_with("equipment_status_cache_update")
def test_refresh_proceeds_when_stale(self):
"""Should proceed with Oracle query when cache is stale."""
import mes_dashboard.services.realtime_equipment_cache as eq
stale_updated = (datetime.now() - timedelta(seconds=200)).isoformat()
mock_client = MagicMock()
mock_client.get.return_value = stale_updated
with patch.object(eq, "_SYNC_INTERVAL", 300):
with patch.object(eq, "try_acquire_lock", return_value=True):
with patch.object(eq, "release_lock"):
with patch.object(eq, "get_redis_client", return_value=mock_client):
with patch.object(eq, "get_key_prefix", return_value="mes_wip"):
with patch.object(eq, "_load_equipment_status_from_oracle", return_value=[{"RESOURCEID": "R001"}]) as mock_oracle:
with patch.object(eq, "_aggregate_by_resourceid", return_value=[{"RESOURCEID": "R001"}]):
with patch.object(eq, "_save_to_redis", return_value=True) as mock_save:
result = eq.refresh_equipment_status_cache(force=False)
assert result is True
mock_oracle.assert_called_once()
mock_save.assert_called_once()
def test_refresh_proceeds_when_force(self):
"""Should bypass freshness gate when force=True."""
import mes_dashboard.services.realtime_equipment_cache as eq
with patch.object(eq, "_SYNC_INTERVAL", 300):
with patch.object(eq, "try_acquire_lock", return_value=True):
with patch.object(eq, "release_lock"):
with patch.object(eq, "get_redis_client") as mock_get_redis_client:
with patch.object(eq, "_load_equipment_status_from_oracle", return_value=[{"RESOURCEID": "R001"}]) as mock_oracle:
with patch.object(eq, "_aggregate_by_resourceid", return_value=[{"RESOURCEID": "R001"}]):
with patch.object(eq, "_save_to_redis", return_value=True) as mock_save:
result = eq.refresh_equipment_status_cache(force=True)
assert result is True
mock_oracle.assert_called_once()
mock_save.assert_called_once()
mock_get_redis_client.assert_not_called()
def test_sync_worker_waits_before_first_refresh(self):
"""Sync worker should not refresh immediately on startup."""
import mes_dashboard.services.realtime_equipment_cache as eq
class StopImmediatelyEvent:
def __init__(self):
self.timeouts = []
def wait(self, timeout=None):
self.timeouts.append(timeout)
return True
fake_stop_event = StopImmediatelyEvent()
with patch.object(eq, "_STOP_EVENT", fake_stop_event):
with patch.object(eq, "refresh_equipment_status_cache") as mock_refresh:
eq._sync_worker(interval=300)
mock_refresh.assert_not_called()
assert fake_stop_event.timeouts == [300]
class TestSharedQueryFragments:
"""Test shared SQL fragment governance for equipment cache."""
def test_equipment_load_uses_shared_sql_fragment(self):
import mes_dashboard.services.realtime_equipment_cache as eq
from mes_dashboard.services.sql_fragments import EQUIPMENT_STATUS_SELECT_SQL
mock_df = pd.DataFrame([{"RESOURCEID": "R001", "EQUIPMENTID": "EQ-01"}])
with patch.object(eq, "read_sql_df", return_value=mock_df) as mock_read:
eq._load_equipment_status_from_oracle()
sql = mock_read.call_args[0][0]
assert sql.strip() == EQUIPMENT_STATUS_SELECT_SQL.strip()