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
13 changes: 9 additions & 4 deletions docs/language_status/register/X51.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ family: "X"
status: "ASSUMED"
title: "A RETURN or XCTL COMMAREA passes LENGTH bytes, a LINK's is passed by address — the det port cuts the bytes it passes beside the DTO at LENGTH (#4817, #4695)"
area: "CICS"
summary: "EXEC CICS RETURN TRANSID COMMAREA LENGTH and XCTL COMMAREA LENGTH hand the next task / the target LENGTH bytes (its EIBCALEN), every byte as it was; the det port passes the record's whole span beside its DTO (#4806, #4817), so CicsTask cuts those bytes at LENGTH (null: all; past the span: all of it), as the DTO's reading already is; a LINK's area is not cut (by address); a LENGTH past the DTO passes the COMMAREA as a byte[] (region EBCDIC), which a facade's handleTransaction answers as an Object by design"
summary: "EXEC CICS RETURN TRANSID COMMAREA LENGTH and XCTL COMMAREA LENGTH hand the next task / the target LENGTH bytes (its EIBCALEN), every byte as it was; the det port passes the record's whole span beside its DTO (#4806, #4817), so CicsTask cuts those bytes at LENGTH (null: all; past the span: all of it), as the DTO's reading already is; a LINK's area is not cut (by address); a LENGTH past the DTO passes the COMMAREA as a byte[] (region EBCDIC), which a det facade's handleTransaction answers as the program's own DTO, read through its layout in the port's page (#4695; the task keeps the bytes)"
reached: "no (unit-proven on CicsTask for RETURN, RETURN IMMEDIATE and XCTL, every LENGTH class: tests/cobol_mainframe/test_commarea_length_bytes_4817.py; no estate program differs by it: a conforming program never reads past EIBCALEN)"
---
- **What.** The det port keeps the bytes of a COMMAREA beside its DTO so a numeric DISPLAY field holding spaces, or a byte no DTO
Expand All @@ -16,7 +16,12 @@ reached: "no (unit-proven on CicsTask for RETURN, RETURN IMMEDIATE and XCTL, eve
of a longer record past LENGTH: a program must not read them (the entry treats the cut as the model, ASSUMED, not as IBM text).
- **What we do.** `CicsTask.returnTransid` / `returnImmediate` / `xctl` keep `min(LENGTH, bytes)` of the bytes passed (a copy); a
null LENGTH keeps all of them. A LINK passes its area by address (#4806), so the callee's storage is the caller's, tail included:
not cut. A RETURN whose LENGTH is past the DTO passes the COMMAREA as a `byte[]` in the region's EBCDIC (#4679): the facade's
`handleTransaction` answers it as an `Object` (it has no DTO type for it), `returned(Dto.class)` refuses it by name, and
`returnedBytes()` is null (the bytes are the answer itself) (#4695).
not cut. A RETURN whose LENGTH is past the DTO passes the COMMAREA as a `byte[]` in the region's EBCDIC (#4679): the task's
`returned(Object.class)` is that `byte[]`, `returned(Dto.class)` refuses it by name, and `returnedBytes()` is null (the bytes
are the answer itself). A det facade's `handleTransaction` (both overloads) answers the program's own DTO read from them
(the service's `wideAnswer`: the DTO's `fromCommarea` layout, #4449, after undoing the region's EBCDIC), so a deployed caller never
sees opaque bytes; EIBCALEN and the bytes stay the task's for the next one, and `lastAnswerBytes()` is unchanged. A DTO with
no layout, or bytes shorter than its record, are answered as before: an `Object` facade keeps the `byte[]`, a typed one is
refused by name (`returned(Dto.class)`; CBSA BNK1CCS, BNK1TFN, BNK1UAC, #5076); a facade with no DTO of its own (`Object
request`) keeps the `byte[]`; a forge-generated (model-written) facade is unchanged (#4695).
- **Status:** ASSUMED. The oracle's (GnuCOBOL) storage past the LENGTH of a copied COMMAREA is not IBM's either; no program may rely on it.
5 changes: 5 additions & 0 deletions gitgalaxy/tools/cobol_to_java/det/cics.py
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,9 @@ def __init__(self, gen, generated: Generated, package: str):
self.codecs: dict[str, list[str]] = {} # DTO class -> its in / out methods
self.stores: dict[str, str] = {} # CICS file -> the Java Store expression
self.repos: dict[str, str] = {} # repository class -> field
self.passes_bytes = (
False # #4695: a RETURN / XCTL passes its COMMAREA as bytes (commareaOut): a facade reads them
)
self.used_screens: set[str] = set()
# #4414: the program issues HANDLE AID (its input commands consult the keys' labels) / PUSH HANDLE (its
# handler state is stacked); set by program._translate from the source before any statement is translated
Expand Down Expand Up @@ -816,10 +819,12 @@ def commarea_out(self, opts: dict, program: str | None = None) -> tuple[str, str
known = self.constant_int(opts["LENGTH"])
if known is None or known > d.extent:
dto = f"DetCics.commareaOut({dto}, {f}, {length}, {d.extent}, CS)"
self.passes_bytes = True
elif d.size > d.extent:
# #4765: a DTO whose OCCURS fields appear once holds the first occurrence only: the record's bytes go, every
# occurrence of the table (as a LINK's span, Dto.size); #4778: and a trailing FILLER's
dto = f"DetCics.commareaOut({dto}, {f}, {d.size}, {d.extent}, CS)"
self.passes_bytes = True
return dto, length

def in_dfhcommarea(self, text: str) -> bool:
Expand Down
59 changes: 54 additions & 5 deletions gitgalaxy/tools/cobol_to_java/det/program.py
Original file line number Diff line number Diff line change
Expand Up @@ -1618,10 +1618,53 @@ def _cics_parts(gen: G.Gen, records: list, roots: dict, proc: S.Procedure,
" }",
"",
]
return members + ca_whole, entry + facades(stub)


def facades(stub: str) -> list[str]:
return members + ca_whole, entry + facades(stub, cx.passes_bytes)


def answer_lines(ret: str, own: str | None) -> list[str]:
"""#4695: the lines that answer handleTransaction of a program whose RETURN or XCTL can pass its COMMAREA as bytes
(a LENGTH past the DTO, #4679: DetCics.commareaOut). The answer is then the program's own DTO read from them
(wideAnswer: the layout of #4449, through the region page), the task keeping the bytes for the next one. Bytes no
layout reads (wideAnswer: null) are answered as before: an Object answer (the flows present several records) keeps
them, a typed one is refused by name by task.returned. An Object answer with no DTO of its own (`own` is None or
Object) has nothing to read them through."""
if ret == "Object" and own in (None, "Object"):
return [" return task.returned(Object.class);"]
dto = own if ret == "Object" else ret
read = f"wideAnswer(wide, {dto}.class)"
if ret == "Object":
return [" Object answer = task.returned(Object.class);",
" if (answer instanceof byte[] wide) {",
f" Object read = {read};",
" return read != null ? read : answer;",
" }",
" return answer;"] # fmt: skip
return [" if (task.returned(Object.class) instanceof byte[] wide) {",
f" {ret} read = {read};",
" if (read != null) {",
" return read;",
" }",
" }",
f" return task.returned({ret}.class);"] # fmt: skip


WIDE_ANSWER = [
" // @facade-helper: a handleTransaction answer read from the COMMAREA bytes a RETURN passed (#4695)",
" /** #4695: the COMMAREA bytes a RETURN passed with a LENGTH past the DTO (the region's EBCDIC), read through",
" * `type`'s layout (fromCommarea, #4449) in this port's page; null when `type` has no layout or the bytes are",
" * shorter than its record. @facade-helper */",
" private static <T> T wideAnswer(byte[] ebcdic, Class<T> type) {",
" try {",
' return type.cast(type.getMethod("fromCommarea", byte[].class).invoke(null, (Object) DetCics.commareaIn(ebcdic, CS)));',
" } catch (ReflectiveOperationException e) {",
" return null;",
" }",
" }",
"",
]


def facades(stub: str, passes_bytes: bool = False) -> list[str]:
"""The stub's handleTransaction / handleLink, kept for their callers (#4465): each runs the program -- one task of
it in the region, as the generator's own facades do (#4343: CicsTask.region(), region.transaction / region.linked,
region.run(task, NAME, this::runTask)) -- and returns what the generated signature says: the COMMAREA its RETURN
Expand All @@ -1631,6 +1674,7 @@ def facades(stub: str) -> list[str]:
entry stops by name rather than return as if the program had run (#4342: no generated entry that does nothing)."""
name = re.search(r'region\.run\(task, ("(?:[^"\\]|\\.)*"), this::runTask\)', stub)
out: list[str] = []
wide = False
for meth in ("handleTransaction", "handleLink"):
# #4870: handleTransaction has two overloads -- the DTO's, and the one carrying the COMMAREA's bytes in (and,
# through lastAnswerBytes(), out) -- each rebuilt on its own
Expand All @@ -1650,10 +1694,15 @@ def facades(stub: str) -> list[str]:
f" region.run(task, {name.group(1)}, this::runTask);"] # fmt: skip
if area and meth == "handleTransaction":
body.append(" lastAnswerBytes = task.returnedBytes();")
if ret != "void":
if ret != "void" and meth == "handleTransaction" and passes_bytes:
body += (lines := answer_lines(ret, req.group(1) if req else None))
wide = wide or len(lines) > 1
elif ret != "void":
body.append(" return request;" if meth == "handleLink" and req and ret == req.group(1)
else f" return task.returned({ret}.class);") # fmt: skip
out += [f" public {ret} {meth}({params}) {{", *body, " }", ""]
if wide:
out += WIDE_ANSWER
# The bytes the byte-carrying overload answers: a field and its accessor, tagged for port_surface (#4870)
if re.search(r"public byte\[\] lastAnswerBytes\(\)", stub):
out += [" // @facade-helper: the bytes the last byte-carrying handleTransaction answered",
Expand Down
3 changes: 2 additions & 1 deletion tests/cobol_mainframe/test_equivalence_cics.py
Original file line number Diff line number Diff line change
Expand Up @@ -760,7 +760,8 @@ def test_the_generated_test_runs_runtask_unless_the_facade_side_is_asked_for(tmp
assert 'if (facades) { region.enter("LINKED", t, ' in java and ".runTask(t); } return; }" in java
linked = ec.cics_equivalence_test({**case, "linked": True}, tmp_path, [])
assert 'new FacadeRegion(task, commarea, "PROG", true)' in linked
assert ec.FACADE_JAVA in java
pkg = java.split("package ", 1)[1].split(";", 1)[0]
assert ec.FACADE_JAVA.replace("__PKG__", pkg) in java # #4695: the port's package names its page's class


# ---- file updates: WRITE, REWRITE, READ UPDATE, SYNCPOINT (CardDemo's update programs) -----------------------
Expand Down
169 changes: 169 additions & 0 deletions tests/cobol_mainframe/test_facade_wide_commarea_4695.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
"""#4695: a det facade's handleTransaction answers the program's own DTO when its RETURN's COMMAREA LENGTH is past the DTO.

Since #4679 such a RETURN passes the COMMAREA as a byte[] in the region's EBCDIC (DetCics.commareaOut). The facade used to
answer that byte[] (CardDemo COCRDLIC, COCRDSLC, COCRDUPC, COACTVWC, COACTUPC: RETURN WS-COMMAREA LENGTH 2000), opaque to
a deployed caller. Now the service's wideAnswer reads the bytes through the DTO's layout (fromCommarea, #4449) in the port's
page; the task keeps the bytes (EIBCALEN, the next task's COMMAREA), and lastAnswerBytes() is unchanged. Only a program that
passes bytes (cx.passes_bytes) carries it, so no other port moves: the helper lives in the service, not in DetCics.

Two layers: the generated source (every facade shape: typed, Object, Object with no own DTO, the byte-carrying overload,
void, handleLink, a program that passes no bytes) and the Java of wideAnswer itself (every answer class: text, spaces,
LOW-VALUES, non-text bytes, no layout, short bytes: null, left to the answer the facade had).
"""

import os
import re
import shutil
import subprocess
from pathlib import Path

import pytest
from gitgalaxy.tools.cobol_to_java.det import program as P

DET_CICS = Path(P.__file__).parent / "cobolrt" / "cics" / "DetCics.java"


def _stub(ret: str, req: str | None) -> str:
arg = f", {req} request" if req else ""
return (
f"public class S {{\n public {ret} handleTransaction(String transid{arg}) {{\n"
' region.run(task, "PROG", this::runTask);\n return task.returned(' + ret + ".class);\n }\n"
f" public {ret} handleTransaction(String transid{arg}, byte[] area) {{\n"
' region.run(task, "PROG", this::runTask);\n return task.returned(' + ret + ".class);\n }\n"
" public byte[] lastAnswerBytes() {\n return null;\n }\n}\n"
)


def test_an_object_facade_with_its_own_dto_converts_wide_bytes_in_both_overloads():
java = "\n".join(P.facades(_stub("Object", "CaDto"), True))
assert java.count("wideAnswer(wide, CaDto.class)") == 2
assert java.count(" return answer;") == 2 # any other answer is as it was
assert java.count("lastAnswerBytes = task.returnedBytes();") == 1 # the bytes stay the task's, as they were
assert java.count("private static <T> T wideAnswer(") == 1 and "@facade-helper" in java


def test_a_program_that_passes_no_bytes_is_unchanged():
"""Its facade and its service are as they were: nothing else of the estate's ports moves."""
for ret, req in (("Object", "CaDto"), ("CaDto", "CaDto")):
java = "\n".join(P.facades(_stub(ret, req)))
assert "wideAnswer" not in java and java.count(f"return task.returned({ret}.class);") == 2


def test_a_typed_facade_converts_and_leaves_what_no_layout_reads_to_task_returned():
java = "\n".join(P.facades(_stub("CaDto", "CaDto"), True))
assert java.count("CaDto read = wideAnswer(wide, CaDto.class);") == 2
assert java.count("return task.returned(CaDto.class);") == 2 # bytes no layout reads: refused by name, as before


@pytest.mark.parametrize("req", [None, "Object"])
def test_an_object_facade_with_no_dto_of_its_own_keeps_the_bytes(req):
java = "\n".join(P.facades(_stub("Object", req), True))
assert "wideAnswer" not in java
assert java.count("return task.returned(Object.class);") == 2


def test_void_and_link_facades_are_as_they_were():
java = "\n".join(
P.facades("public void handleTransaction(String transid) {}\npublic CaDto handleLink(CaDto request) {}\n")
)
assert "wideAnswer" not in java
java = "\n".join(
P.facades('public CaDto handleLink(CaDto request) {\n region.run(task, "PROG", this::runTask);\n}\n')
)
assert " return request;" in java and "wideAnswer" not in java


MAIN = """
import java.nio.charset.Charset;
import java.util.Arrays;

public class Main {
static final Charset EBCDIC = Charset.forName("IBM037");
static final Charset CS = Charset.forName("ISO-8859-1");

static class DetCics {
%(commarea_in)s
}

%(methods)s

public static class Dto {
final byte[] rec;
Dto(byte[] rec) { this.rec = rec; }
public static Dto fromCommarea(byte[] rec) {
if (rec.length < 4) {
throw new IllegalArgumentException(rec.length + " bytes do not fill Dto's 4-byte record");
}
return new Dto(Arrays.copyOf(rec, 4));
}
}

public static class NoLayout { }

static void say(String k, Object v) { System.out.println(k + "=" + v); }

public static void main(String[] a) {
Charset page = Charset.forName("ISO-8859-1");
byte[] wide = new byte[40];
Arrays.fill(wide, (byte) 0x40); // EBCDIC spaces
byte[] head = "AB12".getBytes(EBCDIC);
System.arraycopy(head, 0, wide, 0, 4);
for (int len : new int[] {4, 5, 40}) { // exactly the record, past it, the 2000-byte shape
Dto d = wideAnswer(Arrays.copyOf(wide, len), Dto.class);
say("read " + len, Arrays.equals(d.rec, "AB12".getBytes(page)));
}
byte[] odd = Arrays.copyOf(wide, 12);
odd[1] = 0x00; // LOW-VALUES
odd[2] = 0x40; // a space
Dto d = wideAnswer(odd, Dto.class);
say("low-values", Arrays.equals(d.rec, new byte[] {'A', 0, ' ', '2'}));
say("no-layout", wideAnswer(wide, NoLayout.class) == null);
say("short", wideAnswer(Arrays.copyOf(wide, 3), Dto.class) == null);
}
}
"""


def _jdk() -> Path | None:
jdk = os.environ.get("JDK_17") or os.environ.get("JAVA_HOME")
if jdk and (Path(jdk) / "bin" / "javac").exists():
return Path(jdk)
javac = shutil.which("javac")
return Path(javac).resolve().parent.parent if javac else None


def _commarea_in() -> str:
"""The real DetCics.commareaIn, cut from its source."""
text = DET_CICS.read_text(encoding="utf-8")
m = re.search(r" public static byte\[\] commareaIn\(.*?\n }\n", text, re.S)
assert m
return m.group(0)


@pytest.mark.skipif(_jdk() is None, reason="needs a JDK 17 (JAVA_HOME / JDK_17)")
def test_commarea_as_reads_the_bytes_through_the_layout(tmp_path):
jdk = _jdk()
(tmp_path / "Main.java").write_text(
MAIN % {"commarea_in": _commarea_in(), "methods": "\n".join(P.WIDE_ANSWER)}, encoding="utf-8"
)
subprocess.run(
[str(jdk / "bin" / "javac"), "-encoding", "UTF-8", "-d", str(tmp_path / "c"), str(tmp_path / "Main.java")],
check=True,
) # noqa: S603
res = subprocess.run(
[
str(jdk / "bin" / "java"),
"-Dfile.encoding=UTF-8",
"-Dstdout.encoding=UTF-8",
"-cp",
str(tmp_path / "c"),
"Main",
],
capture_output=True,
text=True,
encoding="utf-8",
check=True,
) # noqa: S603
out = dict(line.split("=", 1) for line in res.stdout.splitlines() if "=" in line)
assert len(out) == 6, res.stdout
assert all(v == "true" for v in out.values()), out
17 changes: 16 additions & 1 deletion tests/tools/equivalence_cics.py
Original file line number Diff line number Diff line change
Expand Up @@ -4559,6 +4559,20 @@ def commarea_class(case: dict[str, Any], src: Path, svc_file: Path) -> str | Non
if (got == raw) {
return true;
}
if (raw instanceof byte[] wide && got != null && got.getClass() == type) {
// #4695: a RETURN with a LENGTH past the DTO passes the COMMAREA as bytes (the region's EBCDIC); the facade
// answers its own DTO, those bytes read through its layout in the port's page -- never a different record
try {
java.nio.charset.Charset page = (java.nio.charset.Charset) Class.forName("__PKG__.entity.vsam.CobolRecords")
.getMethod("charset").invoke(null);
byte[] text = new String(wide, java.nio.charset.Charset.forName("IBM037")).getBytes(page);
Method bytes = type.getMethod("toCommarea");
Object expected = type.getMethod("fromCommarea", byte[].class).invoke(null, (Object) text);
return java.util.Arrays.equals((byte[]) bytes.invoke(got), (byte[]) bytes.invoke(expected));
} catch (ReflectiveOperationException | RuntimeException e) {
return false;
}
}
if (raw == null || got == null || type.isInstance(raw) || got.getClass() != type) {
return false;
}
Expand Down Expand Up @@ -4715,6 +4729,7 @@ def cics_equivalence_test(case: dict[str, Any], src: Path, files: list[dict[str,
recv = [f' if (r.has("{m}")) received.put("{m}", {pkg}.dto.screen.{cls}.fromValues('
f'json.convertValue(r.get("{m}"), new TypeReference<Map<String, String>>() {{ }})));'
for m, cls in screens.items()] # fmt: skip
facade_java = FACADE_JAVA.replace("__PKG__", pkg) # #4695: the page a facade's bytes answer is read in
return f"""package {pkg};

import com.fasterxml.jackson.core.type.TypeReference;
Expand Down Expand Up @@ -5009,7 +5024,7 @@ class EquivalenceRunTest {{
}}

{db2_methods}
{FACADE_JAVA}
{facade_java}
/** #4270: the scenario's TS queues, each item's text in the region's code page (DetCics.region). */
static CicsTask.TempStorage tempStorage(JsonNode sc) {{
CicsTask.TempStorage ts = new CicsTask.TempStorage();
Expand Down
Loading