from_sap: 펑션 그룹(펑션 모듈 소스)도 받는다 — SAPL<그룹> + INCLUDE 문 추적

- GET_PROGRAM_LIST 는 TADIR 의 PROG 만 주므로 GET_OBJECT_LIST_BY_PACKAGE(FUGR) 로 그룹을 받아
  SAPL<그룹> 을 프로그램처럼 저장 (설명에 펑션 이름·텍스트 목록)
- 펑션 그룹은 서버 INCLUDE_LIST 가 비어 온다(실측) → 소스의 INCLUDE 문을 깊이 4 까지 따라가며
  고객 인클루드(Z/Y/네임스페이스, L<그룹>…)만 받는다. 표준 생성기 인클루드(LSVIM…)는 제외
- 실측 ZFI01: 펑션 그룹 14, SAPLZFG_FI01 인클루드 47 → 파서 FUNCTION 38 unit, 미인식 0
- --no-fugr 로 끌 수 있다

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
byeongwook.choi
2026-09-22 10:29:46 +09:00
co-authored by Claude Fable 5.1
parent 5df8595fe2
commit fdae34161e
2 changed files with 168 additions and 12 deletions
+65 -9
View File
@@ -39,13 +39,41 @@ SOURCES = {
}
FUGR_LIST = {"RETURN": {"TYPE": "S", "MESSAGE": "", "TOTAL_ROWS": 1},
"RESULT": [{"OBJECT": "FUGR", "OBJ_NAME": "ZFG_FI01", "AUTHOR": "U1"}]}
FUNC_LIST = {"RETURN": {"TYPE": "S", "MESSAGE": "", "TOTAL_ROWS": 1},
"RESULT": [{"FUNCNAME": "Z_FI_CHECK", "STEXT": "사업자번호 체크"}]}
# 펑션 그룹: 서버 INCLUDE_LIST 는 비어 오고, 소스의 INCLUDE 문을 따라가야 한다 (실측)
SOURCES["SAPLZFG_FI01"] = {
"RETURN": {"TYPE": "S", "MESSAGE": "", "TOTAL_ROWS": 0}, "PROGRAM": "SAPLZFG_FI01",
"SOURCE_CODE": "* regenerated\nINCLUDE LZFG_FI01TOP.\nINCLUDE LZFG_FI01UXX.\nINCLUDE LSVIMFXX.\n* INCLUDE LZFG_FI01ZZZ.\n",
"INCLUDE_LIST": [], "SCREEN_LIST": [], "TCODE_LIST": [],
}
SOURCES["LZFG_FI01TOP"] = {"RETURN": {"TYPE": "S", "MESSAGE": ""}, "PROGRAM": "LZFG_FI01TOP",
"SOURCE_CODE": "FUNCTION-POOL zfg_fi01.\nINCLUDE lsvimdat.\n", "INCLUDE_LIST": []}
SOURCES["LZFG_FI01UXX"] = {"RETURN": {"TYPE": "S", "MESSAGE": ""}, "PROGRAM": "LZFG_FI01UXX",
"SOURCE_CODE": "INCLUDE LZFG_FI01U01.\n", "INCLUDE_LIST": []}
SOURCES["LZFG_FI01U01"] = {"RETURN": {"TYPE": "S", "MESSAGE": ""}, "PROGRAM": "LZFG_FI01U01",
"SOURCE_CODE": "FUNCTION z_fi_check.\n SELECT SINGLE * FROM t001.\nENDFUNCTION.\n",
"INCLUDE_LIST": []}
CALLED: list[str] = []
def fake_sap_call(method: str, params: dict, timeout: int = 180) -> dict:
CALLED.append(f"{method}:{params.get('IV_PROGRAM') or params.get('IV_OBJ_TYPE') or params.get('IV_FUNC_GROUP') or ''}")
if method == "GET_PROGRAM_LIST":
assert params["IV_PACKAGE"] == "ZFI01"
data = PROGRAM_LIST
elif method == "GET_OBJECT_LIST_BY_PACKAGE":
assert params["IV_OBJ_TYPE"] == "FUGR"
data = FUGR_LIST
elif method == "GET_FUNCTION_LIST":
data = FUNC_LIST
elif method == "GET_PROGRAM_SOURCE":
assert params["IV_WITH_INCLUDE"] == "X"
data = SOURCES[params["IV_PROGRAM"]]
name = params["IV_PROGRAM"]
if name not in SOURCES: # 표준 인클루드(LSVIMFXX 등)는 요청 자체가 오면 안 된다
raise AssertionError(f"요청하면 안 되는 소스: {name}")
data = SOURCES[name]
else:
raise AssertionError(method)
ret = data["RETURN"]
@@ -61,8 +89,9 @@ def sap(monkeypatch):
def test_run_writes_raw_files_and_records_errors(tmp_path, sap):
stats = from_sap.run(["zfi01"], tmp_path)
assert stats == {"packages": 1, "listed": 2, "fetched": 1, "skipped": 0, "errors": 1, "includes": 2}
stats = from_sap.run(["zfi01"], tmp_path, fugr=False)
assert stats == {"packages": 1, "listed": 2, "function_groups": 0, "fetched": 1, "skipped": 0,
"errors": 1, "includes": 2}
# 패키지 목록: normalize 가 package_list 로 분류하는 모양
pkg = parse_collected_file((tmp_path / "ZFI01.txt").read_text(encoding="utf-8"))
@@ -85,16 +114,16 @@ def test_run_writes_raw_files_and_records_errors(tmp_path, sap):
def test_rerun_skips_existing_unless_force(tmp_path, sap):
from_sap.run(["ZFI01"], tmp_path)
again = from_sap.run(["ZFI01"], tmp_path)
from_sap.run(["ZFI01"], tmp_path, fugr=False)
again = from_sap.run(["ZFI01"], tmp_path, fugr=False)
assert again["skipped"] == 1 and again["fetched"] == 0
forced = from_sap.run(["ZFI01"], tmp_path, force=True)
forced = from_sap.run(["ZFI01"], tmp_path, force=True, fugr=False)
assert forced["fetched"] == 1
def test_normalize_consumes_output_and_builds_tcodes(tmp_path, sap):
raw, out = tmp_path / "raw", tmp_path / "normalized"
from_sap.run(["ZFI01"], raw)
from_sap.run(["ZFI01"], raw, fugr=False)
stats = normalize_run(raw, out)
assert stats["programs"] == 1 and stats["package_rows"] == 2 and stats["errors"] == 0
@@ -108,7 +137,7 @@ def test_normalize_consumes_output_and_builds_tcodes(tmp_path, sap):
def test_dry_run_writes_nothing(tmp_path, sap):
stats = from_sap.run(["ZFI01"], tmp_path, dry_run=True)
stats = from_sap.run(["ZFI01"], tmp_path, dry_run=True, fugr=False)
assert stats["listed"] == 2 and stats["fetched"] == 0
assert list(tmp_path.iterdir()) == []
@@ -172,3 +201,30 @@ def test_normalize_survives_non_string_fields_and_bad_file(tmp_path):
rows = [json.loads(l) for l in (out / "packages.jsonl").read_text(encoding="utf-8").splitlines()]
assert rows[0]["changed_on"] == "20240102" and rows[0]["text"] == "" and rows[1]["changed_on"] == ""
assert "ZBAD.txt" in (out / "_errors.log").read_text(encoding="utf-8")
def test_function_groups_are_fetched_via_include_statements(tmp_path, sap):
"""펑션 그룹: FUGR 목록 → SAPL<그룹> 저장, 인클루드는 INCLUDE 문을 따라(표준 인클루드는 제외, 주석은 무시)."""
CALLED.clear()
stats = from_sap.run(["ZFI01"], tmp_path)
assert stats["function_groups"] == 1 and stats["listed"] == 3 and stats["fetched"] == 2
prog = parse_collected_file((tmp_path / "SAPLZFG_FI01.txt").read_text(encoding="utf-8"))
assert prog["MAIN_PROGRAM"] == "SAPLZFG_FI01"
assert prog["DESCRIPTION"] == "펑션 그룹 ZFG_FI01: Z_FI_CHECK (사업자번호 체크)"
assert [i["INCLUDE"] for i in prog["INCLUDE_PROGRAM"]] == [
"SAPLZFG_FI01", "LZFG_FI01TOP", "LZFG_FI01UXX", "LZFG_FI01U01"]
assert "ENDFUNCTION" in prog["INCLUDE_PROGRAM"][3]["SOURCE_CODE"]
assert not any(c.endswith(":LSVIMFXX") or c.endswith(":LSVIMDAT") or c.endswith(":LZFG_FI01ZZZ") for c in CALLED)
# 패키지 목록에도 SAPL 프로그램이 들어간다 (package 테이블 조인용)
pkg = parse_collected_file((tmp_path / "ZFI01.txt").read_text(encoding="utf-8"))
assert any(r["OBJ_NAME"] == "SAPLZFG_FI01" for r in pkg)
def test_is_customer_include():
from ingest.from_sap import is_customer_include
assert is_customer_include("LZFG_FI01U01", "SAPLZFG_FI01")
assert is_customer_include("ZFI_COMMON", "ZFI0000")
assert is_customer_include("/PWC/ZINC", "ZFI0000")
assert not is_customer_include("LSVIMFXX", "SAPLZFG_FI01")
assert not is_customer_include("LZOTHERU01", "SAPLZFG_FI01") # 다른 그룹의 L… 인클루드는 제외
assert not is_customer_include("RSDBRUNT", "ZFI0000")