Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/language_status/register/X35.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 8 additions & 2 deletions gitgalaxy/standards/cics/commands/web.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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"),
Expand Down Expand Up @@ -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(
Expand Down
15 changes: 11 additions & 4 deletions gitgalaxy/tools/cobol_to_java/det/cics_web.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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):
Expand All @@ -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

Expand Down
133 changes: 133 additions & 0 deletions tests/cobol_mainframe/test_web_converse_bare_method_7525.py
Original file line number Diff line number Diff line change
@@ -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
15 changes: 9 additions & 6 deletions tests/tools/equivalence_cics_web.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@
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.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"}
_HTTP_X_KEYS = {"method", "path", "query", "status", "statustext", "mediatype", "body"}
Expand Down Expand Up @@ -287,7 +288,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']}")
Expand Down Expand Up @@ -556,9 +557,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_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"):
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):
Expand All @@ -570,7 +573,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")
Expand Down
Loading