From 6dc5364acf88cd5d86f4e767a6c7da84f45a16a0 Mon Sep 17 00:00:00 2001 From: squid-protocol Date: Wed, 7 Oct 2026 08:17:05 -0400 Subject: [PATCH 1/4] spec: generated CicsSpec.java + ggcics_spec.h, drift test (#4270, spec PR 4) cics_command_spec.md section 7, PR 4: the runtimes' RESP and default-abend tables are generated from gitgalaxy/standards/cics and committed; `python -m gitgalaxy.standards.cics regen` rewrites them and `regen --check` (the new cics-spec ratchet in pr_gates.py --ratchets, plus a pytest drift test) fails when one is stale. - gitgalaxy/tools/cobol_to_java/CicsSpec.java (package .cics): name / condition / resp / abcodeFor. The transaction forge emits it beside CicsTask (a forge-only project has no cobolrt); CicsTask.respName / abcodeFor and DetCics.condition / resp delegate to it. - tests/equivalence/cics/ggcics_spec.h: DFHRESP_ and condition_abcode. ggcics.c includes it, defines its short condition names from it and drops its own condition_abcode. - Deletes the PR 4 transitional equality tests; new tests check the runtimes keep no hand copy. Behaviour is unchanged on every proven path; the one difference is that a RESP name / number the old partial switches lacked now resolves instead of throwing. Refusal constants, stated facts and the runner's CONDITION_ABCODE stay for PR 5. Part of #4270. Co-Authored-By: Claude Opus 5.5 --- docs/language_status/cics_command_spec.md | 12 +- gitgalaxy/standards/cics/cli.py | 139 +++++++--- gitgalaxy/tools/cobol_to_java/CicsSpec.java | 253 ++++++++++++++++++ .../cobol_to_java_transaction_forge.py | 58 ++-- .../det/cobolrt/cics/DetCics.java | 56 +--- pyproject.toml | 2 + tests/cics_crucible/test_cics_runtimes.py | 7 +- .../test_cics_spec_equality.py | 60 +---- tests/cobol_mainframe/test_det_translate.py | 22 +- .../test_facade_layout_4449.py | 2 + tests/core_engine/test_cics_spec.py | 53 +++- tests/equivalence/cics/ggcics.c | 33 +-- tests/equivalence/cics/ggcics_spec.h | 131 +++++++++ tests/tools/pr_gates.py | 11 +- 14 files changed, 620 insertions(+), 219 deletions(-) create mode 100644 gitgalaxy/tools/cobol_to_java/CicsSpec.java create mode 100644 tests/equivalence/cics/ggcics_spec.h diff --git a/docs/language_status/cics_command_spec.md b/docs/language_status/cics_command_spec.md index 7f116872f9..8c96883cf9 100644 --- a/docs/language_status/cics_command_spec.md +++ b/docs/language_status/cics_command_spec.md @@ -1,8 +1,16 @@ # Design: one declarative CICS command spec (#4270) **Status: design accepted with the owner's decisions (2026-10-06, section 9); PR 1 (the package, its CLI and the -transitional equality tests) merged (#4589); PR 2 (the translator reads the spec; 45 full entries, 206 name-only) in -review.** All line numbers are against origin/main `b6c3e4ea8`. +transitional equality tests) merged (#4589); PR 2 (the translator reads the spec; 45 full entries, 206 name-only) +merged (#4591); PR 3 (the stub translator reads the spec) merged (#4593); PR 4 (the generated, committed runtime +tables) in review.** All line numbers are against origin/main `b6c3e4ea8`. + +PR 4 as built: `python -m gitgalaxy.standards.cics regen` writes `gitgalaxy/tools/cobol_to_java/CicsSpec.java` +(package `.cics`, emitted by the transaction forge beside `CicsTask`, since a forge-only project has no +`cobolrt`; `DetCics` imports it from there) and `tests/equivalence/cics/ggcics_spec.h` (`DFHRESP_`, +`condition_abcode`; `ggcics.c`'s short condition names are defined from it). `regen --check` is the `cics-spec` +ratchet. Only the RESP and abend tables are generated so far; the refusal constants and the stated facts are PR 5's +(`cics_crucible.CONDITION_ABCODE` too: it is the runner's table, not a runtime's). Every #4270 slice teaches the det port one more EXEC CICS command by writing the same facts about that command again in five to seven places, in three languages. The engine pulls CICS facts out of the same commands with its own diff --git a/gitgalaxy/standards/cics/cli.py b/gitgalaxy/standards/cics/cli.py index 99fb52fadf..8de45256a6 100644 --- a/gitgalaxy/standards/cics/cli.py +++ b/gitgalaxy/standards/cics/cli.py @@ -11,10 +11,12 @@ emit [--json] [KEY ...] the entries (all, or the commands named), as a summary or as JSON (never committed) check the spec's own consistency (beyond what each dataclass checks when it is built) - regen --out DIR render the generated runtime tables (CicsSpec.java, ggcics_spec.h) into DIR + regen [--check | --out DIR] rewrite the committed generated runtime tables (gitgalaxy/tools/cobol_to_java/ + CicsSpec.java, tests/equivalence/cics/ggcics_spec.h); --check: exit 1 when one is + stale (the cics-spec ratchet, pr_gates.py --ratchets); --out: render them into DIR -PR 1 commits no generated file: PR 4 commits them beside the runtimes, delegates the hand-written switches to -them and adds the drift test.""" +Spec PR 4 committed them: DetCics.condition / resp, CicsTask.respName / abcodeFor and ggcics.c's condition enums / +condition_abcode delegate to them. Only tables are generated; every runtime's behaviour stays hand-written.""" from __future__ import annotations @@ -74,78 +76,129 @@ def problems() -> list[str]: return out -def render_java(package: str) -> str: - """CicsSpec.java: the RESP / abend switches DetCics.condition / resp, CicsTask.respName / abcodeFor delegate to - (from PR 4).""" - from gitgalaxy.standards.cics import resp +# The committed generated files (spec PR 4), relative to the repository root. CicsSpec.java sits beside the +# transaction forge, which emits it next to CicsTask (package .cics: a forge-only project has no cobolrt), and +# DetCics imports it from there; ggcics_spec.h sits beside ggcics.c, which includes it. +ROOT = Path(__file__).resolve().parents[3] +JAVA_FILE = Path("gitgalaxy/tools/cobol_to_java/CicsSpec.java") +C_FILE = Path("tests/equivalence/cics/ggcics_spec.h") +JAVA_PACKAGE = "__PACKAGE__.cics" # the forge fills __PACKAGE__ in, as it does in CicsTask - def cases(pairs: list[tuple[str, str]]) -> str: - return "\n".join(f" case {k} -> {v};" for k, v in pairs) - names = cases([(str(n), f'"{c}"') for n, c in sorted(resp.RESP_NAME.items())]) - numbers = cases([(f'"{c}"', str(n)) for c, n in resp.DFHRESP.items()]) - abends = cases([(f'"{c}"', f'"{a}"') for c, a in resp.CONDITION_ABEND.items()]) - return f"""// {BANNER} -package {package}; +# Each generated file is a list of sections, one per spec table. A later table (the stated facts of PR 5, the EIB +# facts) is one more renderer appended to JAVA_SECTIONS / C_SECTIONS: the file around it does not change. +def _java_resp() -> str: + from gitgalaxy.standards.cics import resp -/** The CICS command spec's tables (IBM CICS TS, RESP values; the AEIx / AEYx / AEZx abend codes). */ -public final class CicsSpec {{ - private CicsSpec() {{ + names = "\n".join(f' case {n} -> "{c}";' for n, c in sorted(resp.RESP_NAME.items())) + numbers = "\n".join(f' case "{c}" -> {n};' for c, n in resp.DFHRESP.items()) + return f""" /** The condition name of a RESP value (IBM CICS TS, RESP values: DFHRESP; never an alias), null for a number + * DFHRESP has no name for. */ + public static String name(int resp) {{ + return switch (resp) {{ +{names} + default -> null; + }}; }} /** The condition a RESP value names; "RESP" + the number for one DFHRESP has no name for. */ public static String condition(int resp) {{ - return switch (resp) {{ -{names} - default -> "RESP" + resp; - }}; + String name = name(resp); + return name != null ? name : "RESP" + resp; }} - /** The RESP value of a condition. */ + /** The RESP value of a condition (an alias too: DSIDERR is FILENOTFOUND's number). */ public static int resp(String condition) {{ return switch (condition) {{ {numbers} default -> throw new IllegalArgumentException("no RESP value known for condition " + condition); }}; }} +""" + - /** The abend code of an unhandled condition. */ +def _java_abend() -> str: + from gitgalaxy.standards.cics import resp + + abends = "\n".join(f' case "{c}" -> "{a}";' for c, a in resp.CONDITION_ABEND.items()) + return f""" /** The abend code of an unhandled condition (IBM's AEIx / AEYx / AEZx abend codes, the AEIA topic). */ public static String abcodeFor(String condition) {{ return switch (condition) {{ {abends} default -> throw new IllegalArgumentException("no abend code known for condition " + condition); }}; }} -}} """ -def render_c() -> str: - """ggcics_spec.h: the DFHRESP enum and condition_abcode for the stub (from PR 4).""" +def _c_resp() -> str: from gitgalaxy.standards.cics import resp enum = ",\n".join(f" DFHRESP_{c} = {n}" for c, n in resp.DFHRESP.items()) - cases = "\n".join(f' case DFHRESP_{c}: return "{a}";' for c, a in resp.CONDITION_ABEND.items()) - return f"""/* {BANNER} */ -#ifndef GGCICS_SPEC_H -#define GGCICS_SPEC_H - + return f"""/* IBM CICS TS, RESP values (DFHRESP): the condition name -> its RESP number */ enum {{ {enum} }}; +""" + + +def _c_abend() -> str: + from gitgalaxy.standards.cics import resp -/* The abend code of an unhandled condition; "????" for one the spec has none for. */ + cases = "\n".join(f' case DFHRESP_{c}: return "{a}";' for c, a in resp.CONDITION_ABEND.items()) + return f"""/* The abend code of an unhandled condition (IBM's AEIx / AEYx / AEZx abend codes, the AEIA topic); "????" for + one the spec has none for. */ static const char *condition_abcode(int resp) {{ switch (resp) {{ {cases} default: return "????"; }} }} +""" + + +JAVA_SECTIONS = [_java_resp, _java_abend] +C_SECTIONS = [_c_resp, _c_abend] + + +def render_java(package: str = JAVA_PACKAGE) -> str: + """CicsSpec.java: the tables DetCics.condition / resp and CicsTask.respName / abcodeFor delegate to.""" + body = "\n".join(section() for section in JAVA_SECTIONS) + return f"""// {BANNER} +package {package}; + +/** The CICS command spec's tables (gitgalaxy/standards/cics): RESP values and the default abend codes. */ +public final class CicsSpec {{ + private CicsSpec() {{ + }} + +{body}}} +""" + + +def render_c() -> str: + """ggcics_spec.h: the DFHRESP enum (DFHRESP_) and condition_abcode, which ggcics.c includes.""" + body = "\n".join(section() for section in C_SECTIONS) + return f"""/* {BANNER} */ +#ifndef GGCICS_SPEC_H +#define GGCICS_SPEC_H +{body} #endif """ +def generated() -> dict[Path, str]: + """The committed generated files (relative to the repository root) and the text the spec renders for each.""" + return {JAVA_FILE: render_java(), C_FILE: render_c()} + + +def stale(root: Path = ROOT) -> list[Path]: + """The committed generated files that differ from what the spec renders: [] when none is stale.""" + return [rel for rel, text in generated().items() + if not (root / rel).is_file() or (root / rel).read_text(encoding="utf-8") != text] # fmt: skip + + def main(argv: Sequence[str] | None = None) -> int: ap = argparse.ArgumentParser(prog="python -m gitgalaxy.standards.cics", description=__doc__.splitlines()[0]) sub = ap.add_subparsers(dest="cmd", required=True) @@ -153,9 +206,11 @@ def main(argv: Sequence[str] | None = None) -> int: e.add_argument("keys", nargs="*", help="command keys (default: all), e.g. 'GET CONTAINER'") e.add_argument("--json", action="store_true", help="the full entries as JSON") sub.add_parser("check", help="the spec's own consistency") - r = sub.add_parser("regen", help="render the generated runtime tables into a directory") - r.add_argument("--out", type=Path, required=True, help="where to write CicsSpec.java and ggcics_spec.h") - r.add_argument("--java-package", default="cobolrt.cics", help="the generated Java class's package") + r = sub.add_parser("regen", help="rewrite the committed generated runtime tables (or check / render them)") + how = r.add_mutually_exclusive_group() + how.add_argument("--check", action="store_true", help="exit 1, listing them, when a committed file is stale") + how.add_argument("--out", type=Path, help="render CicsSpec.java and ggcics_spec.h into this directory instead") + r.add_argument("--java-package", default=JAVA_PACKAGE, help="with --out: the generated Java class's package") args = ap.parse_args(argv) if args.cmd == "emit": @@ -180,7 +235,19 @@ def main(argv: Sequence[str] | None = None) -> int: n = len(_commands()) print(f"cics spec: {n} commands, {'ok' if not found else f'{len(found)} problem(s)'}") return 1 if found else 0 - if not re.fullmatch(r"[a-z_][a-z0-9_]*(\.[a-z_][a-z0-9_]*)*", args.java_package): + if args.check: + found = stale() + for rel in found: + print(f"stale: {rel}") + print(f"cics spec generated files: {'up to date' if not found else f'{len(found)} stale'}" + + ("; run `python -m gitgalaxy.standards.cics regen` and commit them" if found else "")) # fmt: skip + return 1 if found else 0 + if args.out is None: + for rel, text in generated().items(): + (ROOT / rel).write_text(text, encoding="utf-8") + print(f"wrote {rel}") + return 0 + if not re.fullmatch(r"(__PACKAGE__|[a-z_][a-z0-9_]*)(\.[a-z_][a-z0-9_]*)*", args.java_package): print(f"not a Java package name: {args.java_package}", file=sys.stderr) return 2 args.out.mkdir(parents=True, exist_ok=True) diff --git a/gitgalaxy/tools/cobol_to_java/CicsSpec.java b/gitgalaxy/tools/cobol_to_java/CicsSpec.java new file mode 100644 index 0000000000..951edcaa3c --- /dev/null +++ b/gitgalaxy/tools/cobol_to_java/CicsSpec.java @@ -0,0 +1,253 @@ +// generated from gitgalaxy/standards/cics by `python -m gitgalaxy.standards.cics regen`; do not edit +package __PACKAGE__.cics; + +/** The CICS command spec's tables (gitgalaxy/standards/cics): RESP values and the default abend codes. */ +public final class CicsSpec { + private CicsSpec() { + } + + /** The condition name of a RESP value (IBM CICS TS, RESP values: DFHRESP; never an alias), null for a number + * DFHRESP has no name for. */ + public static String name(int resp) { + return switch (resp) { + case 0 -> "NORMAL"; + case 1 -> "ERROR"; + case 2 -> "RDATT"; + case 3 -> "WRBRK"; + case 4 -> "EOF"; + case 5 -> "EODS"; + case 6 -> "EOC"; + case 7 -> "INBFMH"; + case 8 -> "ENDINPT"; + case 9 -> "NONVAL"; + case 10 -> "NOSTART"; + case 11 -> "TERMIDERR"; + case 12 -> "FILENOTFOUND"; + case 13 -> "NOTFND"; + case 14 -> "DUPREC"; + case 15 -> "DUPKEY"; + case 16 -> "INVREQ"; + case 17 -> "IOERR"; + case 18 -> "NOSPACE"; + case 19 -> "NOTOPEN"; + case 20 -> "ENDFILE"; + case 21 -> "ILLOGIC"; + case 22 -> "LENGERR"; + case 23 -> "QZERO"; + case 24 -> "SIGNAL"; + case 25 -> "QBUSY"; + case 26 -> "ITEMERR"; + case 27 -> "PGMIDERR"; + case 28 -> "TRANSIDERR"; + case 29 -> "ENDDATA"; + case 30 -> "INVTSREQ"; + case 31 -> "EXPIRED"; + case 32 -> "RETPAGE"; + case 33 -> "RTEFAIL"; + case 34 -> "RTESOME"; + case 35 -> "TSIOERR"; + case 36 -> "MAPFAIL"; + case 37 -> "INVERRTERM"; + case 38 -> "INVMPSZ"; + case 39 -> "IGREQID"; + case 40 -> "OVERFLOW"; + case 41 -> "INVLDC"; + case 42 -> "NOSTG"; + case 43 -> "JIDERR"; + case 44 -> "QIDERR"; + case 45 -> "NOJBUFSP"; + case 46 -> "DSSTAT"; + case 47 -> "SELNERR"; + case 48 -> "FUNCERR"; + case 49 -> "UNEXPIN"; + case 50 -> "NOPASSBKRD"; + case 51 -> "NOPASSBKWR"; + case 53 -> "SYSIDERR"; + case 54 -> "ISCINVREQ"; + case 55 -> "ENQBUSY"; + case 56 -> "ENVDEFERR"; + case 57 -> "IGREQCD"; + case 58 -> "SESSIONERR"; + case 59 -> "SYSBUSY"; + case 60 -> "SESSBUSY"; + case 61 -> "NOTALLOC"; + case 62 -> "CBIDERR"; + case 63 -> "INVEXITREQ"; + case 64 -> "INVPARTNSET"; + case 65 -> "INVPARTN"; + case 66 -> "PARTNFAIL"; + case 69 -> "USERIDERR"; + case 70 -> "NOTAUTH"; + case 71 -> "VOLIDERR"; + case 72 -> "SUPPRESSED"; + case 75 -> "RESIDERR"; + case 80 -> "NOSPOOL"; + case 81 -> "TERMERR"; + case 82 -> "ROLLEDBACK"; + case 83 -> "END"; + case 84 -> "DISABLED"; + case 85 -> "ALLOCERR"; + case 86 -> "STRELERR"; + case 87 -> "OPENERR"; + case 88 -> "SPOLBUSY"; + case 89 -> "SPOLERR"; + case 90 -> "NODEIDERR"; + case 91 -> "TASKIDERR"; + case 92 -> "TCIDERR"; + case 93 -> "DSNNOTFOUND"; + case 94 -> "LOADING"; + case 95 -> "MODELIDERR"; + case 96 -> "OUTDESCRERR"; + case 97 -> "PARTNERIDERR"; + case 98 -> "PROFILEIDERR"; + case 99 -> "NETNAMEIDERR"; + case 100 -> "LOCKED"; + case 101 -> "RECORDBUSY"; + case 102 -> "UOWNOTFOUND"; + case 103 -> "UOWLNOTFOUND"; + case 110 -> "CONTAINERERR"; + case 122 -> "CHANNELERR"; + case 123 -> "CCSIDERR"; + case 124 -> "TIMEDOUT"; + case 125 -> "CODEPAGEERR"; + case 126 -> "INCOMPLETE"; + case 127 -> "APPNOTFOUND"; + case 128 -> "BUSY"; + default -> null; + }; + } + + /** The condition a RESP value names; "RESP" + the number for one DFHRESP has no name for. */ + public static String condition(int resp) { + String name = name(resp); + return name != null ? name : "RESP" + resp; + } + + /** The RESP value of a condition (an alias too: DSIDERR is FILENOTFOUND's number). */ + public static int resp(String condition) { + return switch (condition) { + case "NORMAL" -> 0; + case "ERROR" -> 1; + case "RDATT" -> 2; + case "WRBRK" -> 3; + case "EOF" -> 4; + case "EODS" -> 5; + case "EOC" -> 6; + case "INBFMH" -> 7; + case "ENDINPT" -> 8; + case "NONVAL" -> 9; + case "NOSTART" -> 10; + case "TERMIDERR" -> 11; + case "FILENOTFOUND" -> 12; + case "DSIDERR" -> 12; + case "NOTFND" -> 13; + case "DUPREC" -> 14; + case "DUPKEY" -> 15; + case "INVREQ" -> 16; + case "IOERR" -> 17; + case "NOSPACE" -> 18; + case "NOTOPEN" -> 19; + case "ENDFILE" -> 20; + case "ILLOGIC" -> 21; + case "LENGERR" -> 22; + case "QZERO" -> 23; + case "SIGNAL" -> 24; + case "QBUSY" -> 25; + case "ITEMERR" -> 26; + case "PGMIDERR" -> 27; + case "TRANSIDERR" -> 28; + case "ENDDATA" -> 29; + case "INVTSREQ" -> 30; + case "EXPIRED" -> 31; + case "RETPAGE" -> 32; + case "RTEFAIL" -> 33; + case "RTESOME" -> 34; + case "TSIOERR" -> 35; + case "MAPFAIL" -> 36; + case "INVERRTERM" -> 37; + case "INVMPSZ" -> 38; + case "IGREQID" -> 39; + case "OVERFLOW" -> 40; + case "INVLDC" -> 41; + case "NOSTG" -> 42; + case "JIDERR" -> 43; + case "QIDERR" -> 44; + case "NOJBUFSP" -> 45; + case "DSSTAT" -> 46; + case "SELNERR" -> 47; + case "FUNCERR" -> 48; + case "UNEXPIN" -> 49; + case "NOPASSBKRD" -> 50; + case "NOPASSBKWR" -> 51; + case "SYSIDERR" -> 53; + case "ISCINVREQ" -> 54; + case "ENQBUSY" -> 55; + case "ENVDEFERR" -> 56; + case "IGREQCD" -> 57; + case "SESSIONERR" -> 58; + case "SYSBUSY" -> 59; + case "SESSBUSY" -> 60; + case "NOTALLOC" -> 61; + case "CBIDERR" -> 62; + case "INVEXITREQ" -> 63; + case "INVPARTNSET" -> 64; + case "INVPARTN" -> 65; + case "PARTNFAIL" -> 66; + case "USERIDERR" -> 69; + case "NOTAUTH" -> 70; + case "VOLIDERR" -> 71; + case "SUPPRESSED" -> 72; + case "RESIDERR" -> 75; + case "NOSPOOL" -> 80; + case "TERMERR" -> 81; + case "ROLLEDBACK" -> 82; + case "END" -> 83; + case "DISABLED" -> 84; + case "ALLOCERR" -> 85; + case "STRELERR" -> 86; + case "OPENERR" -> 87; + case "SPOLBUSY" -> 88; + case "SPOLERR" -> 89; + case "NODEIDERR" -> 90; + case "TASKIDERR" -> 91; + case "TCIDERR" -> 92; + case "DSNNOTFOUND" -> 93; + case "LOADING" -> 94; + case "MODELIDERR" -> 95; + case "OUTDESCRERR" -> 96; + case "PARTNERIDERR" -> 97; + case "PROFILEIDERR" -> 98; + case "NETNAMEIDERR" -> 99; + case "LOCKED" -> 100; + case "RECORDBUSY" -> 101; + case "UOWNOTFOUND" -> 102; + case "UOWLNOTFOUND" -> 103; + case "CONTAINERERR" -> 110; + case "CHANNELERR" -> 122; + case "CCSIDERR" -> 123; + case "TIMEDOUT" -> 124; + case "CODEPAGEERR" -> 125; + case "INCOMPLETE" -> 126; + case "APPNOTFOUND" -> 127; + case "BUSY" -> 128; + default -> throw new IllegalArgumentException("no RESP value known for condition " + condition); + }; + } + + /** The abend code of an unhandled condition (IBM's AEIx / AEYx / AEZx abend codes, the AEIA topic). */ + public static String abcodeFor(String condition) { + return switch (condition) { + case "NOTFND" -> "AEIM"; + case "LENGERR" -> "AEIV"; + case "ITEMERR" -> "AEIZ"; + case "QIDERR" -> "AEYH"; + case "MAPFAIL" -> "AEI9"; + case "ENDDATA" -> "AEI2"; + case "PGMIDERR" -> "AEI0"; + case "INVREQ" -> "AEIP"; + case "CONTAINERERR" -> "AEZJ"; + case "CHANNELERR" -> "AEZV"; + default -> throw new IllegalArgumentException("no abend code known for condition " + condition); + }; + } +} diff --git a/gitgalaxy/tools/cobol_to_java/cobol_to_java_transaction_forge.py b/gitgalaxy/tools/cobol_to_java/cobol_to_java_transaction_forge.py index bb67d5351c..f6c2d05281 100644 --- a/gitgalaxy/tools/cobol_to_java/cobol_to_java_transaction_forge.py +++ b/gitgalaxy/tools/cobol_to_java/cobol_to_java_transaction_forge.py @@ -216,6 +216,11 @@ def commarea_alternative_todos(commarea: dict) -> list[str]: return out +# #4270 spec PR 4: the CICS command spec's tables (RESP values, abend codes), generated and committed beside this +# file by `python -m gitgalaxy.standards.cics regen`; emitted next to CicsTask, whose respName / abcodeFor (and +# DetCics's condition / resp) delegate to it. +CICS_SPEC_JAVA = Path(__file__).with_name("CicsSpec.java").read_text(encoding="utf-8") + # #3754: one CICS task, the runtime a program's runTask is written against. CICS_TASK_JAVA = """package __PACKAGE__.cics; @@ -2389,47 +2394,20 @@ def commarea_alternative_todos(commarea: dict) -> list[str]: return parent == null ? this : parent.root(); } - /** #4049: a DFHRESP number a fault plan names, as the condition's name (IBM CICS "RESP values"). */ + /** #4049: a DFHRESP number a fault plan names, as the condition's name (IBM CICS "RESP values"; the spec's + * table, CicsSpec). */ static String respName(int resp) { - return switch (resp) { - case 0 -> "NORMAL"; - case 11 -> "TERMIDERR"; - case 13 -> "NOTFND"; - case 16 -> "INVREQ"; - case 17 -> "IOERR"; - case 18 -> "NOSPACE"; - case 22 -> "LENGERR"; - case 26 -> "ITEMERR"; - case 27 -> "PGMIDERR"; - case 28 -> "TRANSIDERR"; - case 29 -> "ENDDATA"; - case 44 -> "QIDERR"; - case 53 -> "SYSIDERR"; - case 54 -> "ISCINVREQ"; - case 56 -> "ENVDEFERR"; - case 70 -> "NOTAUTH"; - case 100 -> "LOCKED"; - case 110 -> "CONTAINERERR"; - case 122 -> "CHANNELERR"; - default -> throw new IllegalArgumentException("no condition name known for RESP " + resp); - }; + String name = CicsSpec.name(resp); + if (name == null) { + throw new IllegalArgumentException("no condition name known for RESP " + resp); + } + return name; } - /** The abend code of an unhandled condition (IBM's AEIx / AEYx codes, the AEIA topic). */ + /** The abend code of an unhandled condition (IBM's AEIx / AEYx codes, the AEIA topic; the spec's table, + * CicsSpec). */ public static String abcodeFor(String condition) { - return switch (condition) { - case "NOTFND" -> "AEIM"; - case "LENGERR" -> "AEIV"; - case "ITEMERR" -> "AEIZ"; - case "QIDERR" -> "AEYH"; - case "MAPFAIL" -> "AEI9"; - case "ENDDATA" -> "AEI2"; - case "PGMIDERR" -> "AEI0"; - case "INVREQ" -> "AEIP"; - case "CONTAINERERR" -> "AEZJ"; // #4270: IBM abend codes AEZJ "CONTAINERERR condition not handled" - case "CHANNELERR" -> "AEZV"; // and AEZV "CHANNELERR condition not handled" - default -> throw new IllegalArgumentException("no abend code known for condition " + condition); - }; + return CicsSpec.abcodeFor(condition); } private void record(String code, String cause, String condition, String program, String label) { @@ -3151,10 +3129,12 @@ def _endpoint(method: str, svc: str, call: str, arg: str | None, req: str | None def runtime_sources(self) -> dict[str, str]: """#3754: CicsTask (package .cics), when there is a CICS program to run as a task (#4004: or at a - LINK / XCTL level); #4343: and CicsRegion, the deployment's region the programs' facades run their tasks in.""" + LINK / XCTL level), with CicsSpec (#4270 spec PR 4: its tables); #4343: and CicsRegion, the deployment's + region the programs' facades run their tasks in.""" if not self.programs: return {} - out = {"CicsTask": CICS_TASK_JAVA.replace("__PACKAGE__", self.package).replace("__ZONE__", self.zone)} + out = {"CicsTask": CICS_TASK_JAVA.replace("__PACKAGE__", self.package).replace("__ZONE__", self.zone), + "CicsSpec": CICS_SPEC_JAVA.replace("__PACKAGE__", self.package)} # fmt: skip if self.target.features.services: # the region runs the programs' services out["CicsRegion"] = self.region_source() return out diff --git a/gitgalaxy/tools/cobol_to_java/det/cobolrt/cics/DetCics.java b/gitgalaxy/tools/cobol_to_java/det/cobolrt/cics/DetCics.java index 1f2fce58fe..10739beeaa 100644 --- a/gitgalaxy/tools/cobol_to_java/det/cobolrt/cics/DetCics.java +++ b/gitgalaxy/tools/cobol_to_java/det/cobolrt/cics/DetCics.java @@ -1,5 +1,6 @@ package __PACKAGE__.cobolrt.cics; +import __PACKAGE__.cics.CicsSpec; import __PACKAGE__.cics.CicsTask; import __PACKAGE__.cobolrt.Cobol; import __PACKAGE__.cobolrt.Field; @@ -385,61 +386,14 @@ public static String currentDate(java.time.LocalDateTime now) { + String.format(java.util.Locale.ROOT, "%02d", now.getNano() / 10_000_000) + "+0000"; } - /** The condition a RESP value names (IBM CICS TS, RESP values: DFHRESP). */ + /** The condition a RESP value names (IBM CICS TS, RESP values: DFHRESP; the spec's table, CicsSpec). */ public static String condition(int resp) { - return switch (resp) { - case 0 -> "NORMAL"; - case 6 -> "EOC"; - case 11 -> "TERMIDERR"; // #4270 slice 2 - case 12 -> "FILENOTFOUND"; - case 13 -> "NOTFND"; - case 14 -> "DUPREC"; - case 15 -> "DUPKEY"; - case 16 -> "INVREQ"; - case 17 -> "IOERR"; - case 18 -> "NOSPACE"; - case 19 -> "NOTOPEN"; - case 20 -> "ENDFILE"; - case 22 -> "LENGERR"; - case 26 -> "ITEMERR"; - case 27 -> "PGMIDERR"; - case 28 -> "TRANSIDERR"; // #4270 slice 2 - case 29 -> "ENDDATA"; - case 36 -> "MAPFAIL"; - case 44 -> "QIDERR"; - case 56 -> "ENVDEFERR"; // #4270 slice 2 - case 70 -> "NOTAUTH"; - case 84 -> "DISABLED"; - case 110 -> "CONTAINERERR"; // #4270 - case 122 -> "CHANNELERR"; - default -> "RESP" + resp; - }; + return CicsSpec.condition(resp); } - /** The RESP value of a condition the runtime names (IBM CICS TS, RESP values). */ + /** The RESP value of a condition (IBM CICS TS, RESP values; the spec's table, CicsSpec). */ public static int resp(String condition) { - return switch (condition) { - case "NORMAL" -> 0; - case "EOC" -> 6; - case "TERMIDERR" -> 11; // #4270 slice 2 - case "NOTFND" -> 13; - case "INVREQ" -> 16; - case "IOERR" -> 17; - case "LENGERR" -> 22; - case "ITEMERR" -> 26; - case "PGMIDERR" -> 27; - case "TRANSIDERR" -> 28; // #4270 slice 2 - case "ENDDATA" -> 29; - case "QIDERR" -> 44; - case "ENVDEFERR" -> 56; - case "NOTAUTH" -> 70; - case "SYSIDERR" -> 53; - case "TERMERR" -> 81; - case "ROLLEDBACK" -> 82; - case "CONTAINERERR" -> 110; // #4270 - case "CHANNELERR" -> 122; - default -> throw new IllegalArgumentException("no RESP value known for condition " + condition); - }; + return CicsSpec.resp(condition); } /** #4413: a condition whose default action -- with no HANDLE CONDITION label for it -- is to ignore it, the diff --git a/pyproject.toml b/pyproject.toml index f1998ecced..cb8a3d2fb7 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -130,6 +130,8 @@ exclude = [ "gitgalaxy.core" = ["ebcdic_dbcs/*.json"] # #3816: the mixed CJK EBCDIC tables # det-port: the Java runtime a deterministic port is overlaid with, and the CICS copybooks it lays out "gitgalaxy.tools.cobol_to_java.det" = ["cobolrt/*.java", "cobolrt/*/*.java", "copy/*.cpy"] +# #4270 spec PR 4: the CICS spec's generated tables, emitted beside CicsTask +"gitgalaxy.tools.cobol_to_java" = ["CicsSpec.java"] [tool.setuptools_scm] # This tells it to expect tags like "v2.0.5" instead of just "2.0.5" diff --git a/tests/cics_crucible/test_cics_runtimes.py b/tests/cics_crucible/test_cics_runtimes.py index a2ed32d904..ee88c4826d 100644 --- a/tests/cics_crucible/test_cics_runtimes.py +++ b/tests/cics_crucible/test_cics_runtimes.py @@ -95,12 +95,13 @@ def test_the_stub_receive_returns_the_typed_text_once_and_truncates_with_lengerr # ---- the generated CicsTask ------------------------------------------------------------------------------ def _cics_task(tmp_path: Path, main_body: str) -> str: """Compile the generated CicsTask with a main whose body is `main_body`; its stdout.""" - from gitgalaxy.tools.cobol_to_java.cobol_to_java_transaction_forge import CICS_TASK_JAVA + from gitgalaxy.tools.cobol_to_java.cobol_to_java_transaction_forge import CICS_SPEC_JAVA, CICS_TASK_JAVA pkg = tmp_path / "src" / "t" / "cics" pkg.mkdir(parents=True) runtime = CICS_TASK_JAVA.replace("__PACKAGE__", "t").replace("__ZONE__", "UTC") (pkg / "CicsTask.java").write_text(runtime, encoding="utf-8") + (pkg / "CicsSpec.java").write_text(CICS_SPEC_JAVA.replace("__PACKAGE__", "t"), encoding="utf-8") (pkg / "Main.java").write_text("package t.cics;\n\npublic class Main {\n public static void main(String[] a) {\n" + main_body + "\n }\n}\n", encoding="utf-8") # fmt: skip classes = tmp_path / "classes" @@ -591,11 +592,11 @@ def test_the_stub_handles_conditions_and_abends_as_cics_does(tmp_path): def test_the_stub_and_the_runner_agree_on_condition_abend_codes(): - src = STUB.read_text(encoding="utf-8") + src = STUB.with_name("ggcics_spec.h").read_text(encoding="utf-8") # #4270 spec PR 4: the stub's condition_abcode import cics_crucible as runner import equivalence_cics as ec - table = dict(re.findall(r'case (\w+): return "(\w{4})";', src)) + table = dict(re.findall(r'case DFHRESP_(\w+): return "(\w{4})";', src)) assert {c: table[c] for c in runner.CONDITION_ABCODE} == runner.CONDITION_ABCODE assert all(c in ec.DFHRESP for c in table) diff --git a/tests/cobol_mainframe/test_cics_spec_equality.py b/tests/cobol_mainframe/test_cics_spec_equality.py index e3d10df2fe..5c12de3092 100644 --- a/tests/cobol_mainframe/test_cics_spec_equality.py +++ b/tests/cobol_mainframe/test_cics_spec_equality.py @@ -1,4 +1,4 @@ -"""#4270 spec PRs 1-2: TRANSITIONAL equality tests -- gitgalaxy/standards/cics equals every hand copy it will replace. +"""#4270 spec PRs 1-5: TRANSITIONAL equality tests -- gitgalaxy/standards/cics equals every hand copy it will replace. docs/language_status/cics_command_spec.md section 7. Each test names the copy it reads and the spec PR that makes that copy import the spec; that PR DELETES the test (the copy is gone, so there is nothing left to compare): @@ -8,9 +8,10 @@ PR 3 tests/tools/equivalence_cics.py (DFHRESP, CICS_RESP, _CONTAINER_OPTIONS, the refused tuples / sets): DONE, the harness imports them; tests/cobol_mainframe/test_equivalence_cics.py checks its refusals are the spec's PR 4 the Java switches (DetCics.condition / resp, CicsTask.respName / abcodeFor), ggcics.c's enums and - condition_abcode - PR 5 the stated facts (CicsTask.withX, $GGCICS_X, cics_crucible.REGION_*) and the runtime refusal texts - PR 4/5 cics_crucible.CONDITION_ABCODE + condition_abcode: DONE, they delegate to the generated CicsSpec.java / ggcics_spec.h, which the drift test + (tests/core_engine/test_cics_spec.py, the cics-spec ratchet) holds to the spec + PR 5 the stated facts (CicsTask.withX, $GGCICS_X, cics_crucible.REGION_*), the runtime refusal texts and + cics_crucible.CONDITION_ABCODE (the runner's table; spec PR 4 changed only the runtimes) PR 8a the engine's verb tables (core/cics_resources.py, core/cics_tasks.py) Where today's copies disagree with each other, the spec does not silently pick one: the difference is listed here @@ -19,7 +20,6 @@ from __future__ import annotations -import re import sys from pathlib import Path @@ -40,58 +40,10 @@ DETCICS_JAVA = (ROOT / "gitgalaxy/tools/cobol_to_java/det/cobolrt/cics/DetCics.java").read_text(encoding="utf-8") GGCICS_C = (ROOT / "tests/equivalence/cics/ggcics.c").read_text(encoding="utf-8") JAVA = forge.CICS_TASK_JAVA + DETCICS_JAVA -DFHRESP = spec_resp.DFHRESP - - -def _switch(src: str, method: str, key: str, value: str) -> dict[str, str]: - """The `case K -> V;` arms of a Java switch method, as text.""" - m = re.search(rf"\b{method}\([^)]*\)\s*\{{\s*return switch[^{{]*\{{(.*?)\n\s*\}};", src, re.S) - assert m, f"no switch in {method}" - return dict(re.findall(rf"case ({key}) -> ({value});", m.group(1))) - - -# ---- DFHRESP and abend codes ------------------------------------------------------------------------------------ -def test_detcics_condition_and_resp_switches_are_spec_values(): - """Deleted in PR 4 (DetCics.condition / resp delegate to the generated CicsSpec.java). Partial tables by - design (the conditions the runtime raises); every arm must be the spec's.""" - names = _switch(DETCICS_JAVA, "condition", r"\d+", r'"\w+"') - assert {int(k): v.strip('"') for k, v in names.items()} == {int(k): spec_resp.RESP_NAME[int(k)] for k in names} - assert 'default -> "RESP" + resp;' in DETCICS_JAVA # the generated CicsSpec.condition's default too - numbers = _switch(DETCICS_JAVA, "resp", r'"\w+"', r"\d+") - assert {k.strip('"'): int(v) for k, v in numbers.items()} == {k.strip('"'): DFHRESP[k.strip('"')] for k in numbers} - assert len(names) == 24 and len(numbers) == 19 # (today's sizes: a new arm must be a spec value too) - - -def test_cicstask_resp_name_and_abcode_switches_are_spec_values(): - """Deleted in PR 4 (CicsTask.respName / abcodeFor delegate to CicsSpec.java).""" - names = _switch(forge.CICS_TASK_JAVA, "respName", r"\d+", r'"\w+"') - assert {int(k): v.strip('"') for k, v in names.items()} == {int(k): spec_resp.RESP_NAME[int(k)] for k in names} - abends = _switch(forge.CICS_TASK_JAVA, "abcodeFor", r'"\w+"', r'"\w+"') - assert {k.strip('"'): v.strip('"') for k, v in abends.items()} == dict(spec_resp.CONDITION_ABEND) - - -# enums in ggcics.c that are not DFHRESP conditions -_C_NOT_CONDITIONS = {"ERRCOND", "RUNNING", "DONE", "XCTLED"} - - -def test_ggcics_condition_enums_and_abend_codes_are_spec_values(): - """Deleted in PR 4 (ggcics.c includes the generated ggcics_spec.h). The stub's enums are spread over the file - (one per command family that added a condition); every one of them must be the spec's number.""" - enums: dict[str, int] = {} - for body in re.findall(r"enum\s*\{([^}]*)\}", GGCICS_C): - for name, value in re.findall(r"(\w+)\s*=\s*(\d+)", body): - assert name not in enums, f"{name}: two enums" - enums[name] = int(value) - conditions = {k: v for k, v in enums.items() if k not in _C_NOT_CONDITIONS} - assert conditions == {k: DFHRESP[k] for k in conditions} - assert len(conditions) == 19 - abcode = re.search(r"condition_abcode\(int resp\)\s*\{(.*?)\n\}", GGCICS_C, re.S) - assert abcode - assert dict(re.findall(r'case (\w+): return "(\w{4})";', abcode.group(1))) == dict(spec_resp.CONDITION_ABEND) def test_crucible_runner_condition_abcode_equals_the_spec(): - """Deleted when cics_crucible.py imports the spec's table (PR 4 / 5). (crucible_events.ABEND_FOR is the + """Deleted when cics_crucible.py imports the spec's table (PR 5). (crucible_events.ABEND_FOR is the oracle's own table, never compared here: section 6; PR 6's report-only cross-check lists AEZJ / AEZV.)""" assert runner.CONDITION_ABCODE == dict(spec_resp.CONDITION_ABEND) diff --git a/tests/cobol_mainframe/test_det_translate.py b/tests/cobol_mainframe/test_det_translate.py index a754ec6b8a..6b914c1306 100644 --- a/tests/cobol_mainframe/test_det_translate.py +++ b/tests/cobol_mainframe/test_det_translate.py @@ -961,7 +961,10 @@ def test_eoc_is_ignored_by_default_and_handled_like_any_condition(): assert ( body.index("handlers.get(cond)") < body.index("DetCics.ignoredByDefault(cond)") < body.index("abendOnCondition") ) - rt = (Path(C.__file__).parent / "cobolrt/cics/DetCics.java").read_text(encoding="utf-8") + from gitgalaxy.tools.cobol_to_java.cobol_to_java_transaction_forge import CICS_SPEC_JAVA + + # DetCics.condition / resp delegate to the generated CicsSpec (#4270 spec PR 4) + rt = (Path(C.__file__).parent / "cobolrt/cics/DetCics.java").read_text(encoding="utf-8") + CICS_SPEC_JAVA assert 'case 6 -> "EOC";' in rt and 'case "EOC" -> 6;' in rt assert re.search(r'ignoredByDefault\(String condition\) \{\s*return "EOC"\.equals\(condition\);', rt) @@ -1118,11 +1121,12 @@ def test_what_the_container_commands_do_not_model_is_refused_by_name(text, why): def test_the_container_conditions_are_known_by_resp_and_abend_code(): """IBM: CONTAINERERR is RESP 110 (abend AEZJ, "CONTAINERERR condition not handled"), CHANNELERR 122 (AEZV).""" assert C.DFHRESP["CONTAINERERR"] == 110 and C.DFHRESP["CHANNELERR"] == 122 - rt = (Path(C.__file__).parent / "cobolrt/cics/DetCics.java").read_text(encoding="utf-8") - assert 'case 110 -> "CONTAINERERR";' in rt and 'case "CHANNELERR" -> 122;' in rt - from gitgalaxy.tools.cobol_to_java import cobol_to_java_transaction_forge as F + from gitgalaxy.tools.cobol_to_java.cobol_to_java_transaction_forge import CICS_SPEC_JAVA - assert 'case "CONTAINERERR" -> "AEZJ";' in F.CICS_TASK_JAVA and 'case "CHANNELERR" -> "AEZV";' in F.CICS_TASK_JAVA + # DetCics.condition / resp delegate to the generated CicsSpec (#4270 spec PR 4) + rt = (Path(C.__file__).parent / "cobolrt/cics/DetCics.java").read_text(encoding="utf-8") + CICS_SPEC_JAVA + assert 'case 110 -> "CONTAINERERR";' in rt and 'case "CHANNELERR" -> 122;' in rt + assert 'case "CONTAINERERR" -> "AEZJ";' in CICS_SPEC_JAVA and 'case "CHANNELERR" -> "AEZV";' in CICS_SPEC_JAVA # ---- #4270 slice 2: interval control ----------------------------------------------------------------------------- @@ -1205,7 +1209,10 @@ def test_run_transid_puts_the_child_token_and_takes_its_outcome(): def test_the_interval_conditions_are_known_by_resp(): """IBM RESP values: TERMIDERR 11, IOERR 17, TRANSIDERR 28, ENDDATA 29, ENVDEFERR 56 -- both ways in DetCics.""" - rt = (Path(C.__file__).parent / "cobolrt/cics/DetCics.java").read_text(encoding="utf-8") + from gitgalaxy.tools.cobol_to_java.cobol_to_java_transaction_forge import CICS_SPEC_JAVA + + # DetCics.condition / resp delegate to the generated CicsSpec (#4270 spec PR 4) + rt = (Path(C.__file__).parent / "cobolrt/cics/DetCics.java").read_text(encoding="utf-8") + CICS_SPEC_JAVA for name in ("TERMIDERR", "TRANSIDERR", "ENDDATA", "ENVDEFERR"): assert f'case {C.DFHRESP[name]} -> "{name}";' in rt and f'case "{name}" -> {C.DFHRESP[name]};' in rt assert 'case "IOERR" -> 17;' in rt @@ -1672,11 +1679,12 @@ def test_det_cics_moves_ts_bytes_between_pages_strictly(tmp_path): other's 256 characters), NL (X'15') as NEL -- the COBOL side's cp037, not the JDK's LF; the same page is the bytes as they are; a byte one page cannot carry, or part of a multi-byte character, stops the run by name -- never a substituted '?'. gitgalaxy.cics.charset names another region page.""" - from gitgalaxy.tools.cobol_to_java.cobol_to_java_transaction_forge import CICS_TASK_JAVA + from gitgalaxy.tools.cobol_to_java.cobol_to_java_transaction_forge import CICS_SPEC_JAVA, CICS_TASK_JAVA from gitgalaxy.tools.cobol_to_java.det import program as P src = tmp_path / "src" files = {"t/cics/CicsTask.java": CICS_TASK_JAVA.replace("__PACKAGE__", "t").replace("__ZONE__", "UTC"), + "t/cics/CicsSpec.java": CICS_SPEC_JAVA.replace("__PACKAGE__", "t"), **{f"t/{k}": v for k, v in P.runtime_files("t", batch=False).items()}} # fmt: skip files["Main.java"] = """ import java.nio.charset.Charset; diff --git a/tests/cobol_mainframe/test_facade_layout_4449.py b/tests/cobol_mainframe/test_facade_layout_4449.py index 755db99583..7fcd6304b1 100644 --- a/tests/cobol_mainframe/test_facade_layout_4449.py +++ b/tests/cobol_mainframe/test_facade_layout_4449.py @@ -17,6 +17,7 @@ from gitgalaxy.tools.cobol_to_java.cobol_to_java_repository_forge import RepositoryForge from gitgalaxy.tools.cobol_to_java.cobol_to_java_transaction_forge import ( + CICS_SPEC_JAVA, CICS_TASK_JAVA, CicsForge, commarea_codec, @@ -194,6 +195,7 @@ class matches; a class with no layout, or a LENGTH short of the record, is refus root = tmp_path / "src" / "com" / "acme" files = { "cics/CicsTask.java": CICS_TASK_JAVA.replace("__PACKAGE__", "com.acme").replace("__ZONE__", "UTC"), + "cics/CicsSpec.java": CICS_SPEC_JAVA.replace("__PACKAGE__", "com.acme"), "entity/vsam/CobolRecords.java": RepositoryForge({}, {}, "com.acme", target).records_source(needed=True), **{f"dto/contract/{n}.java": src for n, src in forge.dto_sources().items()}, "cics/Main.java": get(MAIN.replace("__NEW_WIZ__", NEW_WIZ[style])), diff --git a/tests/core_engine/test_cics_spec.py b/tests/core_engine/test_cics_spec.py index 29d1889087..24dd7eddfe 100644 --- a/tests/core_engine/test_cics_spec.py +++ b/tests/core_engine/test_cics_spec.py @@ -1,7 +1,8 @@ -"""#4270 spec PRs 1-2: gitgalaxy/standards/cics on its own -- the model's checks, the rendered refusal messages, the +"""#4270 spec PRs 1-4: gitgalaxy/standards/cics on its own -- the model's checks, the rendered refusal messages, the entries (45 full, the name-only API commands), the cics_spec CLI, and the package's cost: stdlib only, lazily loaded, imported only by its listed consumers (spec PR 2: the det translator; PR 3: the equivalence harness), -none of them the engine. +none of them the engine. Spec PR 4: the committed generated runtime tables (CicsSpec.java, ggcics_spec.h) are +never stale, and the runtimes keep no hand copy of them. (The proofs that the spec equals today's hand copies are transitional and live in tests/cobol_mainframe/test_cics_spec_equality.py.)""" @@ -177,6 +178,54 @@ def test_cli_regen_writes_the_runtime_tables(tmp_path): assert 'case 110 -> "CONTAINERERR";' in java and 'case "CHANNELERR" -> "AEZV";' in java assert "DFHRESP_CHANNELERR = 122," in c and 'case DFHRESP_CONTAINERERR: return "AEZJ";' in c assert cli.main(["regen", "--out", str(tmp_path), "--java-package", "Not A Package"]) == 2 + assert java.splitlines()[1] == "package __PACKAGE__.cics;" # (the forge fills the package in, as in CicsTask) + + +# ---- spec PR 4: the committed generated runtime tables ------------------------------------------------------------- +def test_the_committed_generated_files_are_not_stale(): + """The drift test (cics_command_spec.md section 7, PR 4; also the cics-spec ratchet, pr_gates.py --ratchets): + regenerating from the spec changes neither committed file. On a failure: `python -m gitgalaxy.standards.cics + regen`, review the Java / C diff, commit it.""" + assert cli.stale() == [] + for rel in cli.generated(): + assert (ROOT / rel).read_text(encoding="utf-8").startswith(("// " + cli.BANNER, "/* " + cli.BANNER)), rel + + +def test_regen_check_reports_a_stale_file(tmp_path, capsys): + for rel, text in cli.generated().items(): + (tmp_path / rel).parent.mkdir(parents=True, exist_ok=True) + (tmp_path / rel).write_text(text, encoding="utf-8") + assert cli.stale(tmp_path) == [] + (tmp_path / cli.C_FILE).write_text(cli.render_c().replace("AEZV", "AEZX"), encoding="utf-8") + assert cli.stale(tmp_path) == [cli.C_FILE] + assert cli.main(["regen", "--check"]) == 0 and "up to date" in capsys.readouterr().out + + +def _java_cases(src: str) -> list[str]: + """The `case -> "";` / `case "" -> ;` arms of a Java source.""" + return re.findall(r'case (?:\d+ -> "[A-Z0-9]+"|"[A-Z0-9]+" -> (?:\d+|"[A-Z0-9]{4}"));', src) + + +def test_the_runtimes_keep_no_hand_copy_of_the_generated_tables(): + """DetCics.condition / resp, CicsTask.respName / abcodeFor delegate to CicsSpec; ggcics.c includes ggcics_spec.h + and names its conditions by the spec's numbers (no literal RESP), with no condition_abcode of its own.""" + from gitgalaxy.tools.cobol_to_java import cobol_to_java_transaction_forge as forge + + detcics = (ROOT / "gitgalaxy/tools/cobol_to_java/det/cobolrt/cics/DetCics.java").read_text(encoding="utf-8") + assert "return CicsSpec.condition(resp);" in detcics and "return CicsSpec.resp(condition);" in detcics + assert ( + "CicsSpec.name(resp)" in forge.CICS_TASK_JAVA + and "return CicsSpec.abcodeFor(condition);" in forge.CICS_TASK_JAVA + ) + assert _java_cases(detcics) == [] and _java_cases(forge.CICS_TASK_JAVA) == [] + assert len(_java_cases(forge.CICS_SPEC_JAVA)) == len(RESP_NAME) + len(DFHRESP) + len(CONDITION_ABEND) + ggcics = (ROOT / "tests/equivalence/cics/ggcics.c").read_text(encoding="utf-8") + assert '#include "ggcics_spec.h"' in ggcics and "condition_abcode(int" not in ggcics + for body in re.findall(r"enum\s*\{([^}]*)\}", ggcics): + for name, value in re.findall(r"(\w+)\s*=\s*(\w+)", body): + if name in DFHRESP: + assert value == f"DFHRESP_{name}", name + assert re.search(r"\bERRCOND = DFHRESP_ERROR\b", ggcics) def test_python_m_runs_the_cli(): diff --git a/tests/equivalence/cics/ggcics.c b/tests/equivalence/cics/ggcics.c index 3fc4f5a549..f92d2c2238 100644 --- a/tests/equivalence/cics/ggcics.c +++ b/tests/equivalence/cics/ggcics.c @@ -53,6 +53,7 @@ #include #include #include +#include "ggcics_spec.h" /* #4270 spec PR 4: DFHRESP_* and condition_abcode, generated from gitgalaxy/standards/cics */ #ifdef _WIN32 #include #define MKDIR(p) _mkdir(p) /* MinGW / MSVC: no mode argument */ @@ -85,9 +86,11 @@ typedef struct { char rtran[4], rterm[4], rqueue[8]; /* START / RETRIEVE RTRANSID, RTERMID, QUEUE (GG-FLAGS names which) */ } gg_cics; -enum { NORMAL = 0, NOTFND = 13, LENGERR = 22, FILENOTFOUND = 12, MAPFAIL = 36, ITEMERR = 26, QIDERR = 44, - INVREQ = 16, PGMIDERR = 27, ENDDATA = 29, DUPREC = 14, ENDFILE = 20, CONTAINERERR = 110, - CHANNELERR = 122 }; +/* the conditions the stub raises, by their short names; the numbers are the spec's (ggcics_spec.h) */ +enum { NORMAL = DFHRESP_NORMAL, NOTFND = DFHRESP_NOTFND, LENGERR = DFHRESP_LENGERR, FILENOTFOUND = DFHRESP_FILENOTFOUND, + MAPFAIL = DFHRESP_MAPFAIL, ITEMERR = DFHRESP_ITEMERR, QIDERR = DFHRESP_QIDERR, INVREQ = DFHRESP_INVREQ, + PGMIDERR = DFHRESP_PGMIDERR, ENDDATA = DFHRESP_ENDDATA, DUPREC = DFHRESP_DUPREC, ENDFILE = DFHRESP_ENDFILE, + CONTAINERERR = DFHRESP_CONTAINERERR, CHANNELERR = DFHRESP_CHANNELERR }; static int seq = 0; static int ended = 0; /* a RETURN, XCTL or abend ended the task */ @@ -901,7 +904,7 @@ static char terminal_buf[32768]; static int terminal_at = 0, terminal_n = 0; /* the input not yet returned: terminal_buf[at..n) */ static char set_buf[32768]; /* RECEIVE SET's data, valid until the next RECEIVE */ -enum { EOC = 6 }; +enum { EOC = DFHRESP_EOC }; static int terminal_receive(gg_cics *c, char *into) { char path[4096], ev[96], flags[41]; @@ -1192,7 +1195,7 @@ int GGCXCTL(gg_cics *c, char *commarea, int len) { #define MAX_LEVELS 32 #define MAX_PUSH 16 #define NCOND 130 -enum { ERRCOND = 1 }; +enum { ERRCOND = DFHRESP_ERROR }; /* HANDLE CONDITION ERROR's slot */ typedef struct { short cond[NCOND]; /* >0 label index, -1 IGNORE, 0 default */ @@ -1233,23 +1236,6 @@ static void channel_pass(level *L, int passed); /* #4270 */ static int lvl = 0; /* the current level, 0 = level 1 */ static char task_abcode[5] = " "; -/* The abend code of an unhandled condition (the AEIA topic of IBM's abend codes, SPEC 6.2). */ -static const char *condition_abcode(int resp) { - switch (resp) { - case NOTFND: return "AEIM"; - case LENGERR: return "AEIV"; - case ITEMERR: return "AEIZ"; - case QIDERR: return "AEYH"; - case MAPFAIL: return "AEI9"; - case ENDDATA: return "AEI2"; - case PGMIDERR: return "AEI0"; - case INVREQ: return "AEIP"; - case CONTAINERERR: return "AEZJ"; /* #4270: IBM abend codes AEZJ / AEZV, "... condition not handled" */ - case CHANNELERR: return "AEZV"; - default: return "????"; - } -} - static const char *current_program(void) { return levels[lvl].prog; } /* The program's entry: the translator names it (GG-NAME1) for the level it runs at. It starts @@ -1888,7 +1874,8 @@ int GGCASCH(gg_cics *c) { * The virtual clock is $GGCICS_NOW; a task takes no time (SPEC 4). START records its request * as an event (with its expiry); the runner's scheduler keeps the requests and dispatches * them. RETRIEVE reads the data of the requests the task was started for. */ -enum { TRANSIDERR = 28, TERMIDERR = 11, IOERR = 17, ENVDEFERR = 56 }; +enum { TRANSIDERR = DFHRESP_TRANSIDERR, TERMIDERR = DFHRESP_TERMIDERR, IOERR = DFHRESP_IOERR, + ENVDEFERR = DFHRESP_ENVDEFERR }; static time_t now_epoch(void) { const char *s = getenv("GGCICS_NOW"); diff --git a/tests/equivalence/cics/ggcics_spec.h b/tests/equivalence/cics/ggcics_spec.h new file mode 100644 index 0000000000..210bccd44f --- /dev/null +++ b/tests/equivalence/cics/ggcics_spec.h @@ -0,0 +1,131 @@ +/* generated from gitgalaxy/standards/cics by `python -m gitgalaxy.standards.cics regen`; do not edit */ +#ifndef GGCICS_SPEC_H +#define GGCICS_SPEC_H + +/* IBM CICS TS, RESP values (DFHRESP): the condition name -> its RESP number */ +enum { + DFHRESP_NORMAL = 0, + DFHRESP_ERROR = 1, + DFHRESP_RDATT = 2, + DFHRESP_WRBRK = 3, + DFHRESP_EOF = 4, + DFHRESP_EODS = 5, + DFHRESP_EOC = 6, + DFHRESP_INBFMH = 7, + DFHRESP_ENDINPT = 8, + DFHRESP_NONVAL = 9, + DFHRESP_NOSTART = 10, + DFHRESP_TERMIDERR = 11, + DFHRESP_FILENOTFOUND = 12, + DFHRESP_DSIDERR = 12, + DFHRESP_NOTFND = 13, + DFHRESP_DUPREC = 14, + DFHRESP_DUPKEY = 15, + DFHRESP_INVREQ = 16, + DFHRESP_IOERR = 17, + DFHRESP_NOSPACE = 18, + DFHRESP_NOTOPEN = 19, + DFHRESP_ENDFILE = 20, + DFHRESP_ILLOGIC = 21, + DFHRESP_LENGERR = 22, + DFHRESP_QZERO = 23, + DFHRESP_SIGNAL = 24, + DFHRESP_QBUSY = 25, + DFHRESP_ITEMERR = 26, + DFHRESP_PGMIDERR = 27, + DFHRESP_TRANSIDERR = 28, + DFHRESP_ENDDATA = 29, + DFHRESP_INVTSREQ = 30, + DFHRESP_EXPIRED = 31, + DFHRESP_RETPAGE = 32, + DFHRESP_RTEFAIL = 33, + DFHRESP_RTESOME = 34, + DFHRESP_TSIOERR = 35, + DFHRESP_MAPFAIL = 36, + DFHRESP_INVERRTERM = 37, + DFHRESP_INVMPSZ = 38, + DFHRESP_IGREQID = 39, + DFHRESP_OVERFLOW = 40, + DFHRESP_INVLDC = 41, + DFHRESP_NOSTG = 42, + DFHRESP_JIDERR = 43, + DFHRESP_QIDERR = 44, + DFHRESP_NOJBUFSP = 45, + DFHRESP_DSSTAT = 46, + DFHRESP_SELNERR = 47, + DFHRESP_FUNCERR = 48, + DFHRESP_UNEXPIN = 49, + DFHRESP_NOPASSBKRD = 50, + DFHRESP_NOPASSBKWR = 51, + DFHRESP_SYSIDERR = 53, + DFHRESP_ISCINVREQ = 54, + DFHRESP_ENQBUSY = 55, + DFHRESP_ENVDEFERR = 56, + DFHRESP_IGREQCD = 57, + DFHRESP_SESSIONERR = 58, + DFHRESP_SYSBUSY = 59, + DFHRESP_SESSBUSY = 60, + DFHRESP_NOTALLOC = 61, + DFHRESP_CBIDERR = 62, + DFHRESP_INVEXITREQ = 63, + DFHRESP_INVPARTNSET = 64, + DFHRESP_INVPARTN = 65, + DFHRESP_PARTNFAIL = 66, + DFHRESP_USERIDERR = 69, + DFHRESP_NOTAUTH = 70, + DFHRESP_VOLIDERR = 71, + DFHRESP_SUPPRESSED = 72, + DFHRESP_RESIDERR = 75, + DFHRESP_NOSPOOL = 80, + DFHRESP_TERMERR = 81, + DFHRESP_ROLLEDBACK = 82, + DFHRESP_END = 83, + DFHRESP_DISABLED = 84, + DFHRESP_ALLOCERR = 85, + DFHRESP_STRELERR = 86, + DFHRESP_OPENERR = 87, + DFHRESP_SPOLBUSY = 88, + DFHRESP_SPOLERR = 89, + DFHRESP_NODEIDERR = 90, + DFHRESP_TASKIDERR = 91, + DFHRESP_TCIDERR = 92, + DFHRESP_DSNNOTFOUND = 93, + DFHRESP_LOADING = 94, + DFHRESP_MODELIDERR = 95, + DFHRESP_OUTDESCRERR = 96, + DFHRESP_PARTNERIDERR = 97, + DFHRESP_PROFILEIDERR = 98, + DFHRESP_NETNAMEIDERR = 99, + DFHRESP_LOCKED = 100, + DFHRESP_RECORDBUSY = 101, + DFHRESP_UOWNOTFOUND = 102, + DFHRESP_UOWLNOTFOUND = 103, + DFHRESP_CONTAINERERR = 110, + DFHRESP_CHANNELERR = 122, + DFHRESP_CCSIDERR = 123, + DFHRESP_TIMEDOUT = 124, + DFHRESP_CODEPAGEERR = 125, + DFHRESP_INCOMPLETE = 126, + DFHRESP_APPNOTFOUND = 127, + DFHRESP_BUSY = 128 +}; + +/* The abend code of an unhandled condition (IBM's AEIx / AEYx / AEZx abend codes, the AEIA topic); "????" for + one the spec has none for. */ +static const char *condition_abcode(int resp) { + switch (resp) { + case DFHRESP_NOTFND: return "AEIM"; + case DFHRESP_LENGERR: return "AEIV"; + case DFHRESP_ITEMERR: return "AEIZ"; + case DFHRESP_QIDERR: return "AEYH"; + case DFHRESP_MAPFAIL: return "AEI9"; + case DFHRESP_ENDDATA: return "AEI2"; + case DFHRESP_PGMIDERR: return "AEI0"; + case DFHRESP_INVREQ: return "AEIP"; + case DFHRESP_CONTAINERERR: return "AEZJ"; + case DFHRESP_CHANNELERR: return "AEZV"; + default: return "????"; + } +} + +#endif diff --git a/tests/tools/pr_gates.py b/tests/tools/pr_gates.py index ae17275fb8..1409c13e87 100644 --- a/tests/tools/pr_gates.py +++ b/tests/tools/pr_gates.py @@ -9,8 +9,9 @@ python tests/tools/pr_gates.py --e2e # add the equivalence proofs (Docker GnuCOBOL + JDK 17) python tests/tools/pr_gates.py --vs-main # re-run each failing gate on a fresh origin/main worktree and # label it "caused by branch" or "pre-existing on main@" - python tests/tools/pr_gates.py --ratchets # every ratchet a fix may move, in order: ports compile, estate-crucible - # gate, fact cross-check, corpus completeness pins, ground-truth ledger; + python tests/tools/pr_gates.py --ratchets # every ratchet a fix may move, in order: the CICS spec's generated + # files, ports compile, estate-crucible gate, fact cross-check, + # corpus completeness pins, ground-truth ledger; # prints pass/fail and the exact update command per failure # (--only-ratchets ports estate ... to pick; skips say "not available: why") @@ -168,6 +169,12 @@ def ratchets() -> list[Ratchet]: """The ratchets a fix may move, in the order to run them. Each reuses its tool's own entry point.""" t = "tests/tools/" return [ + Ratchet( + "cics-spec", + [PY, "-m", "gitgalaxy.standards.cics", "regen", "--check"], + "python -m gitgalaxy.standards.cics regen (commit CicsSpec.java / ggcics_spec.h; #4270 spec PR 4)", + lambda env: None, + ), # fmt: skip Ratchet( "ports", [PY, t + "ports_compile_check.py"], From 1ba2674dffc42b5e4a54a9155356f5b5e5d8c5f9 Mon Sep 17 00:00:00 2001 From: squid-protocol Date: Wed, 7 Oct 2026 08:17:33 -0400 Subject: [PATCH 2/4] spec status: PR 4 done (#4608) Co-Authored-By: Claude Opus 5.5 --- docs/language_status/cics_spec_status.md | 2 +- tests/tools/cics_spec_status.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/language_status/cics_spec_status.md b/docs/language_status/cics_spec_status.md index 11c722d8c0..e233295499 100644 --- a/docs/language_status/cics_spec_status.md +++ b/docs/language_status/cics_spec_status.md @@ -15,7 +15,7 @@ Census use is `programs (burned / non-burned)`, counts only. A spec key is the d | 1 | done (#4589) | gitgalaxy/standards/cics/ model + resp.py (DFHRESP, abend codes) + entries for the slice 1-4 commands, with today's text word for word | none | | 2 | done (#4591) | full entries for the other 34 OPTIONS commands (PR 1 has the 9 of slices 1-4) + LOAD / RELEASE (45 in all) | translator | | 3 | done (#4593) | equivalence_cics.py refusals / options / DFHRESP from the spec | stub translator | -| 4 | open | generated, committed runtime tables: CicsSpec.java (delegated to by DetCics condition / resp, respName, abcodeFor) and ggcics_spec.h | both runtimes | +| 4 | done (#4608) | generated, committed runtime tables: CicsSpec.java (delegated to by DetCics condition / resp, respName, abcodeFor) and ggcics_spec.h | both runtimes | | 5 | open | Fact wiring in cics_crucible.py | runner, both runtimes | | 6 | open | outcome conformance check (3.2), report-only for one week, then an undeclared (RESP, RESP2) fails the crucible cell | harness, CI | | 7 | open | check-register in pr_gates.py | docs, skill | diff --git a/tests/tools/cics_spec_status.py b/tests/tools/cics_spec_status.py index 5bbd209f54..da0098856e 100644 --- a/tests/tools/cics_spec_status.py +++ b/tests/tools/cics_spec_status.py @@ -45,7 +45,7 @@ FORMAT = "cics-spec-status/1" # The spec PRs merged so far (cics_command_spec.md section 7): each spec PR adds its own row here. -SPEC_PRS_DONE = {"1": "#4589", "2": "#4591", "3": "#4593"} +SPEC_PRS_DONE = {"1": "#4589", "2": "#4591", "3": "#4593", "4": "#4608"} # ---- live reads ------------------------------------------------------------------------------------------------- From bee0fc4ae7b9f01ec1b4a187f25a6d0916040223 Mon Sep 17 00:00:00 2001 From: squid-protocol Date: Wed, 7 Oct 2026 13:15:06 -0400 Subject: [PATCH 3/4] spec PR 4: cics_spec regen -- no variable reused across types (mypy audit) Co-Authored-By: Claude Opus 5.5 --- gitgalaxy/standards/cics/cli.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/gitgalaxy/standards/cics/cli.py b/gitgalaxy/standards/cics/cli.py index 8de45256a6..ed4275ec84 100644 --- a/gitgalaxy/standards/cics/cli.py +++ b/gitgalaxy/standards/cics/cli.py @@ -236,16 +236,16 @@ def main(argv: Sequence[str] | None = None) -> int: print(f"cics spec: {n} commands, {'ok' if not found else f'{len(found)} problem(s)'}") return 1 if found else 0 if args.check: - found = stale() - for rel in found: - print(f"stale: {rel}") - print(f"cics spec generated files: {'up to date' if not found else f'{len(found)} stale'}" - + ("; run `python -m gitgalaxy.standards.cics regen` and commit them" if found else "")) # fmt: skip - return 1 if found else 0 + old = stale() + for path in old: + print(f"stale: {path}") + print(f"cics spec generated files: {'up to date' if not old else f'{len(old)} stale'}" + + ("; run `python -m gitgalaxy.standards.cics regen` and commit them" if old else "")) # fmt: skip + return 1 if old else 0 if args.out is None: - for rel, text in generated().items(): - (ROOT / rel).write_text(text, encoding="utf-8") - print(f"wrote {rel}") + for path, text in generated().items(): + (ROOT / path).write_text(text, encoding="utf-8") + print(f"wrote {path}") return 0 if not re.fullmatch(r"(__PACKAGE__|[a-z_][a-z0-9_]*)(\.[a-z_][a-z0-9_]*)*", args.java_package): print(f"not a Java package name: {args.java_package}", file=sys.stderr) From ed5c9668a1377670ea8429c1dfd1cf85c61ceeac Mon Sep 17 00:00:00 2001 From: squid-protocol Date: Wed, 7 Oct 2026 14:03:06 -0400 Subject: [PATCH 4/4] spec PR 4: the cics-spec ratchet needs no external resource, so it runs when nothing else is present test_ratchets_skip_when_nothing_present asserted that no ratchet ran; the new cics-spec drift ratchet (regen --check) has no prerequisite and correctly runs. Assert it is the only one (CI full-suite, run 37660750159). Co-Authored-By: Claude Opus 5.5 --- tests/tools/test_pr_gates_fleet.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/tools/test_pr_gates_fleet.py b/tests/tools/test_pr_gates_fleet.py index 124918cd74..7236f319ac 100644 --- a/tests/tools/test_pr_gates_fleet.py +++ b/tests/tools/test_pr_gates_fleet.py @@ -67,7 +67,8 @@ def test_ratchets_skip_when_nothing_present(monkeypatch, tmp_path, capsys): monkeypatch.setattr(pr_gates, "run_gate", lambda cmds, root, e: ran.append(cmds) or (True, "")) assert pr_gates.run_ratchets(None, env) == 0 # skipped is not failed ... out = capsys.readouterr().out - assert not ran # ... and nothing was run + # ... and nothing that needs an external resource ran: only cics-spec, which needs none (spec PR 4) + assert [c[0][2:] for c in ran] == [["gitgalaxy.standards.cics", "regen", "--check"]] assert out.count("not available:") == 6 assert "not checked" in out and "mainframe_corpus.py fetch" in out and "mvn" in out