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:
co-authored by
Claude Fable 5.1
parent
5df8595fe2
commit
fdae34161e
+103
-3
@@ -19,6 +19,9 @@ ZAA_ICF 서비스(ZCL_ZAA_AGENT_API)가 설치돼 있어야 한다.
|
|||||||
INCLUDE_PROGRAM: [{INCLUDE, SOURCE_CODE}], TCODE_LIST: [...]}
|
INCLUDE_PROGRAM: [{INCLUDE, SOURCE_CODE}], TCODE_LIST: [...]}
|
||||||
_errors.log 실패한 프로그램 (계속 진행하고 여기 기록)
|
_errors.log 실패한 프로그램 (계속 진행하고 여기 기록)
|
||||||
|
|
||||||
|
- 펑션 모듈: 프로그램 목록 API 에는 안 나온다(TADIR 의 FUGR). 패키지의 펑션 그룹을 따로 받아
|
||||||
|
그룹 메인 프로그램 SAPL<그룹> 을 프로그램처럼 저장한다 (펑션 소스는 그 인클루드 L<그룹>U01 …).
|
||||||
|
펑션 그룹은 서버 INCLUDE_LIST 가 비어 오므로 소스의 INCLUDE 문을 따라 받는다. --no-fugr 로 끈다.
|
||||||
- SAP 목록 API 는 SUBC 를 생략하면 실행 프로그램(1)과 모듈 풀(M)만 준다. Include 는 각 프로그램의
|
- SAP 목록 API 는 SUBC 를 생략하면 실행 프로그램(1)과 모듈 풀(M)만 준다. Include 는 각 프로그램의
|
||||||
INCLUDE_LIST 로 따라오므로 따로 받지 않는다.
|
INCLUDE_LIST 로 따라오므로 따로 받지 않는다.
|
||||||
- 텍스트 심볼(TEXT-nnn)은 이 API 에 없다 → TEXT_SYMBOL 은 빈 배열 (from_dir 와 같은 한계).
|
- 텍스트 심볼(TEXT-nnn)은 이 API 에 없다 → TEXT_SYMBOL 은 빈 배열 (from_dir 와 같은 한계).
|
||||||
@@ -28,6 +31,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -69,6 +73,56 @@ def list_programs(package: str | None = None, pattern: str | None = None,
|
|||||||
return [r for r in rows if isinstance(r, dict) and r.get("OBJ_NAME")]
|
return [r for r in rows if isinstance(r, dict) and r.get("OBJ_NAME")]
|
||||||
|
|
||||||
|
|
||||||
|
# 소스 안의 INCLUDE 문. 주석줄(* 로 시작)은 제외한다
|
||||||
|
_INCLUDE_STMT = re.compile(r"^(?![ \t]*\*)[ \t]*INCLUDE[ \t]+([A-Za-z0-9_/]+)[ \t]*\.", re.I | re.M)
|
||||||
|
MAX_INCLUDE_DEPTH = 4
|
||||||
|
|
||||||
|
|
||||||
|
def is_customer_include(name: str, program: str) -> bool:
|
||||||
|
"""받아올 인클루드인가 — 고객 개발(Z/Y/네임스페이스)과 그 펑션 그룹의 L<그룹>… 인클루드만.
|
||||||
|
|
||||||
|
SAP 표준(LSVIMFXX, lsvimdat 같은 생성기 인클루드)은 소스가 크고 업무 로직이 아니라 뺀다.
|
||||||
|
"""
|
||||||
|
n = name.upper()
|
||||||
|
if n.startswith(("Z", "Y", "/")):
|
||||||
|
return True
|
||||||
|
p = program.upper()
|
||||||
|
if p.startswith("SAPL") and n.startswith("L" + p[4:]): # SAPLZFG_FI01 → LZFG_FI01TOP / U01 / F01 …
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_includes_from_source(program: str, includes: list[dict], seen: set[str]) -> int:
|
||||||
|
"""INCLUDE 문을 따라 인클루드를 받아 includes 에 덧붙인다 (D010INC 가 비어 있을 때의 대안).
|
||||||
|
|
||||||
|
펑션 그룹(SAPL*)은 서버의 INCLUDE_LIST 가 비어 온다(실측 2026-09-22). 메인 → LxxxUXX → LxxxU01 …
|
||||||
|
처럼 단계가 있어 깊이 MAX_INCLUDE_DEPTH 까지 따라간다. 받은 개수를 돌려준다.
|
||||||
|
"""
|
||||||
|
added = 0
|
||||||
|
frontier = list(includes)
|
||||||
|
for _ in range(MAX_INCLUDE_DEPTH):
|
||||||
|
next_frontier: list[dict] = []
|
||||||
|
for inc in frontier:
|
||||||
|
for ref in _INCLUDE_STMT.findall(inc["SOURCE_CODE"]):
|
||||||
|
ref = ref.upper()
|
||||||
|
if ref in seen or not is_customer_include(ref, program):
|
||||||
|
continue
|
||||||
|
seen.add(ref)
|
||||||
|
res = _call("GET_PROGRAM_SOURCE", {"IV_PROGRAM": ref})
|
||||||
|
code = ((res.get("parsed") or {}).get("SOURCE_CODE") or "") if not res.get("error") else ""
|
||||||
|
if res.get("error") or res["return"]["type"] == "E" or not code.strip():
|
||||||
|
_log(f" include {ref} 못 받음: {res.get('error') or res['return']['message']}")
|
||||||
|
continue
|
||||||
|
item = {"INCLUDE": ref, "SOURCE_CODE": code}
|
||||||
|
includes.append(item)
|
||||||
|
next_frontier.append(item)
|
||||||
|
added += 1
|
||||||
|
if not next_frontier:
|
||||||
|
break
|
||||||
|
frontier = next_frontier
|
||||||
|
return added
|
||||||
|
|
||||||
|
|
||||||
def fetch_program(name: str, description: str = "") -> dict:
|
def fetch_program(name: str, description: str = "") -> dict:
|
||||||
"""GET_PROGRAM_SOURCE(+Include) → 수집 JSON 모양의 payload. 실패하면 RuntimeError."""
|
"""GET_PROGRAM_SOURCE(+Include) → 수집 JSON 모양의 payload. 실패하면 RuntimeError."""
|
||||||
res = _call("GET_PROGRAM_SOURCE", {"IV_PROGRAM": name, "IV_WITH_INCLUDE": "X"})
|
res = _call("GET_PROGRAM_SOURCE", {"IV_PROGRAM": name, "IV_WITH_INCLUDE": "X"})
|
||||||
@@ -95,6 +149,8 @@ def fetch_program(name: str, description: str = "") -> dict:
|
|||||||
continue
|
continue
|
||||||
seen.add(inc_name)
|
seen.add(inc_name)
|
||||||
includes.append({"INCLUDE": inc_name, "SOURCE_CODE": code})
|
includes.append({"INCLUDE": inc_name, "SOURCE_CODE": code})
|
||||||
|
# 서버 INCLUDE_LIST 에 없는 인클루드를 소스의 INCLUDE 문으로 보충 (펑션 그룹은 이 경로로만 받아진다)
|
||||||
|
_resolve_includes_from_source(program, includes, seen)
|
||||||
|
|
||||||
tcodes = [
|
tcodes = [
|
||||||
{"TCODE": (t.get("TCODE") or "").strip().upper(), "TTEXT": t.get("TTEXT") or ""}
|
{"TCODE": (t.get("TCODE") or "").strip().upper(), "TTEXT": t.get("TTEXT") or ""}
|
||||||
@@ -109,6 +165,35 @@ def fetch_program(name: str, description: str = "") -> dict:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def list_function_groups(package: str) -> list[dict]:
|
||||||
|
"""패키지의 펑션 그룹 → [{OBJ_NAME: 'SAPLZFG_FI01', TEXT: '펑션 그룹 ZFG_FI01: FN1, FN2 …'}]
|
||||||
|
|
||||||
|
펑션 모듈 소스는 그룹 메인 프로그램 SAPL<그룹> 의 인클루드(L<그룹>U01 …)에 있다.
|
||||||
|
GET_PROGRAM_LIST 는 TADIR 의 PROG 만 주므로 펑션 그룹(FUGR)은 여기서 따로 받는다.
|
||||||
|
"""
|
||||||
|
res = _call("GET_OBJECT_LIST_BY_PACKAGE", {"IV_PACKAGE": package, "IV_OBJ_TYPE": "FUGR"})
|
||||||
|
if res.get("error"):
|
||||||
|
raise RuntimeError(res["error"])
|
||||||
|
if res["return"]["type"] == "E":
|
||||||
|
raise RuntimeError(f"SAP 오류: {res['return']['message']}")
|
||||||
|
out = []
|
||||||
|
for r in (res.get("parsed") or {}).get("RESULT") or []:
|
||||||
|
if not isinstance(r, dict) or not r.get("OBJ_NAME"):
|
||||||
|
continue
|
||||||
|
group = r["OBJ_NAME"].strip().upper()
|
||||||
|
# 그룹 안의 펑션 이름·설명을 프로그램 설명으로 — 펑션 이름 검색이 프로그램 단위에서도 되게
|
||||||
|
names: list[str] = []
|
||||||
|
fl = _call("GET_FUNCTION_LIST", {"IV_FUNC_GROUP": group})
|
||||||
|
if not fl.get("error"):
|
||||||
|
for f in (fl.get("parsed") or {}).get("RESULT") or []:
|
||||||
|
if isinstance(f, dict) and f.get("FUNCNAME"):
|
||||||
|
text = (f.get("STEXT") or f.get("FUNC_TEXT") or "").strip()
|
||||||
|
names.append(f["FUNCNAME"].strip().upper() + (f" ({text})" if text else ""))
|
||||||
|
desc = f"펑션 그룹 {group}" + (": " + ", ".join(names[:30]) if names else "")
|
||||||
|
out.append({"OBJ_NAME": "SAPL" + group, "TEXT": desc, "SUBC": "F", "UDAT": r.get("UDAT") or ""})
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
def package_rows(package: str, rows: list[dict]) -> list[dict]:
|
def package_rows(package: str, rows: list[dict]) -> list[dict]:
|
||||||
"""프로그램 목록 → normalize 가 packages.jsonl 로 만드는 package_list 모양."""
|
"""프로그램 목록 → normalize 가 packages.jsonl 로 만드는 package_list 모양."""
|
||||||
return [
|
return [
|
||||||
@@ -129,9 +214,11 @@ def _write_json(path: Path, obj) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def run(packages: list[str], out_dir: Path, pattern: str | None = None, subc: str | None = None,
|
def run(packages: list[str], out_dir: Path, pattern: str | None = None, subc: str | None = None,
|
||||||
limit: int | None = None, force: bool = False, dry_run: bool = False) -> dict:
|
limit: int | None = None, force: bool = False, dry_run: bool = False,
|
||||||
|
fugr: bool = True) -> dict:
|
||||||
out_dir.mkdir(parents=True, exist_ok=True)
|
out_dir.mkdir(parents=True, exist_ok=True)
|
||||||
stats = {"packages": 0, "listed": 0, "fetched": 0, "skipped": 0, "errors": 0, "includes": 0}
|
stats = {"packages": 0, "listed": 0, "function_groups": 0, "fetched": 0, "skipped": 0, "errors": 0,
|
||||||
|
"includes": 0}
|
||||||
errors: list[str] = []
|
errors: list[str] = []
|
||||||
|
|
||||||
targets: list[tuple[str, str]] = [] # (program, description)
|
targets: list[tuple[str, str]] = [] # (program, description)
|
||||||
@@ -150,6 +237,17 @@ def run(packages: list[str], out_dir: Path, pattern: str | None = None, subc: st
|
|||||||
continue
|
continue
|
||||||
stats["packages"] += 1
|
stats["packages"] += 1
|
||||||
_log(f" 프로그램 {len(rows)}건")
|
_log(f" 프로그램 {len(rows)}건")
|
||||||
|
if fugr and not pattern:
|
||||||
|
# 펑션 그룹 → SAPL<그룹> 을 프로그램처럼 받는다 (펑션 모듈 소스가 그 인클루드에 있다)
|
||||||
|
try:
|
||||||
|
groups = list_function_groups(pkg)
|
||||||
|
_log(f" 펑션 그룹 {len(groups)}건")
|
||||||
|
stats["function_groups"] += len(groups)
|
||||||
|
rows = rows + groups
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
stats["errors"] += 1
|
||||||
|
errors.append(f"{pkg}\tFUGR_LIST\t{e}")
|
||||||
|
_log(f" ! 펑션 그룹 목록 실패: {e}")
|
||||||
if not dry_run:
|
if not dry_run:
|
||||||
_write_json(out_dir / f"{pkg}.txt", package_rows(pkg, rows))
|
_write_json(out_dir / f"{pkg}.txt", package_rows(pkg, rows))
|
||||||
targets += [(r["OBJ_NAME"].upper(), r.get("TEXT") or "") for r in rows]
|
targets += [(r["OBJ_NAME"].upper(), r.get("TEXT") or "") for r in rows]
|
||||||
@@ -206,6 +304,8 @@ def main() -> None:
|
|||||||
ap.add_argument("--limit", type=int, default=None, help="받을 프로그램 수 상한 (시험용)")
|
ap.add_argument("--limit", type=int, default=None, help="받을 프로그램 수 상한 (시험용)")
|
||||||
ap.add_argument("--force", action="store_true", help="이미 받은 파일도 다시 받는다")
|
ap.add_argument("--force", action="store_true", help="이미 받은 파일도 다시 받는다")
|
||||||
ap.add_argument("--dry-run", action="store_true", help="목록만 보고 저장하지 않는다")
|
ap.add_argument("--dry-run", action="store_true", help="목록만 보고 저장하지 않는다")
|
||||||
|
ap.add_argument("--no-fugr", action="store_true",
|
||||||
|
help="펑션 그룹(SAPL<그룹>, 펑션 모듈 소스)은 받지 않는다 (기본은 같이 받음)")
|
||||||
args = ap.parse_args()
|
args = ap.parse_args()
|
||||||
|
|
||||||
if not args.packages and not args.pattern:
|
if not args.packages and not args.pattern:
|
||||||
@@ -221,7 +321,7 @@ def main() -> None:
|
|||||||
|
|
||||||
out_dir = Path(args.out) if args.out else settings.data_raw
|
out_dir = Path(args.out) if args.out else settings.data_raw
|
||||||
stats = run(args.packages, out_dir, pattern=args.pattern, subc=args.subc,
|
stats = run(args.packages, out_dir, pattern=args.pattern, subc=args.subc,
|
||||||
limit=args.limit, force=args.force, dry_run=args.dry_run)
|
limit=args.limit, force=args.force, dry_run=args.dry_run, fugr=not args.no_fugr)
|
||||||
print(json.dumps(stats, ensure_ascii=False))
|
print(json.dumps(stats, ensure_ascii=False))
|
||||||
if stats["errors"]:
|
if stats["errors"]:
|
||||||
print(f"실패 {stats['errors']}건 → {out_dir / '_errors.log'}", file=sys.stderr)
|
print(f"실패 {stats['errors']}건 → {out_dir / '_errors.log'}", file=sys.stderr)
|
||||||
|
|||||||
+65
-9
@@ -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:
|
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":
|
if method == "GET_PROGRAM_LIST":
|
||||||
assert params["IV_PACKAGE"] == "ZFI01"
|
assert params["IV_PACKAGE"] == "ZFI01"
|
||||||
data = PROGRAM_LIST
|
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":
|
elif method == "GET_PROGRAM_SOURCE":
|
||||||
assert params["IV_WITH_INCLUDE"] == "X"
|
name = params["IV_PROGRAM"]
|
||||||
data = SOURCES[params["IV_PROGRAM"]]
|
if name not in SOURCES: # 표준 인클루드(LSVIMFXX 등)는 요청 자체가 오면 안 된다
|
||||||
|
raise AssertionError(f"요청하면 안 되는 소스: {name}")
|
||||||
|
data = SOURCES[name]
|
||||||
else:
|
else:
|
||||||
raise AssertionError(method)
|
raise AssertionError(method)
|
||||||
ret = data["RETURN"]
|
ret = data["RETURN"]
|
||||||
@@ -61,8 +89,9 @@ def sap(monkeypatch):
|
|||||||
|
|
||||||
|
|
||||||
def test_run_writes_raw_files_and_records_errors(tmp_path, sap):
|
def test_run_writes_raw_files_and_records_errors(tmp_path, sap):
|
||||||
stats = from_sap.run(["zfi01"], tmp_path)
|
stats = from_sap.run(["zfi01"], tmp_path, fugr=False)
|
||||||
assert stats == {"packages": 1, "listed": 2, "fetched": 1, "skipped": 0, "errors": 1, "includes": 2}
|
assert stats == {"packages": 1, "listed": 2, "function_groups": 0, "fetched": 1, "skipped": 0,
|
||||||
|
"errors": 1, "includes": 2}
|
||||||
|
|
||||||
# 패키지 목록: normalize 가 package_list 로 분류하는 모양
|
# 패키지 목록: normalize 가 package_list 로 분류하는 모양
|
||||||
pkg = parse_collected_file((tmp_path / "ZFI01.txt").read_text(encoding="utf-8"))
|
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):
|
def test_rerun_skips_existing_unless_force(tmp_path, sap):
|
||||||
from_sap.run(["ZFI01"], tmp_path)
|
from_sap.run(["ZFI01"], tmp_path, fugr=False)
|
||||||
again = from_sap.run(["ZFI01"], tmp_path)
|
again = from_sap.run(["ZFI01"], tmp_path, fugr=False)
|
||||||
assert again["skipped"] == 1 and again["fetched"] == 0
|
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
|
assert forced["fetched"] == 1
|
||||||
|
|
||||||
|
|
||||||
def test_normalize_consumes_output_and_builds_tcodes(tmp_path, sap):
|
def test_normalize_consumes_output_and_builds_tcodes(tmp_path, sap):
|
||||||
raw, out = tmp_path / "raw", tmp_path / "normalized"
|
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)
|
stats = normalize_run(raw, out)
|
||||||
assert stats["programs"] == 1 and stats["package_rows"] == 2 and stats["errors"] == 0
|
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):
|
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 stats["listed"] == 2 and stats["fetched"] == 0
|
||||||
assert list(tmp_path.iterdir()) == []
|
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()]
|
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 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")
|
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")
|
||||||
|
|||||||
Reference in New Issue
Block a user