Trace pipeline pool isolation: - Switch event_fetcher and lineage_engine to read_sql_df_slow (non-pooled) - Reduce EVENT_FETCHER_MAX_WORKERS 4→2, TRACE_EVENTS_MAX_WORKERS 4→2 - Add 60s timeout per batch query, cache skip for CID>10K - Early del raw_domain_results + gc.collect() for large queries - Increase DB_SLOW_MAX_CONCURRENT: base 3→5, dev 2→3, prod 3→5 Test fixes (51 pre-existing failures → 0): - reject_history: WORKFLOW CSV header, strict bool validation, pareto mock path - portal shell: remove non-existent /tmtt-defect route from tests - conftest: add --run-stress option to skip stress/load tests by default - migration tests: skipif baseline directory missing - performance test: update Vite asset assertion - wip hold: add firstname/waferdesc mock params - template integration: add /reject-history canonical route Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
171 lines
6.0 KiB
Python
171 lines
6.0 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Unit tests for EventFetcher."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import patch
|
|
|
|
import pandas as pd
|
|
|
|
from mes_dashboard.services.event_fetcher import EventFetcher
|
|
|
|
|
|
def test_cache_key_is_stable_for_sorted_ids():
|
|
key1 = EventFetcher._cache_key("history", ["CID-B", "CID-A", "CID-A"])
|
|
key2 = EventFetcher._cache_key("history", ["CID-A", "CID-B"])
|
|
|
|
assert key1 == key2
|
|
assert key1.startswith("evt:history:")
|
|
|
|
|
|
def test_get_rate_limit_config_supports_env_override(monkeypatch):
|
|
monkeypatch.setenv("EVT_HISTORY_RATE_MAX_REQUESTS", "33")
|
|
monkeypatch.setenv("EVT_HISTORY_RATE_WINDOW_SECONDS", "77")
|
|
|
|
config = EventFetcher._get_rate_limit_config("history")
|
|
|
|
assert config["bucket"] == "event-history"
|
|
assert config["max_attempts"] == 33
|
|
assert config["window_seconds"] == 77
|
|
|
|
|
|
@patch("mes_dashboard.services.event_fetcher.read_sql_df")
|
|
@patch("mes_dashboard.services.event_fetcher.cache_get")
|
|
def test_fetch_events_cache_hit_skips_db(mock_cache_get, mock_read_sql_df):
|
|
mock_cache_get.return_value = {"CID-1": [{"CONTAINERID": "CID-1"}]}
|
|
|
|
result = EventFetcher.fetch_events(["CID-1"], "materials")
|
|
|
|
assert result["CID-1"][0]["CONTAINERID"] == "CID-1"
|
|
mock_read_sql_df.assert_not_called()
|
|
|
|
|
|
@patch("mes_dashboard.services.event_fetcher.cache_set")
|
|
@patch("mes_dashboard.services.event_fetcher.cache_get", return_value=None)
|
|
@patch("mes_dashboard.services.event_fetcher.read_sql_df")
|
|
@patch("mes_dashboard.services.event_fetcher.SQLLoader.load_with_params")
|
|
def test_fetch_events_upstream_history_branch(
|
|
mock_sql_load,
|
|
mock_read_sql_df,
|
|
_mock_cache_get,
|
|
mock_cache_set,
|
|
):
|
|
mock_sql_load.return_value = "SELECT * FROM UPSTREAM"
|
|
mock_read_sql_df.return_value = pd.DataFrame(
|
|
[
|
|
{"CONTAINERID": "CID-1", "WORKCENTER_GROUP": "DB"},
|
|
{"CONTAINERID": "CID-2", "WORKCENTER_GROUP": "WB"},
|
|
]
|
|
)
|
|
|
|
result = EventFetcher.fetch_events(["CID-1", "CID-2"], "upstream_history")
|
|
|
|
assert sorted(result.keys()) == ["CID-1", "CID-2"]
|
|
assert mock_sql_load.call_args.args[0] == "mid_section_defect/upstream_history"
|
|
_, params = mock_read_sql_df.call_args.args
|
|
assert len(params) == 2
|
|
mock_cache_set.assert_called_once()
|
|
assert mock_cache_set.call_args.args[0].startswith("evt:upstream_history:")
|
|
|
|
|
|
@patch("mes_dashboard.services.event_fetcher.cache_set")
|
|
@patch("mes_dashboard.services.event_fetcher.cache_get", return_value=None)
|
|
@patch("mes_dashboard.services.event_fetcher.read_sql_df")
|
|
@patch("mes_dashboard.services.event_fetcher.SQLLoader.load")
|
|
def test_fetch_events_history_branch_replaces_container_filter(
|
|
mock_sql_load,
|
|
mock_read_sql_df,
|
|
_mock_cache_get,
|
|
_mock_cache_set,
|
|
):
|
|
mock_sql_load.return_value = (
|
|
"SELECT * FROM t WHERE h.CONTAINERID = :container_id {{ WORKCENTER_FILTER }}"
|
|
)
|
|
mock_read_sql_df.return_value = pd.DataFrame([])
|
|
|
|
EventFetcher.fetch_events(["CID-1"], "history")
|
|
|
|
sql, params = mock_read_sql_df.call_args.args
|
|
assert "h.CONTAINERID = :container_id" not in sql
|
|
assert "{{ WORKCENTER_FILTER }}" not in sql
|
|
assert params == {"p0": "CID-1"}
|
|
|
|
|
|
@patch("mes_dashboard.services.event_fetcher.cache_set")
|
|
@patch("mes_dashboard.services.event_fetcher.cache_get", return_value=None)
|
|
@patch("mes_dashboard.services.event_fetcher.read_sql_df")
|
|
@patch("mes_dashboard.services.event_fetcher.SQLLoader.load")
|
|
def test_fetch_events_materials_branch_replaces_aliased_container_filter(
|
|
mock_sql_load,
|
|
mock_read_sql_df,
|
|
_mock_cache_get,
|
|
_mock_cache_set,
|
|
):
|
|
mock_sql_load.return_value = (
|
|
"SELECT * FROM t m WHERE m.CONTAINERID = :container_id ORDER BY TXNDATE"
|
|
)
|
|
mock_read_sql_df.return_value = pd.DataFrame([])
|
|
|
|
EventFetcher.fetch_events(["CID-1", "CID-2"], "materials")
|
|
|
|
sql, params = mock_read_sql_df.call_args.args
|
|
assert "m.CONTAINERID = :container_id" not in sql
|
|
assert "IN" in sql.upper()
|
|
assert params == {"p0": "CID-1", "p1": "CID-2"}
|
|
|
|
|
|
@patch("mes_dashboard.services.event_fetcher.cache_set")
|
|
@patch("mes_dashboard.services.event_fetcher.cache_get", return_value=None)
|
|
@patch("mes_dashboard.services.event_fetcher.read_sql_df")
|
|
@patch("mes_dashboard.services.event_fetcher.SQLLoader.load")
|
|
def test_fetch_events_rejects_branch_replaces_aliased_container_filter(
|
|
mock_sql_load,
|
|
mock_read_sql_df,
|
|
_mock_cache_get,
|
|
_mock_cache_set,
|
|
):
|
|
mock_sql_load.return_value = (
|
|
"SELECT * FROM t r LEFT JOIN c ON c.CONTAINERID = r.CONTAINERID "
|
|
"WHERE r.CONTAINERID = :container_id ORDER BY r.TXNDATE"
|
|
)
|
|
mock_read_sql_df.return_value = pd.DataFrame([])
|
|
|
|
EventFetcher.fetch_events(["CID-1", "CID-2"], "rejects")
|
|
|
|
sql, params = mock_read_sql_df.call_args.args
|
|
assert "r.CONTAINERID = :container_id" not in sql
|
|
assert "IN" in sql.upper()
|
|
assert params == {"p0": "CID-1", "p1": "CID-2"}
|
|
|
|
|
|
@patch("mes_dashboard.services.event_fetcher.cache_set")
|
|
@patch("mes_dashboard.services.event_fetcher.cache_get", return_value=None)
|
|
@patch("mes_dashboard.services.event_fetcher.read_sql_df")
|
|
@patch("mes_dashboard.services.event_fetcher.SQLLoader.load")
|
|
def test_fetch_events_holds_branch_replaces_aliased_container_filter(
|
|
mock_sql_load,
|
|
mock_read_sql_df,
|
|
_mock_cache_get,
|
|
_mock_cache_set,
|
|
):
|
|
mock_sql_load.return_value = (
|
|
"SELECT * FROM t h LEFT JOIN c ON c.CONTAINERID = h.CONTAINERID "
|
|
"WHERE h.CONTAINERID = :container_id ORDER BY h.HOLDTXNDATE DESC"
|
|
)
|
|
mock_read_sql_df.return_value = pd.DataFrame([])
|
|
|
|
EventFetcher.fetch_events(["CID-1", "CID-2"], "holds")
|
|
|
|
sql, params = mock_read_sql_df.call_args.args
|
|
assert "h.CONTAINERID = :container_id" not in sql
|
|
assert "IN" in sql.upper()
|
|
assert params == {"p0": "CID-1", "p1": "CID-2"}
|
|
|
|
|
|
def test_event_fetcher_uses_slow_connection():
|
|
"""Regression: event_fetcher must use read_sql_df_slow (non-pooled)."""
|
|
import mes_dashboard.services.event_fetcher as ef
|
|
from mes_dashboard.core.database import read_sql_df_slow
|
|
|
|
assert ef.read_sql_df is read_sql_df_slow
|