Files
cobol-java-v3/tests/test_jcl_deep.py
T
hangshuo652 bc1d56d1a4 feat: Phase 2 complete — 13 Phases of COBOL type classification and test benchmark
P0.6: gcov infrastructure
P1: extract_structure output expansion (11 new feature fields)
P2: Confusion group rule engine (8 pairs + contradiction + backtrack)
P3: 4-factor confidence calculation + quality gate update
P4: 33+2 COBOL program type test samples (22 files, 7 categories)
P5: parametrized/ test data generation engine
P6: japanese_data.py lookup tables
P7-10: Type-specific test suites (~159 parametrized tests)
P11: Full classification pipeline (classify_program) + orchestrator integration
P12: Documentation (module-interfaces, test-plan v3.0, coverage-matrix)

Architecture decisions:
- classification_pipeline/ merged to hina/pipeline/
- parametrized/ as independent module
- japanese_data.py as root-level file
- hina/__all__ only exports classify_program()

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-19 23:51:55 +08:00

470 lines
14 KiB
Python

"""JC-101~130: Deep JCL parser testing
Covers COND variations, DD statement variants, control statements,
error recovery, tokenization edge cases, and direct data class tests.
"""
import sys, os, tempfile
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
from jcl.parser import parse_jcl, CondParam, JobStep, Job, DDEntry
def _write_jcl(content: str) -> str:
"""Write JCL content to a temp file and return the file path."""
tmp = tempfile.NamedTemporaryFile(
mode="w", suffix=".jcl", delete=False, encoding="utf-8"
)
tmp.write(content)
tmp.close()
return tmp.name
# =====================================================================
# COND variations
# =====================================================================
def test_cond_basic():
"""JC-101: COND=(0,NE) -- basic return-code condition"""
path = _write_jcl("//J JOB\n//S EXEC PGM=P,COND=(0,NE)")
try:
job = parse_jcl(path)
assert job is not None
assert len(job.steps) == 1
step = job.steps[0]
assert step.cond is not None
assert step.cond.code == 0
assert step.cond.operator == "NE"
assert step.cond.step_name is None
finally:
os.unlink(path)
def test_cond_step_specific():
"""JC-102: COND=(0,NE,STEP1) -- step-specific condition
Current parser captures (code, op) only; the trailing step_name
is present in the JCL but not parsed into CondParam.step_name.
"""
path = _write_jcl("//J JOB\n//S EXEC PGM=P,COND=(0,NE,STEP1)")
try:
job = parse_jcl(path)
assert job is not None
assert job.steps[0].cond is not None
assert job.steps[0].cond.code == 0
assert job.steps[0].cond.operator == "NE"
# step_name is not parsed by the current regex
assert job.steps[0].cond.step_name is None
finally:
os.unlink(path)
def test_cond_even():
"""JC-103: COND=EVEN -- execute even if prior step fails
Current parser does not recognise the EVEN keyword;
cond remains None.
"""
path = _write_jcl("//J JOB\n//S EXEC PGM=P,COND=EVEN")
try:
job = parse_jcl(path)
assert job is not None
assert job.steps[0].cond is None
finally:
os.unlink(path)
def test_cond_only():
"""JC-104: COND=ONLY -- execute only if prior step fails
Current parser does not recognise the ONLY keyword;
cond remains None.
"""
path = _write_jcl("//J JOB\n//S EXEC PGM=P,COND=ONLY")
try:
job = parse_jcl(path)
assert job is not None
assert job.steps[0].cond is None
finally:
os.unlink(path)
def test_cond_compound():
"""JC-105: COND=((0,NE),(4,GT)) -- compound condition
Current parser's regex looks for a single parenthesised pair;
nested outer parens cause the match to fail, leaving cond=None.
"""
path = _write_jcl("//J JOB\n//S EXEC PGM=P,COND=((0,NE),(4,GT))")
try:
job = parse_jcl(path)
assert job is not None
assert job.steps[0].cond is None
finally:
os.unlink(path)
def test_cond_no_parens():
"""JC-106: COND=0 -- condition without parentheses
Current parser requires parentheses around (code,op);
bare COND=0 does not match and cond is None.
"""
path = _write_jcl("//J JOB\n//S EXEC PGM=P,COND=0")
try:
job = parse_jcl(path)
assert job is not None
assert job.steps[0].cond is None
finally:
os.unlink(path)
# =====================================================================
# DD statement variations
# =====================================================================
def test_dd_dsn_only():
"""JC-107: DD with DSN only"""
path = _write_jcl("//J JOB\n//S EXEC PGM=P\n//DD1 DD DSN=MY.DATA")
try:
job = parse_jcl(path)
assert job is not None
assert len(job.steps[0].dd_entries) == 1
assert job.steps[0].dd_entries[0].dsn == "MY.DATA"
finally:
os.unlink(path)
def test_dd_dsn_disp():
"""JC-108: DD with DSN + DISP
Current parser extracts DSN but does not parse DISP;
the disp field remains None.
"""
path = _write_jcl("//J JOB\n//S EXEC PGM=P\n//DD1 DD DSN=MY.DATA,DISP=SHR")
try:
job = parse_jcl(path)
assert job is not None
dd = job.steps[0].dd_entries[0]
assert dd.dsn == "MY.DATA"
# disp is declared on DDEntry but not yet populated by the parser
assert dd.disp is None
finally:
os.unlink(path)
def test_dd_unit_vol():
"""JC-109: DD with UNIT + VOL -- attributes not extracted but DD entry created"""
path = _write_jcl("//J JOB\n//S EXEC PGM=P\n//DD1 DD UNIT=SYSDA,VOL=SER=VOL001")
try:
job = parse_jcl(path)
assert job is not None
assert len(job.steps[0].dd_entries) == 1
assert job.steps[0].dd_entries[0].dd_name == "DD1"
finally:
os.unlink(path)
def test_dd_space():
"""JC-110: DD with SPACE -- nested parens in SPACE value do not break parsing"""
path = _write_jcl("//J JOB\n//S EXEC PGM=P\n//DD1 DD SPACE=(CYL,(10,5),RLSE)")
try:
job = parse_jcl(path)
assert job is not None
assert len(job.steps[0].dd_entries) == 1
finally:
os.unlink(path)
def test_dd_dcb():
"""JC-111: DD with DCB -- nested parens in DCB value do not break parsing"""
path = _write_jcl("//J JOB\n//S EXEC PGM=P\n//DD1 DD DCB=(LRECL=80,RECFM=FB)")
try:
job = parse_jcl(path)
assert job is not None
assert len(job.steps[0].dd_entries) == 1
finally:
os.unlink(path)
def test_dd_all_attributes():
"""JC-112: DD with all common attributes combined on one line"""
jcl = (
"//J JOB\n"
"//S EXEC PGM=P\n"
"//DD1 DD DSN=MY.DATA,DISP=SHR,UNIT=SYSDA,"
"VOL=SER=VOL001,SPACE=(CYL,(10,5),RLSE),DCB=(LRECL=80,RECFM=FB)"
)
path = _write_jcl(jcl)
try:
job = parse_jcl(path)
assert job is not None
dd = job.steps[0].dd_entries[0]
assert dd.dsn == "MY.DATA"
finally:
os.unlink(path)
# =====================================================================
# Control statements
# =====================================================================
def test_include_member():
"""JC-113: INCLUDE member silently skipped (not yet parsed)"""
path = _write_jcl("//J JOB\n// INCLUDE MEMBER=MYMEM\n//S EXEC PGM=P")
try:
job = parse_jcl(path)
assert job is not None
# INCLUDE is ignored; only the EXEC step is present
assert len(job.steps) == 1
finally:
os.unlink(path)
def test_jes2_delimiter_inline():
"""JC-114: Inline data delimited by /* (JES2 delimiter)
Current parser recognises SYSIN DD * and captures lines until /*.
"""
path = _write_jcl("//J JOB\n//S EXEC PGM=P\n//SYSIN DD *\nline1\n/*")
try:
job = parse_jcl(path)
assert job is not None
dd = job.steps[0].dd_entries[-1]
assert dd.dd_name == "SYSIN"
assert dd.inline_data == ["line1"]
finally:
os.unlink(path)
def test_proc_call():
"""JC-115: PROC call via EXEC PROC=name
Current EXEC regex only handles PGM=; with PROC=, (?:PGM=)?
matches empty and the first \\w+ after EXEC is "PROC" rather
than the member name. The step is still created.
"""
path = _write_jcl("//J JOB\n//STEP1 EXEC PROC=MYPROC")
try:
job = parse_jcl(path)
assert job is not None
assert job.steps[0].step_name == "STEP1"
finally:
os.unlink(path)
def test_proc_with_parm_override():
"""JC-116: PROC with PARM.C=VAL override"""
path = _write_jcl("//J JOB\n//STEP1 EXEC PROC=MYPROC,PARM.C=VAL")
try:
job = parse_jcl(path)
assert job is not None
assert job.steps[0].step_name == "STEP1"
finally:
os.unlink(path)
# =====================================================================
# Error recovery
# =====================================================================
def test_malformed_bad_keyword():
"""JC-117: Malformed line with unrecognised keyword does not crash"""
path = _write_jcl("//J JOB\n//S EXEC PGM=P\n//DD1 DD BADKEYWORD=XYZ")
try:
job = parse_jcl(path)
assert job is not None
assert len(job.steps[0].dd_entries) == 1
assert job.steps[0].dd_entries[0].dd_name == "DD1"
finally:
os.unlink(path)
def test_continuation_nothing_after():
"""JC-118: Continuation comma followed by a bare // line"""
path = _write_jcl("//J JOB\n//S EXEC PGM=P\n//DD1 DD DSN=A,\n//")
try:
job = parse_jcl(path)
assert job is not None
# The continuation merges the bare // onto the DD line;
# DSN extraction still works because the regex stops at comma.
dd = job.steps[0].dd_entries[0]
assert dd.dsn == "A"
finally:
os.unlink(path)
def test_only_comments_and_blanks():
"""JC-119: File with only comments and blank lines yields None"""
path = _write_jcl("//* THIS IS A COMMENT\n//* ANOTHER COMMENT\n\n")
try:
job = parse_jcl(path)
assert job is None
finally:
os.unlink(path)
def test_tokenization_variable_whitespace():
"""JC-120: Variable whitespace between tokens"""
path = _write_jcl("//J JOB\n//S EXEC PGM=P\n//DD1 DD DSN=MY.DATA")
try:
job = parse_jcl(path)
assert job is not None
assert len(job.steps) == 1
assert job.steps[0].dd_entries[0].dsn == "MY.DATA"
finally:
os.unlink(path)
def test_tokenization_tabs():
"""JC-121: Tab characters instead of spaces"""
path = _write_jcl("//J\tJOB\n//S\tEXEC\tPGM=P\n//DD1\tDD\tDSN=MY.DATA")
try:
job = parse_jcl(path)
assert job is not None
assert len(job.steps) == 1
assert job.steps[0].dd_entries[0].dsn == "MY.DATA"
finally:
os.unlink(path)
# =====================================================================
# Data class direct tests
# =====================================================================
def test_cond_param_direct():
"""JC-122: CondParam with code=0, operator='NE' """
c = CondParam(code=0, operator="NE")
assert c.code == 0
assert c.operator == "NE"
assert c.step_name is None
def test_cond_param_with_step():
"""JC-123: CondParam with step_name set"""
c = CondParam(code=4, operator="GT", step_name="STEP1")
assert c.code == 4
assert c.operator == "GT"
assert c.step_name == "STEP1"
def test_dd_entry_dsn_disp():
"""JC-124: DDEntry with dsn and disp"""
d = DDEntry(dd_name="DD1", dsn="MY.DATA", disp="SHR")
assert d.dd_name == "DD1"
assert d.dsn == "MY.DATA"
assert d.disp == "SHR"
assert d.sysout is None
assert d.inline_data == []
def test_dd_entry_inline_data():
"""JC-125: DDEntry with inline data"""
d = DDEntry(dd_name="SYSIN", inline_data=["line1", "line2"])
assert d.dd_name == "SYSIN"
assert d.inline_data == ["line1", "line2"]
def test_job_steps_append():
"""JC-126: Job with steps list append"""
j = Job("TESTJOB")
assert j.job_name == "TESTJOB"
assert len(j.steps) == 0
j.steps.append(JobStep("S1", "PGM1"))
j.steps.append(JobStep("S2", "PGM2"))
assert len(j.steps) == 2
assert j.steps[0].step_name == "S1"
assert j.steps[0].program == "PGM1"
assert j.steps[1].step_name == "S2"
assert j.steps[1].program == "PGM2"
def test_job_step_cond_dd():
"""JC-127: JobStep with cond and dd_entries lists"""
cond = CondParam(code=0, operator="NE")
dd1 = DDEntry(dd_name="SYSUT1", dsn="INPUT.DATA")
dd2 = DDEntry(dd_name="SYSUT2", dsn="OUTPUT.DATA", disp="OLD")
step = JobStep(step_name="S1", program="PGM1", cond=cond)
step.dd_entries.append(dd1)
step.dd_entries.append(dd2)
assert step.step_name == "S1"
assert step.program == "PGM1"
assert step.cond is not None
assert step.cond.code == 0
assert step.cond.operator == "NE"
assert len(step.dd_entries) == 2
assert step.dd_entries[0].dd_name == "SYSUT1"
assert step.dd_entries[0].dsn == "INPUT.DATA"
assert step.dd_entries[1].dd_name == "SYSUT2"
assert step.dd_entries[1].dsn == "OUTPUT.DATA"
assert step.dd_entries[1].disp == "OLD"
# =====================================================================
# Additional edge cases
# =====================================================================
def test_multi_step_with_cond():
"""JC-128: Multiple steps, each with a condition"""
path = _write_jcl(
"//J JOB\n"
"//STEP1 EXEC PGM=PGM1,COND=(0,NE)\n"
"//STEP2 EXEC PGM=PGM2,COND=(4,GT)\n"
"//STEP3 EXEC PGM=PGM3"
)
try:
job = parse_jcl(path)
assert job is not None
assert len(job.steps) == 3
assert job.steps[0].step_name == "STEP1"
assert job.steps[0].cond is not None
assert job.steps[0].cond.code == 0
assert job.steps[0].cond.operator == "NE"
assert job.steps[1].cond is not None
assert job.steps[1].cond.code == 4
assert job.steps[1].cond.operator == "GT"
assert job.steps[2].cond is None
finally:
os.unlink(path)
def test_dd_multiple_entries():
"""JC-129: Multiple DD entries under one step"""
path = _write_jcl(
"//J JOB\n"
"//S EXEC PGM=P\n"
"//DD1 DD DSN=IN.DATA,DISP=SHR\n"
"//DD2 DD DSN=OUT.DATA,DISP=OLD\n"
"//DD3 DD DUMMY\n"
)
try:
job = parse_jcl(path)
assert job is not None
assert len(job.steps[0].dd_entries) == 3
assert job.steps[0].dd_entries[0].dd_name == "DD1"
assert job.steps[0].dd_entries[0].dsn == "IN.DATA"
assert job.steps[0].dd_entries[1].dd_name == "DD2"
assert job.steps[0].dd_entries[1].dsn == "OUT.DATA"
assert job.steps[0].dd_entries[2].dd_name == "DD3"
assert job.steps[0].dd_entries[2].dsn is None
finally:
os.unlink(path)
def test_cond_even_only_not_captured():
"""JC-130: COND=EVEN and COND=ONLY -- explicit check that cond is None"""
path = _write_jcl(
"//J JOB\n"
"//S1 EXEC PGM=P,COND=EVEN\n"
"//S2 EXEC PGM=P,COND=ONLY"
)
try:
job = parse_jcl(path)
assert job is not None
assert len(job.steps) == 2
assert job.steps[0].step_name == "S1"
assert job.steps[0].cond is None # EVEN not parsed
assert job.steps[1].step_name == "S2"
assert job.steps[1].cond is None # ONLY not parsed
finally:
os.unlink(path)