feat(execution,obs): 호가단위 스냅(dcy-030/#28) + 운영 관측성(dcy-029/#29) + REL-007 환류
dcy-030 (issue #28, P0 체결 복구): - REQ-083 KRX 호가단위(tick) 순수 유틸 (domain/pricing.py) - REQ-070 CHG: 슬리피지 실효가 호가단위 스냅 — 상한 준수(매수 floor/매도 ceil) + band<1tick 하한/상한 보장 + 부동소수 epsilon 가드. 147700→148400/54500→54700. - BAT-044 release-candidate dcy-029 (issue #29, 운영 관측성): - REQ-084 구조화 로깅 기반 (runtime/logging_setup.py): ISO8601 KST·레벨·Loki JSON ·correlation id(slot_id) - REQ-085 주문 결과 로깅 (event=order_submit|fill|reject) - REQ-087 거부 ERROR 가시성 (조용한 롤백 제거) - REQ-086 청산 reason+actor 동반 노출 (안전망/전략 구분, 신규 필드 없음) - REQ-088 운영 조회 CLI report (application/operator_report.py, 보유·오늘 체결 ·실현 PnL net·대기, direct-DB) - REQ-089 운영자 런북 docs/operations.md - REQ-090 baseline 정합 (runtime-flows·project-structure·interface-contract) - BAT-045(P0)·BAT-046(P1) release-candidate REL-007: confirmed → feedback-captured (운영 인스턴스 라이브 검증 + #28/#29 환류 정규화). BAT-037~041 released. 검증: 537 passed, 47 skipped, 회귀 0, ruff clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
"""구조화 로깅 기반 단위 테스트 (REQ-084, dcy-029 FR-1·FR-7)."""
|
||||
import json
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
|
||||
from trex_core.runtime import logging_setup as ls
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _reset_corr():
|
||||
ls.set_correlation_id("")
|
||||
yield
|
||||
ls.set_correlation_id("")
|
||||
|
||||
|
||||
def _record(msg="hello", level=logging.INFO, **extra):
|
||||
rec = logging.LogRecord("trex.test", level, __file__, 1, msg, None, None)
|
||||
for k, v in extra.items():
|
||||
setattr(rec, k, v)
|
||||
return rec
|
||||
|
||||
|
||||
class TestTextFormatter:
|
||||
def test_has_iso8601_kst_timestamp_level_and_msg(self):
|
||||
out = ls.TextFormatter().format(_record("boot ok"))
|
||||
# ISO8601 with +09:00 KST offset, level, message
|
||||
assert "+09:00" in out
|
||||
assert "INFO" in out
|
||||
assert "boot ok" in out
|
||||
|
||||
def test_includes_correlation_id_when_set(self):
|
||||
ls.set_correlation_id("slot-123")
|
||||
rec = _record("x")
|
||||
ls._CorrelationFilter().filter(rec)
|
||||
out = ls.TextFormatter().format(rec)
|
||||
assert "cid=slot-123" in out
|
||||
|
||||
def test_omits_cid_when_unset(self):
|
||||
rec = _record("x")
|
||||
ls._CorrelationFilter().filter(rec)
|
||||
out = ls.TextFormatter().format(rec)
|
||||
assert "cid=" not in out
|
||||
|
||||
|
||||
class TestJsonFormatter:
|
||||
def test_is_valid_json_with_required_keys(self):
|
||||
rec = _record("submitted")
|
||||
ls._CorrelationFilter().filter(rec)
|
||||
obj = json.loads(ls.JsonFormatter().format(rec))
|
||||
assert obj["msg"] == "submitted"
|
||||
assert obj["level"] == "INFO"
|
||||
assert obj["logger"] == "trex.test"
|
||||
assert "+09:00" in obj["ts"]
|
||||
assert obj["correlation_id"] == ""
|
||||
|
||||
def test_correlation_id_propagates(self):
|
||||
ls.set_correlation_id("slot-9")
|
||||
rec = _record("y")
|
||||
ls._CorrelationFilter().filter(rec)
|
||||
obj = json.loads(ls.JsonFormatter().format(rec))
|
||||
assert obj["correlation_id"] == "slot-9"
|
||||
|
||||
def test_extra_fields_included(self):
|
||||
rec = _record("z", symbol="005930")
|
||||
ls._CorrelationFilter().filter(rec)
|
||||
obj = json.loads(ls.JsonFormatter().format(rec))
|
||||
assert obj["symbol"] == "005930"
|
||||
|
||||
|
||||
class TestConfigureAndContext:
|
||||
def test_configure_text_default(self, monkeypatch, capsys):
|
||||
monkeypatch.delenv("TREX_LOG_FORMAT", raising=False)
|
||||
ls.configure_logging(level=logging.INFO)
|
||||
logging.getLogger("trex.x").info("via root")
|
||||
err = capsys.readouterr().err
|
||||
assert "via root" in err and "INFO" in err and "+09:00" in err
|
||||
|
||||
def test_configure_json_format(self, monkeypatch, capsys):
|
||||
monkeypatch.setenv("TREX_LOG_FORMAT", "json")
|
||||
ls.configure_logging(level=logging.INFO)
|
||||
with ls.correlation_id("slot-77"):
|
||||
logging.getLogger("trex.x").info("jsonline")
|
||||
line = [ln for ln in capsys.readouterr().err.splitlines() if "jsonline" in ln][-1]
|
||||
obj = json.loads(line)
|
||||
assert obj["msg"] == "jsonline"
|
||||
assert obj["correlation_id"] == "slot-77"
|
||||
# cleanup: restore text formatter for other tests
|
||||
monkeypatch.delenv("TREX_LOG_FORMAT", raising=False)
|
||||
ls.configure_logging(level=logging.INFO)
|
||||
|
||||
def test_correlation_id_context_manager_restores(self):
|
||||
ls.set_correlation_id("outer")
|
||||
with ls.correlation_id("inner"):
|
||||
assert ls.get_correlation_id() == "inner"
|
||||
assert ls.get_correlation_id() == "outer"
|
||||
|
||||
def test_configure_is_idempotent_single_handler(self):
|
||||
ls.configure_logging()
|
||||
ls.configure_logging()
|
||||
assert len(logging.getLogger().handlers) == 1
|
||||
Reference in New Issue
Block a user