Files
ABAP-Indexing/tests/test_declarations.py
T

195 lines
7.2 KiB
Python

"""정의부(선언) 수집·조립 — parser/declarations.py, index/decls.py.
인덱스의 코드 원문은 로직만이라, 붙여넣으려면 선언이 따로 필요하다. 여기서 지키는 것:
1. 잘라낸 선언 원문이 **그대로 붙여넣을 수 있는 한 문장**일 것 (체인 항목도)
2. `BEGIN OF … END OF` 구조는 한 덩어리일 것
3. 의존을 따라가되(`LIKE GT_DATA` → GT_DATA) 사전 타입 참조(`LIKE BSEG-WRBTR`)는 끌어오지 말 것
4. 조각 안에 이미 있는 선언은 다시 붙이지 말 것
"""
from __future__ import annotations
import pytest
from fastapi.testclient import TestClient
from config.settings import settings
from index import decls as decls_mod
from parser.declarations import extract_declaration_blocks, type_name_after, type_refs
from parser.statements import split_statements
TOP = """REPORT zdecl_t1.
TYPES: BEGIN OF ty_item,
matnr TYPE matnr,
menge TYPE menge_d,
END OF ty_item,
ty_items TYPE STANDARD TABLE OF ty_item.
TABLES: bseg.
DATA: gt_items TYPE ty_items,
gs_item TYPE ty_item,
gv_cnt TYPE i.
DATA: BEGIN OF gt_log OCCURS 0,
msg(80) TYPE c,
END OF gt_log.
CONSTANTS gc_bwart TYPE bwart VALUE '101'.
FIELD-SYMBOLS <ls_item> TYPE ty_item.
"""
FORM_CODE = """FORM collect_items.
DATA lv_local TYPE i.
LOOP AT gt_items INTO gs_item.
lv_local = lv_local + 1.
ADD gs_item-menge TO gv_cnt.
ENDLOOP.
IF gv_cnt > 0.
gt_log-msg = gc_bwart.
APPEND gt_log.
ENDIF.
ENDFORM.
"""
def _blocks(src: str):
sts, _ = split_statements(src, "ZDECL_T1TOP")
return {d.name: d for d in extract_declaration_blocks(
sts, list(range(len(sts))), src.split("\n"), "ZDECL_T1TOP", "global")}
# ------------------------------------------------------------------ 파서
def test_chain_item_is_pasteable():
"""체인 항목은 헤드(`DATA:`)를 다시 붙이고 ',' 를 '.' 로 닫아야 유효한 문장이 된다."""
d = _blocks(TOP)["GS_ITEM"]
assert d.code.startswith("DATA:")
assert d.code.rstrip().endswith(".")
assert "gs_item" in d.code and "gv_cnt" not in d.code # 형제 항목이 딸려오지 않는다
def test_begin_of_block_is_one_declaration():
d = _blocks(TOP)["TY_ITEM"]
assert d.kind == "types"
assert "BEGIN OF ty_item" in d.code and d.code.rstrip().endswith("END OF ty_item.")
assert d.line_end - d.line_start == 3
def test_declaration_dependencies():
b = _blocks(TOP)
assert b["TY_ITEMS"].depends == ["TY_ITEM"]
assert b["GT_ITEMS"].depends == ["TY_ITEMS"]
assert b["TY_ITEM"].depends == ["MATNR", "MENGE_D"] # DDIC — 프로그램 안에는 없다
@pytest.mark.parametrize("expr,expected", [
("TYPE ANY", None), # 이름 없는 타입식 — 뒤 토큰을 삼키면 안 된다
("TYPE STANDARD TABLE OF TY_X", "TY_X"),
("TYPE REF TO LCL_Y", "LCL_Y"),
("LIKE GT_DATA", "GT_DATA"),
("TYPE C", "C"),
])
def test_type_name_after(expr, expected):
assert type_name_after(expr.split(), 0)[0] == expected
def test_type_any_does_not_swallow_next_token():
"""`USING p_a TYPE ANY pv_b TYPE x` 의 pv_b 를 타입으로 오인하면 파라미터가 사라진다."""
_name, nxt = type_name_after("TYPE ANY PV_CLASS TYPE SETHIER-SETCLASS".split(), 0)
assert "TYPE ANY PV_CLASS TYPE SETHIER-SETCLASS".split()[nxt] == "PV_CLASS"
def test_type_refs_keeps_ddic_component():
assert type_refs("DATA LV_X LIKE BSEG-WRBTR".split()) == ["BSEG-WRBTR"]
def test_deferred_class_is_not_a_block():
"""`CLASS x DEFINITION DEFERRED.` 뒤의 선언이 살아 있어야 한다 (ENDCLASS 가 없다)."""
src = ("CLASS lcl_a DEFINITION DEFERRED.\n"
"DATA go_ref TYPE REF TO lcl_a.\n")
assert "GO_REF" in _blocks(src)
def test_form_signature_params():
"""타입이 붙은 파라미터가 여러 개여도 전부 잡아야 한다."""
from parser.dataflow import extract_declarations
sig = "USING P_DATE LIKE P0001-BEGDA P_DAYS LIKE T5A4A-DLYDY CHANGING P_OUT TYPE ANY"
names = [d.name for d in extract_declarations([], [], "U1", "unit", unit_type="FORM", signature=sig)]
assert names == ["P_DATE", "P_DAYS", "P_OUT"]
# ------------------------------------------------------------------ 조립 (DB)
@pytest.fixture()
def client(tmp_path, monkeypatch):
import query.api as api
monkeypatch.setattr(settings, "database_url", f"sqlite:///{tmp_path / 'index.db'}")
monkeypatch.setattr(settings, "data_normalized", tmp_path / "normalized")
monkeypatch.setattr(settings, "data_parsed", tmp_path / "parsed")
monkeypatch.setattr(settings, "llm_base_url", "")
c = TestClient(api.app)
payload = {
"MAIN_PROGRAM": "ZDECL_T1",
"DESCRIPTION": "정의부 테스트",
"INCLUDE_PROGRAM": [
{"INCLUDE": "ZDECL_T1TOP", "SOURCE_CODE": TOP},
{"INCLUDE": "ZDECL_T1F01", "SOURCE_CODE": FORM_CODE},
],
}
assert c.post("/ingest", json=payload).json()["status"] == "loaded"
return c
def test_program_declarations_endpoint(client):
r = client.get("/programs/ZDECL_T1/declarations").json()
names = {d["name"] for d in r["declarations"]}
assert {"TY_ITEM", "TY_ITEMS", "GT_ITEMS", "GT_LOG", "GC_BWART", "<LS_ITEM>"} <= names
assert r["count"] == len(r["declarations"])
def test_unit_code_carries_declarations(client):
r = client.get("/programs/ZDECL_T1/units/COLLECT_ITEMS/code").json()
picked = [d["name"] for d in r["declarations"]]
# 쓰인 것 + 그 의존까지 (GT_ITEMS → TY_ITEMS → TY_ITEM)
assert {"GT_ITEMS", "TY_ITEMS", "TY_ITEM", "GS_ITEM", "GV_CNT", "GT_LOG", "GC_BWART"} <= set(picked)
# 의존이 먼저 나와야 그대로 붙여넣어 컴파일된다
assert picked.index("TY_ITEM") < picked.index("TY_ITEMS") < picked.index("GT_ITEMS")
# unit 안에서 선언된 로컬 변수는 이미 코드에 있으므로 다시 붙이지 않는다
assert "LV_LOCAL" not in picked
assert "DATA: BEGIN OF gt_log" in r["declaration_code"]
def test_ddic_field_reference_is_not_a_work_area(client):
"""`LIKE BSEG-WRBTR` 은 사전 타입 참조다 — `TABLES: bseg` 를 딸려오게 하면 안 된다."""
from index.db import connect
con = connect()
try:
d = decls_mod.resolve(con, "ZDECL_T1", None, " DATA lv_amt LIKE bseg-wrbtr.")
assert [x["name"] for x in d["declarations"]] == []
assert "BSEG" in d["external_refs"]
# 반면 작업영역을 실제로 쓰면 선언이 필요하다
d2 = decls_mod.resolve(con, "ZDECL_T1", None, " bseg-wrbtr = 100.")
assert [x["name"] for x in d2["declarations"]] == ["BSEG"]
finally:
con.close()
def test_unresolved_reports_missing_declaration(client):
from index.db import connect
con = connect()
try:
d = decls_mod.resolve(con, "ZDECL_T1", None, " gv_missing = 1.")
assert d["unresolved"] == ["GV_MISSING"]
# 호출문의 형식 파라미터·예외 이름은 '선언 없음'이 아니다
d2 = decls_mod.resolve(
con, "ZDECL_T1", None,
" CALL FUNCTION 'Z_X' EXPORTING i_bukrs = gv_cnt EXCEPTIONS no_rate_found = 1.")
assert d2["unresolved"] == []
finally:
con.close()