Honeywell HC900을 Modbus TCP로 직접 폴링 → gRPC → C# 크롤러 → PostgreSQL. 기존 Experion OPC UA 데이터 경로를 HC900 직접 통신으로 대체. - industrial-comm/cpp: C++ Modbus 게이트웨이 (gRPC 서버) - src: C# .NET 8 ASP.NET Core 크롤러 + 웹 UI (3-Layer) - mcp-server: Python FastMCP (RAG/NL2SQL/P&ID) - 다중 컨트롤러(N-Controller) 지원 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
155 lines
7.5 KiB
Python
155 lines
7.5 KiB
Python
"""Phase B Verifier MVP — 단위 테스트 (최소 10케이스)."""
|
|
|
|
from __future__ import annotations
|
|
import json, pytest
|
|
from verifier import validators
|
|
|
|
|
|
# ── 헬퍼 ──
|
|
|
|
def _noop_conn():
|
|
return None # _load_tag_set 내부에서 호출되지 않도록 monkeypatch로 대체
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
# R1 — tag-existence
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
|
|
def test_R1_valid_tag_format_pass(monkeypatch):
|
|
"""PASS: ficq-6113.pv → 올바른 형식 + DB 존재."""
|
|
monkeypatch.setattr(validators, "_load_tag_set", lambda gc: {"ficq-6113", "p-6102"})
|
|
assert validators.validate_tag("ficq-6113.pv", _noop_conn) is None
|
|
|
|
def test_R1_valid_tag_no_suffix_pass(monkeypatch):
|
|
"""PASS: p-6102 → suffix 없어도 OK."""
|
|
monkeypatch.setattr(validators, "_load_tag_set", lambda gc: {"ficq-6113", "p-6102"})
|
|
assert validators.validate_tag("p-6102", _noop_conn) is None
|
|
|
|
def test_R1_tags_dot_suffix_pass(monkeypatch):
|
|
"""PASS: vp-6117a.pv → 문자 포함 태그 번호."""
|
|
monkeypatch.setattr(validators, "_load_tag_set", lambda gc: {"vp-6117a", "vp-6117b"})
|
|
assert validators.validate_tag("vp-6117a.pv", _noop_conn) is None
|
|
|
|
def test_R1_invalid_tag_format_fail():
|
|
"""FAIL: raw_material_input → 형식 위반 (대문자 + 밑줄)."""
|
|
err = validators.validate_tag("raw_material_input", _noop_conn)
|
|
assert err is not None
|
|
assert err.code == "invalid_tag_format"
|
|
assert "R1" in err.rule
|
|
|
|
def test_R1_unknown_tag_fail(monkeypatch):
|
|
"""FAIL: RM-6101 → 형식은 맞지만 DB에 없음."""
|
|
monkeypatch.setattr(validators, "_load_tag_set", lambda gc: {"ficq-6113", "p-6102"})
|
|
err = validators.validate_tag("RM-6101", _noop_conn)
|
|
assert err is not None
|
|
assert err.code == "tag_not_found"
|
|
assert "suggested" in err.extra
|
|
|
|
def test_R1_none_or_empty_pass():
|
|
"""PASS: None/빈 문자열 → 검증 스킵."""
|
|
assert validators.validate_tag(None, _noop_conn) is None
|
|
assert validators.validate_tag("", _noop_conn) is None
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
# R2 — area-format
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
|
|
def test_R2_valid_area_pass():
|
|
"""PASS: P6, P6-1, P10, UTIL."""
|
|
for a in ("P6", "P6-1", "P10", "UTIL", "PACKING"):
|
|
assert validators.validate_area(a) is None, f"area={a} expected PASS"
|
|
|
|
def test_R2_invalid_area_format_fail():
|
|
"""FAIL: 6-1 → P prefix 없음."""
|
|
err = validators.validate_area("6-1")
|
|
assert err is not None
|
|
assert err.code == "invalid_area_format"
|
|
|
|
def test_R2_unknown_area_fail():
|
|
"""FAIL: P7 → 존재하지 않는 area."""
|
|
err = validators.validate_area("P7")
|
|
assert err is not None
|
|
assert err.code == "unknown_area"
|
|
|
|
def test_R2_empty_or_none_pass():
|
|
"""PASS: None/빈 문자열 → 선택 필터."""
|
|
assert validators.validate_area(None) is None
|
|
assert validators.validate_area("") is None
|
|
|
|
def test_R2_sub_area_pass():
|
|
"""PASS: P6-2 → 형식+base 모두 유효."""
|
|
assert validators.validate_area("P6-2") is None
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
# R4 — trace_connections 보강
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
|
|
def test_R4_valid_direction_pass():
|
|
"""PASS: upstream, downstream."""
|
|
assert validators.validate_direction("upstream") is None
|
|
assert validators.validate_direction("downstream") is None
|
|
assert validators.validate_direction(None) is None
|
|
|
|
def test_R4_invalid_direction_fail():
|
|
"""FAIL: sideways → 허용되지 않은 방향."""
|
|
err = validators.validate_direction("sideways")
|
|
assert err is not None
|
|
assert err.code == "invalid_direction"
|
|
|
|
def test_R4_valid_max_depth_pass():
|
|
"""PASS: 1, 50, 20."""
|
|
for n in (1, 50, 20, None):
|
|
assert validators.validate_max_depth(n) is None, f"max_depth={n} expected PASS"
|
|
|
|
def test_R4_invalid_max_depth_out_of_range():
|
|
"""FAIL: 0, 51 → 범위 초과."""
|
|
err0 = validators.validate_max_depth(0)
|
|
assert err0 is not None and err0.code == "max_depth_out_of_range"
|
|
err51 = validators.validate_max_depth(51)
|
|
assert err51 is not None and err51.code == "max_depth_out_of_range"
|
|
|
|
def test_R4_invalid_max_depth_not_int():
|
|
"""FAIL: 'abc' → 정수 아님."""
|
|
err = validators.validate_max_depth("abc")
|
|
assert err is not None and err.code == "invalid_max_depth"
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
# R3 / R5 stub
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
|
|
def test_R3_stub_returns_none():
|
|
"""Phase B.2 전까지는 항상 None."""
|
|
assert validators.validate_response_text("anything") is None
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
# VerifierError 직렬화
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
|
|
def test_verifier_error_to_dict():
|
|
err = validators.VerifierError("R1", "tag_not_found", "hint msg", suggested=["a", "b"])
|
|
d = err.to_dict()
|
|
assert d["verifier_error"] == "R1.tag_not_found"
|
|
assert d["hint"] == "hint msg"
|
|
assert d["suggested"] == ["a", "b"]
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
# log_rejection — 파일 적재
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
|
|
def test_log_rejection_writes_jsonl(tmp_path, monkeypatch):
|
|
monkeypatch.setattr(validators, "_LOG_DIR", tmp_path)
|
|
err = validators.VerifierError("R2", "invalid_area_format", "bad area", valid_areas=["P6"])
|
|
validators.log_rejection("find_tags", {"area": "6-1"}, err)
|
|
files = list(tmp_path.iterdir())
|
|
assert len(files) == 1
|
|
content = files[0].read_text(encoding="utf-8")
|
|
rec = json.loads(content)
|
|
assert rec["tool"] == "find_tags"
|
|
assert rec["params"] == {"area": "6-1"}
|
|
assert rec["verifier_error"]["verifier_error"] == "R2.invalid_area_format"
|