From e6c3a5eb2a0f6c69269343a41c84be71fb7c415c Mon Sep 17 00:00:00 2001 From: squid-protocol Date: Fri, 6 Nov 2026 14:54:33 -0500 Subject: [PATCH 1/2] det: WEB CONVERSE takes the bare method keywords DELETE/GET/HEAD/OPTIONS/POST/PUT/TRACE (Part of #7525, #7709) The bare keyword is IBM's form of METHOD(cvda). It resolves at translation time to the CVDA number in both the det port and the GnuCOBOL oracle emitter, so the runtime and the C stub are unchanged; the spec's one_of group refuses none or two. TRACE / OPTIONS stay refused at run time by name. Co-Authored-By: Claude Sonnet 5.5 --- docs/language_status/register/X35.md | 3 + gitgalaxy/standards/cics/commands/web.py | 10 +- gitgalaxy/tools/cobol_to_java/det/cics_web.py | 15 +- .../test_web_converse_bare_method_7525.py | 133 ++++++++++++++++++ tests/tools/equivalence_cics_web.py | 16 ++- 5 files changed, 165 insertions(+), 12 deletions(-) create mode 100644 tests/cobol_mainframe/test_web_converse_bare_method_7525.py diff --git a/docs/language_status/register/X35.md b/docs/language_status/register/X35.md index d89c41742..339a317b2 100644 --- a/docs/language_status/register/X35.md +++ b/docs/language_status/register/X35.md @@ -50,6 +50,9 @@ reached: "partly (unit-proven on CicsTask and the stub C, which answer one step - **Refused by name** (the translators): CERTIFICATE, CODEPAGE, HTTPVNUM / HTTPRNUM, CIPHERS on OPEN; DOCTOKEN, CONTAINER, SET, NOTRUNCATE, TOCONTAINER, ACTION, CLOSESTATUS, AUTHENTICATE / USERNAME / PASSWORD, CHARACTERSET, BODYCHARSET on CONVERSE, and a CONVERSE without INTO; WEB SEND / RECEIVE / PARSE URL / EXTRACT / READ / WRITE HTTPHEADER stay refused whole. +- **WEB CONVERSE takes IBM's bare method keywords (#7525).** DELETE, GET, HEAD, OPTIONS, POST, PUT and TRACE are IBM's bare form of `METHOD(cvda)` + (exactly one of the eight; none or two refused by the spec group in both translators). Each resolves at translation time to the CVDA + number `METHOD(DFHVALUE(...))` gives, so the runtime and the stub are unchanged; TRACE / OPTIONS stay refused at run time by name. - **WEB CONVERSE URIMAP is modelled from the stated peers (#7525); NOTRUNCATE stays REFUSED.** URIMAP (IBM: a CLIENT URIMAP "applies only to this request", "do not specify the PATH or PATHLENGTH options"): `CicsTask.webConverse` takes the request's path from the URIMAP's `path` in `http_peers`, ASSUMED to be the URIMAP the session's own peer states (the run states a CLIENT URIMAP only as a diff --git a/gitgalaxy/standards/cics/commands/web.py b/gitgalaxy/standards/cics/commands/web.py index 19ec8b358..01e9f884c 100644 --- a/gitgalaxy/standards/cics/commands/web.py +++ b/gitgalaxy/standards/cics/commands/web.py @@ -50,6 +50,7 @@ _TOKEN_OUT = Arg("area_out", width=8) _TOKEN_IN = Arg("area_in", width=8) +METHOD_FLAGS = ("DELETE", "GET", "HEAD", "OPTIONS", "POST", "PUT", "TRACE") # #7525: IBM's bare form of METHOD(cvda) _CONVERT_FLAGS = ("CLICONVERT", "NOINCONVERT", "NOOUTCONVERT", "NOCLICONVERT") # the stated server(s): one fact both runtimes read (CicsTask.withHttpPeers, $GGCICS_HTTP) @@ -138,6 +139,7 @@ options={ "SESSTOKEN": _TOKEN_IN, "METHOD": Arg("cvda"), + **{f: Arg("flag") for f in METHOD_FLAGS}, "URIMAP": Arg("area_in", width=8), "PATH": Arg("area_in"), "PATHLENGTH": Arg("value"), @@ -173,13 +175,17 @@ "TOCONTAINER": Refusal("a response into a container is not modelled", "X35"), }, default_refusal=Refusal( - "only METHOD, URIMAP, PATH, QUERYSTRING, FROM, MEDIATYPE, INTO / MAXLENGTH / TOLENGTH, STATUSCODE / STATUSTEXT / " + "only METHOD (or its bare forms), URIMAP, PATH, QUERYSTRING, FROM, MEDIATYPE, INTO / MAXLENGTH / TOLENGTH, STATUSCODE / STATUSTEXT / " "STATUSLEN and CLIENTCONV are modelled", "X35", ), groups=( required("SESSTOKEN", msg="WEB CONVERSE without SESSTOKEN"), - required("METHOD", msg="WEB CONVERSE without METHOD"), + one_of( + "METHOD", + *METHOD_FLAGS, + msg="WEB CONVERSE without METHOD (or one of its bare forms DELETE, GET, HEAD, OPTIONS, POST, PUT, TRACE), or with more than one", + ), required("INTO", msg="WEB CONVERSE without INTO: only a response into the program's area is modelled"), requires(("PATH",), ("PATHLENGTH",), msg="WEB CONVERSE PATH without PATHLENGTH", both_ways=True), requires( diff --git a/gitgalaxy/tools/cobol_to_java/det/cics_web.py b/gitgalaxy/tools/cobol_to_java/det/cics_web.py index d8e9215c6..a9f796359 100644 --- a/gitgalaxy/tools/cobol_to_java/det/cics_web.py +++ b/gitgalaxy/tools/cobol_to_java/det/cics_web.py @@ -11,6 +11,10 @@ _msg, _requires, ) +from gitgalaxy.tools.cobol_to_java.det.cvda import CVDA + +# #7525: IBM's bare forms of WEB CONVERSE METHOD(cvda); written out here as the spec's METHOD_FLAGS (a test pins them equal) +METHOD_WORDS = ("DELETE", "GET", "HEAD", "OPTIONS", "POST", "PUT", "TRACE") def code_page_pair_refusal(opts: dict[str, str | None], key: str) -> str | None: @@ -172,9 +176,11 @@ def web(self, key: str, opts: dict, ind: str) -> list[str]: return [f"{ind}CicsTask.WebOpened {r} = {call};", f"{ind}if ({r}.resp() == 0) DetCics.putText({self.field(_arg(opts['SESSTOKEN']))}, {r}.sesstoken(), CS);", *self.outcome(opts, f"{r}.resp()", f"{r}.resp2()", ind)] # fmt: skip - for o in ("METHOD", "INTO"): - if not opts.get(o): - raise CicsError(_msg(key, "required", o)) + bare = [f for f in METHOD_WORDS if f in opts] # #7525: IBM's bare form of METHOD(cvda) + if len(bare) + ("METHOD" in opts) != 1 or ("METHOD" in opts and not opts["METHOD"]): + raise CicsError(_msg(key, "one_of", "METHOD")) + if not opts.get("INTO"): + raise CicsError(_msg(key, "required", "INTO")) for a, n in (("PATH", "PATHLENGTH"), ("QUERYSTRING", "QUERYSTRLEN"), ("FROM", "FROMLENGTH"), ("STATUSTEXT", "STATUSLEN")): # fmt: skip if (a in opts) != (n in opts): @@ -192,8 +198,9 @@ def text(o: str) -> str: def num(o: str, absent: str) -> str: return self.int_(_arg(opts[o])) if o in opts else absent + method = str(CVDA[bare[0]]) if bare else num("METHOD", "0") into = _arg(opts["INTO"]) - call = (f"task.webConverse({text('SESSTOKEN')}, {text('URIMAP')}, {num('METHOD', '0')}, {text('PATH')}, {num('PATHLENGTH', '0')}, " + call = (f"task.webConverse({text('SESSTOKEN')}, {text('URIMAP')}, {method}, {text('PATH')}, {num('PATHLENGTH', '0')}, " f"{text('QUERYSTRING')}, {num('QUERYSTRLEN', '0')}, {text('FROM')}, {num('FROMLENGTH', '0')}, " f"{text('MEDIATYPE')}, {num('MAXLENGTH', str(self.size(into)))}, {num('STATUSLEN', '-1')}, CS)") # fmt: skip diff --git a/tests/cobol_mainframe/test_web_converse_bare_method_7525.py b/tests/cobol_mainframe/test_web_converse_bare_method_7525.py new file mode 100644 index 000000000..25a33ca00 --- /dev/null +++ b/tests/cobol_mainframe/test_web_converse_bare_method_7525.py @@ -0,0 +1,133 @@ +"""#7525 (register X35): EXEC CICS WEB CONVERSE with IBM's bare method keyword (DELETE, GET, HEAD, OPTIONS, POST, PUT, TRACE) +in place of METHOD(cvda). IBM (EXEC CICS WEB CONVERSE): the method is given by one of those keywords or by METHOD. Both +translators (the det port and the GnuCOBOL oracle's) resolve the keyword to its CVDA at translation time, so the runtime and +the stub see the same number as METHOD(DFHVALUE(x)); two methods, or none, are refused by the spec group. The end-to-end +proof reuses the URIMAP program of test_web_converse_urimap_e2e_7525.py with each METHOD(DFHVALUE(x)) written as the keyword.""" + +from __future__ import annotations + +import json +import os +import subprocess +import sys +from pathlib import Path + +import pytest + +from gitgalaxy.standards.cics.commands.web import METHOD_FLAGS, WEB_CONVERSE +from gitgalaxy.tools.cobol_to_java.det.cics_web import METHOD_WORDS +from gitgalaxy.tools.cobol_to_java.det.cvda import CVDA + +HERE = Path(__file__).resolve().parent +TOOLS = HERE.parents[0] / "tools" +sys.path.insert(0, str(HERE)) +import test_det_translate as t # noqa: E402 + +TAIL = " INTO(AREA) MAXLENGTH(R2) NOHANDLE" +BODY = " FROM(AREA) FROMLENGTH(R) MEDIATYPE(REC)" + + +def _det(method: str, extra: str = "") -> str: + return " ".join(t._ChanCics().command(f"WEB CONVERSE SESSTOKEN(KEY) {method}{extra}{TAIL}", "")) + + +def test_the_spec_names_the_seven_keywords_as_flags_of_the_method(): + assert METHOD_FLAGS == ("DELETE", "GET", "HEAD", "OPTIONS", "POST", "PUT", "TRACE") + assert METHOD_WORDS == METHOD_FLAGS + for f in METHOD_FLAGS: + assert WEB_CONVERSE.options[f].kind == "flag" + assert f not in WEB_CONVERSE.refused + + +@pytest.mark.parametrize("word", METHOD_FLAGS) +def test_det_bare_keyword_is_the_same_java_as_the_value_form(word): + assert _det(word) == _det("METHOD(R)").replace("INT(R)", str(CVDA[word]), 1) + assert f", {CVDA[word]}, " in _det(word) + + +def test_det_bare_keyword_with_a_body_and_with_a_path(): + assert _det("POST", BODY) == _det("METHOD(R)", BODY).replace("INT(R)", "636", 1) + assert ", 748, " in _det("GET", " PATH(REC) PATHLENGTH(R)") + + +@pytest.mark.parametrize( + ("cmd", "why"), + [ + ("WEB CONVERSE SESSTOKEN(KEY)" + TAIL, "without METHOD"), + ("WEB CONVERSE SESSTOKEN(KEY) GET POST" + TAIL, "or with more than one"), + ("WEB CONVERSE SESSTOKEN(KEY) METHOD(R) GET" + TAIL, "or with more than one"), + ("WEB CONVERSE SESSTOKEN(KEY) GET MAXLENGTH(R2)", "without INTO"), + ], +) +def test_det_refuses_no_method_and_two_methods(cmd, why): + with pytest.raises(t.C.CicsError, match=why): + t._ChanCics().command(cmd, "") + + +def test_oracle_translator_moves_the_cvda_and_refuses_as_the_det_port_does(): + pytest.importorskip("tree_sitter_language_pack") + sys.path.insert(0, str(TOOLS)) + import equivalence_cics as ec + + base = "WEB CONVERSE SESSTOKEN(WS-TOK) {} INTO(WS-IN) MAXLENGTH(WS-MAX)" + for word in METHOD_FLAGS: + bare = ec.translate_command(base.format(word)) + assert f"MOVE {CVDA[word]} TO GG-NUM" in bare + assert any("GGCWCNV" in line for line in bare) + assert bare == ec.translate_command(base.format(f"METHOD({CVDA[word]})")) + for bad in ("", "GET POST", "METHOD(WS-M) PUT"): + with pytest.raises(ec.Unsupported, match="METHOD"): + ec.translate_command(base.format(bad)) + + +def test_trace_and_options_stay_refused_by_name_by_the_runtime(): + # the keywords resolve to the numbers the runtime refuses (cics_task_web.java: 760 TRACE, 759 OPTIONS) + assert (CVDA["TRACE"], CVDA["OPTIONS"]) == (760, 759) + assert "WEB CONVERSE METHOD TRACE / OPTIONS" in ( + Path(__file__).resolve().parents[2] / "gitgalaxy/tools/cobol_to_java/cics_task_web.py" + ).read_text(encoding="utf-8") + + +@pytest.mark.slow(reason="a Maven build of the generated project and a GnuCOBOL container run") +@pytest.mark.skipif(os.environ.get("EQUIVALENCE_E2E") != "1", reason="needs Docker (GnuCOBOL) and a JDK + Maven") +def test_a_bare_keyword_program_is_proven_against_gnucobol(tmp_path, monkeypatch): + pytest.importorskip("tree_sitter_language_pack") + sys.path.insert(0, str(TOOLS)) + import det_port + import test_web_converse_urimap_e2e_7525 as e + + from gitgalaxy.tools.cobol_to_java.det import program as P + + program = e.PROGRAM + for w in ("POST", "PUT", "GET", "DELETE"): + program = program.replace(f"METHOD(DFHVALUE({w}))", w) + assert "DFHVALUE(PATCH)" in program and "DFHVALUE(POST)" not in program + estate = tmp_path / "estate" + (estate / "cbl").mkdir(parents=True) + (estate / "csd").mkdir() + (estate / "cbl" / "WCUDEMO.cbl").write_text(program, encoding="ascii") + (estate / "csd" / "WCU.csd").write_text(e.CSD, encoding="ascii") + for argv in (["init", "-q"], ["add", "-A"], ["-c", "user.name=t", "-c", "user.email=t@t", "commit", "-qm", "e"]): + subprocess.run(["git", "-C", str(estate), *argv], check=True) # noqa: S603, S607 + (tmp_path / "case.json").write_text(json.dumps(e._case()), encoding="utf-8") + project = det_port.estate(estate, tmp_path / "translate") + port = tmp_path / "port" + (port / "service").mkdir(parents=True) + stub = (project / "src/main/java" / det_port.PKG_DIR / "service" / "WcudemoService.java").read_text("utf-8") + r = P.translate( + estate / "cbl" / "WCUDEMO.cbl", [estate / "cbl"], stub, det_port.PKG, P.estate_files(project), project + ) + assert r.stats["holes"] == [], r.stats["holes"] + (port / "service" / f"{r.service}.java").write_text(r.java, encoding="utf-8") + for rel, text in P.runtime_files(det_port.PKG, P.has_batch(project)).items(): + (port / rel).parent.mkdir(parents=True, exist_ok=True) + (port / rel).write_text(text, encoding="utf-8") + keep = tmp_path / "proof" + proc = subprocess.run([sys.executable, str(TOOLS / "equivalence.py"), "run", "wcu-7525b", "--case-file", # noqa: S603 + str(tmp_path / "case.json"), "--corpus-dir", str(estate), "--port", str(port), "--keep", + str(keep), "--faults", "none"], capture_output=True, text=True, check=False) # fmt: skip + report_file = keep / "report.json" + report = json.loads(report_file.read_text(encoding="utf-8")) if report_file.is_file() else {} + assert proc.returncode == 0 and report.get("proven") is True, proc.stdout[-4000:] + proc.stderr[-3000:] + events = report["outputs"]["converse"]["cobol"] + assert [x["text"].rstrip() for x in events if x.get("event") == "SEND-TEXT"] == e.SENT diff --git a/tests/tools/equivalence_cics_web.py b/tests/tools/equivalence_cics_web.py index 1c137923d..f485be150 100644 --- a/tests/tools/equivalence_cics_web.py +++ b/tests/tools/equivalence_cics_web.py @@ -10,8 +10,10 @@ from typing import Any import equivalence_cics as ec -from gitgalaxy.tools.cobol_to_java.det.cics_web import code_page_pair_refusal +from gitgalaxy.standards.cics.commands.web import METHOD_FLAGS +from gitgalaxy.tools.cobol_to_java.det.cics_web import code_page_pair_refusal +from gitgalaxy.tools.cobol_to_java.det.cvda import CVDA _HTTP_PEER_KEYS = {"scheme", "host", "port", "urimap", "path", "exchanges"} _HTTP_X_KEYS = {"method", "path", "query", "status", "statustext", "mediatype", "body"} @@ -287,7 +289,7 @@ def _web_extract(opts: dict[str, str | None], labels: list[str] | None, key: str if opts.get("PORTNUMBER"): back.append(f" MOVE GG-NUM TO {opts['PORTNUMBER']}") if opts.get("SCHEME"): - back += [f" MOVE 1096 TO {opts['SCHEME']}", f" IF GG-NAME1(1:5) = 'HTTPS'", f" MOVE 1029 TO {opts['SCHEME']}", + back += [f" MOVE 1096 TO {opts['SCHEME']}", " IF GG-NAME1(1:5) = 'HTTPS'", f" MOVE 1029 TO {opts['SCHEME']}", " END-IF"] # fmt: skip if opts.get("REQUESTTYPE"): back.append(f" MOVE 1033 TO {opts['REQUESTTYPE']}") @@ -556,9 +558,11 @@ def _web(opts: dict[str, str | None], labels: list[str] | None) -> list[str]: "URIMAP" in opts and "PATH" in opts ): # (the spec group is not enforced by _check_spec: the det translator checks it too) raise ec.Unsupported(ec._rule(key, "at_most_one", "URIMAP"), [key]) - for o in ("METHOD", "INTO"): - if not opts.get(o): - raise ec.Unsupported(ec._rule(key, "required", o), [key]) + bare = [f for f in METHOD_FLAGS if f in opts] # #7525: IBM's bare form of METHOD(cvda) + if len(bare) + ("METHOD" in opts) != 1 or ("METHOD" in opts and not opts["METHOD"]): + raise ec.Unsupported(ec._rule(key, "one_of", "METHOD"), [key]) + if not opts.get("INTO"): + raise ec.Unsupported(ec._rule(key, "required", "INTO"), [key]) for a, n in (("PATH", "PATHLENGTH"), ("QUERYSTRING", "QUERYSTRLEN"), ("FROM", "FROMLENGTH"), ("STATUSTEXT", "STATUSLEN")): # fmt: skip if (a in opts) != (n in opts): @@ -570,7 +574,7 @@ def _web(opts: dict[str, str | None], labels: list[str] | None) -> list[str]: if "CLIENTCONV" in opts and not opts["CLIENTCONV"]: raise ec.Unsupported(ec.NEEDS_ARGUMENT, [key]) into = opts["INTO"] - lines = [f"MOVE {opts['METHOD']} TO GG-NUM", f"MOVE {opts.get('MAXLENGTH') or f'LENGTH OF {into}'} TO GG-LEN", + lines = [f"MOVE {CVDA[bare[0]] if bare else opts['METHOD']} TO GG-NUM", f"MOVE {opts.get('MAXLENGTH') or f'LENGTH OF {into}'} TO GG-LEN", f"MOVE {opts.get('STATUSLEN') or '-1'} TO GG-ITEM"] # fmt: skip if opts.get("URIMAP"): # #7525: the name in GG-NAME1, GG-FLAGS names it with the other options given lines.append(f"MOVE {opts['URIMAP']} TO GG-NAME1") From a761b5207fe778d71b06552c1368e6db201c9edc Mon Sep 17 00:00:00 2001 From: squid-protocol Date: Fri, 6 Nov 2026 15:12:59 -0500 Subject: [PATCH 2/2] oracle emitter: take the bare method words from the det port, not the spec package (Part of #7525) test_only_the_listed_consumers_import_the_spec pins the spec's importers; equivalence_cics_web.py now reads METHOD_WORDS from det/cics_web.py instead. Co-Authored-By: Claude Sonnet 5.5 --- tests/tools/equivalence_cics_web.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/tools/equivalence_cics_web.py b/tests/tools/equivalence_cics_web.py index f485be150..619bee75d 100644 --- a/tests/tools/equivalence_cics_web.py +++ b/tests/tools/equivalence_cics_web.py @@ -11,8 +11,7 @@ import equivalence_cics as ec -from gitgalaxy.standards.cics.commands.web import METHOD_FLAGS -from gitgalaxy.tools.cobol_to_java.det.cics_web import code_page_pair_refusal +from gitgalaxy.tools.cobol_to_java.det.cics_web import METHOD_WORDS, code_page_pair_refusal from gitgalaxy.tools.cobol_to_java.det.cvda import CVDA _HTTP_PEER_KEYS = {"scheme", "host", "port", "urimap", "path", "exchanges"} @@ -558,7 +557,7 @@ def _web(opts: dict[str, str | None], labels: list[str] | None) -> list[str]: "URIMAP" in opts and "PATH" in opts ): # (the spec group is not enforced by _check_spec: the det translator checks it too) raise ec.Unsupported(ec._rule(key, "at_most_one", "URIMAP"), [key]) - bare = [f for f in METHOD_FLAGS if f in opts] # #7525: IBM's bare form of METHOD(cvda) + bare = [f for f in METHOD_WORDS if f in opts] # #7525: IBM's bare form of METHOD(cvda) if len(bare) + ("METHOD" in opts) != 1 or ("METHOD" in opts and not opts["METHOD"]): raise ec.Unsupported(ec._rule(key, "one_of", "METHOD"), [key]) if not opts.get("INTO"):