- CTNode에 6개 신규 필드 추가 - header : 머리글 (노드 블록 앞 텍스트) - footer : 꼬리글 (노드 블록 뒤 텍스트) - prefix : 접두사 (content 직전 인라인) - suffix : 접미사 (content 직후 인라인) - data_type : 값의 자료형 힌트 (DataType enum) - value : 해석된 값 (Any — str/int/float/bool/list/dict 등) - types.py: DataType enum 추가 (UNSET, STRING~CODE, CUSTOM) - node.py: full_text() 메서드 추가 (6개 필드 순서대로 조합) - tree.py: to_dict / from_dict 직렬화에 신규 필드 포함 - 테스트: test_node_fields.py 신규 추가 (15개 케이스) - 전체 테스트 42/42 통과
195 lines
6.6 KiB
Python
195 lines
6.6 KiB
Python
"""
|
|
tests/test_node_fields.py
|
|
CTNode 의 6개 신규 필드(header, footer, prefix, suffix, data_type, value) 테스트
|
|
"""
|
|
|
|
import pytest
|
|
from ctstruct import CTNode, CTTree, NodeType, DataType
|
|
|
|
|
|
class TestNewFields:
|
|
"""신규 필드 기본 동작 확인."""
|
|
|
|
def test_default_values(self):
|
|
"""미지정 시 모두 None / UNSET 이어야 한다."""
|
|
node = CTNode(NodeType.PARAGRAPH, "hello")
|
|
assert node.header is None
|
|
assert node.footer is None
|
|
assert node.prefix is None
|
|
assert node.suffix is None
|
|
assert node.data_type == DataType.UNSET
|
|
assert node.value is None
|
|
|
|
def test_set_on_construction(self):
|
|
"""생성자에서 모든 필드를 지정할 수 있어야 한다."""
|
|
node = CTNode(
|
|
node_type = NodeType.PARAGRAPH,
|
|
content = "본문",
|
|
header = "== 시작 ==",
|
|
footer = "== 끝 ==",
|
|
prefix = "[",
|
|
suffix = "]",
|
|
data_type = DataType.STRING,
|
|
value = "본문",
|
|
)
|
|
assert node.header == "== 시작 =="
|
|
assert node.footer == "== 끝 =="
|
|
assert node.prefix == "["
|
|
assert node.suffix == "]"
|
|
assert node.data_type == DataType.STRING
|
|
assert node.value == "본문"
|
|
|
|
def test_set_after_construction(self):
|
|
"""생성 후 직접 속성 할당도 가능해야 한다."""
|
|
node = CTNode(NodeType.PARAGRAPH, "숫자")
|
|
node.header = "머리글"
|
|
node.footer = "꼬리글"
|
|
node.prefix = "₩"
|
|
node.suffix = "원"
|
|
node.data_type = DataType.INTEGER
|
|
node.value = 42
|
|
assert node.value == 42
|
|
assert node.data_type == DataType.INTEGER
|
|
|
|
def test_value_types(self):
|
|
"""value 필드는 어떤 파이썬 타입도 허용해야 한다."""
|
|
for val, dtype in [
|
|
(123, DataType.INTEGER),
|
|
(3.14, DataType.FLOAT),
|
|
(True, DataType.BOOLEAN),
|
|
(None, DataType.NULL),
|
|
([1, 2, 3], DataType.LIST),
|
|
({"k": "v"}, DataType.DICT),
|
|
("텍스트", DataType.STRING),
|
|
]:
|
|
node = CTNode(NodeType.CUSTOM, data_type=dtype, value=val)
|
|
assert node.value == val
|
|
assert node.data_type == dtype
|
|
|
|
|
|
class TestFullText:
|
|
"""full_text() — header/prefix/content/suffix/footer 조합 확인."""
|
|
|
|
def test_content_only(self):
|
|
node = CTNode(NodeType.PARAGRAPH, "hello")
|
|
assert node.full_text() == "hello"
|
|
|
|
def test_prefix_suffix(self):
|
|
node = CTNode(NodeType.PARAGRAPH, "world", prefix="(", suffix=")")
|
|
assert node.full_text() == "(world)"
|
|
|
|
def test_header_footer(self):
|
|
node = CTNode(NodeType.PARAGRAPH, "body", header="HEAD\n", footer="\nFOOT")
|
|
assert node.full_text() == "HEAD\nbody\nFOOT"
|
|
|
|
def test_all_fields(self):
|
|
node = CTNode(
|
|
NodeType.PARAGRAPH,
|
|
content = "내용",
|
|
header = "H|",
|
|
footer = "|F",
|
|
prefix = "→",
|
|
suffix = "←",
|
|
)
|
|
assert node.full_text() == "H|→내용←|F"
|
|
|
|
def test_none_fields_skipped(self):
|
|
"""None 필드는 빈 문자열로 취급되지 않고 생략되어야 한다."""
|
|
node = CTNode(NodeType.PARAGRAPH, content=None, prefix=">>")
|
|
assert node.full_text() == ">>"
|
|
|
|
|
|
class TestSerialization:
|
|
"""to_dict / from_dict 에 신규 필드가 포함되는지 확인."""
|
|
|
|
def _make_node(self) -> CTNode:
|
|
return CTNode(
|
|
NodeType.PARAGRAPH,
|
|
content = "직렬화 테스트",
|
|
header = "머리글",
|
|
footer = "꼬리글",
|
|
prefix = ">>",
|
|
suffix = "<<",
|
|
data_type = DataType.MARKDOWN,
|
|
value = "**bold**",
|
|
)
|
|
|
|
def test_to_dict_contains_fields(self):
|
|
tree = CTTree()
|
|
tree.root.append(self._make_node())
|
|
d = tree.to_dict()
|
|
child = d["children"][0]
|
|
assert child["header"] == "머리글"
|
|
assert child["footer"] == "꼬리글"
|
|
assert child["prefix"] == ">>"
|
|
assert child["suffix"] == "<<"
|
|
assert child["data_type"] == DataType.MARKDOWN.value
|
|
assert child["value"] == "**bold**"
|
|
|
|
def test_roundtrip(self):
|
|
"""직렬화 → 역직렬화 후 모든 신규 필드가 보존되어야 한다."""
|
|
tree = CTTree()
|
|
tree.root.append(self._make_node())
|
|
|
|
restored = CTTree.from_dict(tree.to_dict())
|
|
para = restored.find(NodeType.PARAGRAPH)[0]
|
|
|
|
assert para.header == "머리글"
|
|
assert para.footer == "꼬리글"
|
|
assert para.prefix == ">>"
|
|
assert para.suffix == "<<"
|
|
assert para.data_type == DataType.MARKDOWN
|
|
assert para.value == "**bold**"
|
|
|
|
def test_unknown_data_type_falls_back_to_custom(self):
|
|
"""알 수 없는 data_type 문자열은 CUSTOM 으로 폴백되어야 한다."""
|
|
d = {
|
|
"id": "test0001",
|
|
"type": "PARAGRAPH",
|
|
"content": "?",
|
|
"attributes": {},
|
|
"header": None, "footer": None, "prefix": None, "suffix": None,
|
|
"data_type": "not_a_real_type",
|
|
"value": None,
|
|
"children": [],
|
|
}
|
|
tree = CTTree()
|
|
tree.root = CTTree.from_dict(d).root
|
|
# ROOT 복원 확인 — 타입은 ROOT가 됨
|
|
# 직접 역직렬화 테스트
|
|
from ctstruct.types import DataType as DT
|
|
from ctstruct.node import CTNode as CN
|
|
from ctstruct.types import NodeType as NT
|
|
raw_dtype = d.get("data_type", DT.UNSET.value)
|
|
try:
|
|
dtype = DT(raw_dtype)
|
|
except ValueError:
|
|
dtype = DT.CUSTOM
|
|
assert dtype == DT.CUSTOM
|
|
|
|
|
|
class TestPretty:
|
|
"""pretty() 출력에 신규 필드가 표시되는지 확인."""
|
|
|
|
def test_pretty_shows_new_fields(self):
|
|
node = CTNode(
|
|
NodeType.PARAGRAPH,
|
|
content = "텍스트",
|
|
header = "H",
|
|
prefix = "P",
|
|
data_type = DataType.STRING,
|
|
value = "v",
|
|
)
|
|
p = node.pretty()
|
|
assert "header='H'" in p
|
|
assert "prefix='P'" in p
|
|
assert "value='v'" in p
|
|
|
|
def test_dtype_shown_when_set(self):
|
|
node = CTNode(NodeType.PARAGRAPH, data_type=DataType.INTEGER, value=7)
|
|
assert "dtype=integer" in node.pretty()
|
|
|
|
def test_dtype_hidden_when_unset(self):
|
|
node = CTNode(NodeType.PARAGRAPH, "hi")
|
|
assert "dtype" not in node.pretty()
|