"""정의부(선언) 수집·조립 — 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 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", ""} <= 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()