zioinfo-mail/workspace/guardia-itsm/test_e5_finops.py
DESKTOP-TKLFCPR\ython cfe2901a55 refactor(structure): consolidate all projects under workspace/
- itsm/    -> workspace/guardia-itsm/
- manager/ -> workspace/guardia-manager/
- app/     -> workspace/guardia-messenger/
- manual/  -> workspace/guardia-docs/

workspace/zioinfo-web/ unchanged.
git mv preserves full commit history.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 23:50:56 +09:00

269 lines
11 KiB
Python

"""E-5 FinOps 비용 분석 테스트"""
import sys, ast, os, re, json
os.environ.setdefault("GUARDIA_SECRET_KEY", "test-e5-secret-key-32bytes-padded!")
os.environ.setdefault("DATABASE_URL", "sqlite+aiosqlite:///./test_e5.db")
ok = True
print("=== 1. 구문 검사 ===")
files = ["routers/finops.py", "main.py"]
for f in files:
try:
with open(f, encoding="utf-8") as fh:
src = fh.read()
ast.parse(src)
print(f" OK {f}")
except SyntaxError as e:
print(f" ERR {f}: {e}")
ok = False
print("\n=== 2. routers/finops.py 엔드포인트 확인 ===")
with open("routers/finops.py", encoding="utf-8") as f:
finops_src = f.read()
checks = [
('@router.post("/costs"', "POST /costs 비용 등록"),
('@router.get("/costs"', "GET /costs 비용 목록"),
('@router.get("/summary"', "GET /summary 월별 요약"),
('@router.get("/trend"', "GET /trend 트렌드"),
('@router.get("/allocation"', "GET /allocation 배분"),
('@router.get("/anomalies"', "GET /anomalies 이상 탐지"),
('@router.get("/recommendations"', "GET /recommendations 권고"),
('@router.post("/budget"', "POST /budget 예산 등록"),
('@router.get("/budget"', "GET /budget 예산 대비"),
('@router.get("/optimize"', "GET /optimize AI 최적화"),
("COST_CATEGORIES", "COST_CATEGORIES 카테고리 정의"),
("SERVICES", "SERVICES 서비스 목록"),
("_costs", "_costs 인메모리 저장소"),
("_budgets", "_budgets 예산 저장소"),
("_gen_cost_id", "_gen_cost_id ID 생성"),
("_filter_costs", "_filter_costs 필터 함수"),
("_sum_costs", "_sum_costs 합계 함수"),
("mom_change_pct", "mom_change_pct 전월 비교"),
("usage_pct", "usage_pct 예산 사용률"),
("potential_saving_pct", "potential_saving_pct 절감 가능률"),
("localhost:11434", "Ollama 내부 LLM (외부 API 금지)"),
("UserRole.ADMIN", "ADMIN 권한 제한"),
("UserRole.PM", "PM 권한 제한"),
("trend_dir", "trend_dir UP/DOWN/STABLE"),
("COST-", "COST- ID 접두사"),
("variance", "variance 예산 편차"),
]
for sym, desc in checks:
status = "OK" if sym in finops_src else "ERR"
if status == "ERR": ok = False
print(f" {status} {desc}")
print("\n=== 3. main.py E-5 라우터 등록 확인 ===")
with open("main.py", encoding="utf-8") as f:
main_src = f.read()
main_checks = [
("finops," in main_src or "finops\n" in main_src, "finops 임포트"),
("finops.router" in main_src, "finops.router 등록"),
("E-5" in main_src, "E-5 주석"),
]
for check, desc in main_checks:
status = "OK" if check else "ERR"
if status == "ERR": ok = False
print(f" {status} {desc}")
print("\n=== 4. 비용 카테고리 및 서비스 구성 검증 ===")
try:
# 로컬 정의
COST_CATEGORIES = {
"SERVER": "서버 운영비", "NETWORK": "네트워크/통신비",
"STORAGE": "스토리지 비용", "LICENSE": "소프트웨어 라이선스",
"MAINTENANCE": "유지보수비", "PERSONNEL": "인건비", "OTHER": "기타",
}
SERVICES = ["MES", "ERP", "GROUPWARE", "SECURITY", "INFRA", "BACKUP", "OTHER"]
assert len(COST_CATEGORIES) >= 6, f"카테고리 6개 이상 필요: {len(COST_CATEGORIES)}"
assert len(SERVICES) >= 5, f"서비스 5개 이상 필요: {len(SERVICES)}"
assert "SERVER" in COST_CATEGORIES, "SERVER 카테고리 없음"
assert "LICENSE" in COST_CATEGORIES, "LICENSE 카테고리 없음"
assert "INFRA" in SERVICES, "INFRA 서비스 없음"
print(f" OK 카테고리 {len(COST_CATEGORIES)}개: {list(COST_CATEGORIES)}")
print(f" OK 서비스 {len(SERVICES)}개: {SERVICES}")
except AssertionError as e:
print(f" ERR {e}")
ok = False
print("\n=== 5. 비용 집계 로직 검증 ===")
try:
# _sum_costs 로직 재현
def sum_costs(items):
return round(sum(c["amount"] for c in items), 2)
def filter_costs(costs, year, month=None, service=None):
result = []
for c in costs.values():
if c["year"] != year: continue
if month is not None and c["month"] != month: continue
if service is not None and c["service"] != service: continue
result.append(c)
return result
costs_db = {
"C1": {"year": 2026, "month": 5, "category": "SERVER", "service": "INFRA", "amount": 1500000},
"C2": {"year": 2026, "month": 5, "category": "LICENSE", "service": "ERP", "amount": 800000},
"C3": {"year": 2026, "month": 5, "category": "NETWORK", "service": "INFRA", "amount": 300000},
"C4": {"year": 2026, "month": 4, "category": "SERVER", "service": "INFRA", "amount": 1400000},
}
may_items = filter_costs(costs_db, 2026, 5)
assert len(may_items) == 3, f"5월 항목 3개 기대: {len(may_items)}"
assert sum_costs(may_items) == 2600000, f"5월 합계 오류: {sum_costs(may_items)}"
print(f" OK 5월 필터: {len(may_items)}건, 합계: {sum_costs(may_items):,}")
infra_items = filter_costs(costs_db, 2026, 5, "INFRA")
assert len(infra_items) == 2, f"INFRA 5월 2건 기대: {len(infra_items)}"
assert sum_costs(infra_items) == 1800000, f"INFRA 합계 오류"
print(f" OK INFRA 5월 필터: {len(infra_items)}건, {sum_costs(infra_items):,}")
# 전월 비교
apr_total = sum_costs(filter_costs(costs_db, 2026, 4))
may_total = sum_costs(filter_costs(costs_db, 2026, 5))
mom_change = round((may_total - apr_total) / apr_total * 100, 1)
assert mom_change > 0, f"전월 대비 증가 기대: {mom_change}%"
print(f" OK 전월 비교: {apr_total:,}원 -> {may_total:,}원 ({mom_change:+.1f}%)")
except AssertionError as e:
print(f" ERR {e}")
ok = False
print("\n=== 6. 예산 대비 실적 검증 ===")
try:
def calc_budget_status(actual, budget):
if budget <= 0:
return "NO_BUDGET", 0.0
usage_pct = round(actual / budget * 100, 1)
if usage_pct > 110:
return "OVER", usage_pct
elif usage_pct > 90:
return "WARNING", usage_pct
else:
return "OK", usage_pct
tests = [
(900000, 1000000, "OK", 90.0),
(950000, 1000000, "WARNING", 95.0),
(1150000,1000000, "OVER", 115.0),
(0, 0, "NO_BUDGET", 0.0),
]
for actual, budget, exp_status, exp_pct in tests:
status, pct = calc_budget_status(actual, budget)
assert status == exp_status, f"상태 오류: actual={actual}, budget={budget}, got={status}"
assert pct == exp_pct, f"사용률 오류: {pct} (기대: {exp_pct})"
print(f" OK 예산 상태: OK/WARNING/OVER/NO_BUDGET 모두 정확")
# variance 계산
actual = 1150000
budget = 1000000
variance = round(actual - budget, 2)
assert variance == 150000, f"variance 오류: {variance}"
remaining = round(budget - actual, 2)
assert remaining == -150000, f"remaining 오류: {remaining}"
print(f" OK variance={variance:,}원, remaining={remaining:,}원 (초과)")
except AssertionError as e:
print(f" ERR {e}")
ok = False
print("\n=== 7. 이상 탐지 로직 검증 ===")
try:
def detect_anomalies(curr_amt, prev_amt, threshold=30.0):
if prev_amt <= 0:
return None
change_pct = round((curr_amt - prev_amt) / prev_amt * 100, 1)
if abs(change_pct) >= threshold:
return {
"change_pct": change_pct,
"direction": "UP" if change_pct > 0 else "DOWN",
"severity": "CRITICAL" if abs(change_pct) >= 50 else "WARNING",
}
return None
# 50% 급증 → CRITICAL
anomaly = detect_anomalies(1500000, 1000000, 30.0)
assert anomaly is not None, "50% 급증 탐지 실패"
assert anomaly["severity"] == "CRITICAL", f"CRITICAL 기대: {anomaly['severity']}"
assert anomaly["direction"] == "UP", "방향 오류"
print(f" OK 50% 급증 탐지: {anomaly['change_pct']}% CRITICAL")
# 35% 증가 → WARNING
anomaly2 = detect_anomalies(1350000, 1000000, 30.0)
assert anomaly2 is not None, "35% 증가 탐지 실패"
assert anomaly2["severity"] == "WARNING", f"WARNING 기대: {anomaly2['severity']}"
print(f" OK 35% 증가 탐지: {anomaly2['change_pct']}% WARNING")
# 10% 변동 → 탐지 안 함
anomaly3 = detect_anomalies(1100000, 1000000, 30.0)
assert anomaly3 is None, "10% 변동 오탐"
print(f" OK 10% 변동 미탐지 (정상 범위)")
# 급감도 탐지
anomaly4 = detect_anomalies(400000, 1000000, 30.0)
assert anomaly4 is not None
assert anomaly4["direction"] == "DOWN", "급감 방향 오류"
print(f" OK 60% 급감 탐지: {anomaly4['change_pct']}% DOWN")
except AssertionError as e:
print(f" ERR {e}")
ok = False
print("\n=== 8. 비용 트렌드 방향 판정 검증 ===")
try:
def trend_direction(first_amt, last_amt):
if first_amt <= 0:
return "STABLE"
trend_pct = round((last_amt - first_amt) / first_amt * 100, 1)
return "UP" if trend_pct > 5 else "DOWN" if trend_pct < -5 else "STABLE"
assert trend_direction(1000000, 1200000) == "UP", "20% 증가 UP 기대"
assert trend_direction(1000000, 800000) == "DOWN", "20% 감소 DOWN 기대"
assert trend_direction(1000000, 1030000) == "STABLE", "3% 변화 STABLE 기대"
assert trend_direction(0, 1000000) == "STABLE", "첫달 0 STABLE 기대"
print(f" OK 트렌드 방향 판정 UP/DOWN/STABLE 모두 정확")
except AssertionError as e:
print(f" ERR {e}")
ok = False
print("\n=== 9. 비용 ID 포맷 검증 ===")
try:
import re as re_mod
from datetime import datetime as dt
now = dt.utcnow()
fake_id = f"COST-{now.strftime('%Y%m%d')}-ABCDEF"
pattern = re_mod.compile(r"COST-\d{8}-[A-Z0-9]{6}")
assert pattern.match(fake_id), f"ID 포맷 불일치: {fake_id}"
assert fake_id.startswith("COST-"), "접두사 오류"
print(f" OK 비용 ID 포맷: {fake_id}")
except AssertionError as e:
print(f" ERR {e}")
ok = False
print("\n=== 10. 보안 정책 확인 ===")
sec_checks = [
("localhost:11434" in finops_src, "Ollama 내부 LLM 사용"),
("openai" not in finops_src.lower(), "외부 OpenAI 미사용"),
("anthropic" not in finops_src.lower(), "외부 Anthropic 미사용"),
("ip_addr" not in finops_src, "IP 원본 미포함"),
("os_pw" not in finops_src, "서버 자격증명 미포함"),
("disclaimer" in finops_src, "AI 분석 면책 조항 포함"),
("UserRole.ADMIN" in finops_src, "ADMIN 전용 기능 존재"),
]
for check, desc in sec_checks:
status = "OK" if check else "ERR"
if status == "ERR": ok = False
print(f" {status} {desc}")
print("\n=== E-5 FinOps 비용 분석 테스트 완료 ===")
if ok:
print("모든 검사 통과")
else:
sys.exit(1)