Initial commit: ABAP indexing pipeline (ingest, parser, summarize, index, query, wiki)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
"""정의부(선언) 수집·조립 — 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()
|
||||
Reference in New Issue
Block a user