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
29 changes: 25 additions & 4 deletions gitgalaxy/tools/cobol_to_java/cobol_to_java_batch_forge.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,10 @@ def sources(self) -> dict[tuple[str, ...], dict[str, str]]:
if not self.applications:
return {}
pkg = f"{self.package}.{SUBPACKAGE}"
out = {name: text.replace("{pkg}", pkg) for name, text in _RUNTIME.items()}
out = {
name: text.replace("{pkg}", pkg).replace("{zone}", self.target.culture.zone)
for name, text in _RUNTIME.items()
}
if self.target.features.rest_controllers:
out["BatchJobController"] = _CONTROLLER.replace("{pkg}", pkg)
for j in self.applications:
Expand Down Expand Up @@ -380,23 +383,41 @@ def audit_line(self) -> str:
"MainframeClock": """package {pkg};

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

/** The time a ported program reads -- FUNCTION CURRENT-DATE, ACCEPT FROM DATE / TIME, EXEC CICS ASKTIME
* (#3753): `gitgalaxy.clock` (an ISO local date-time, e.g. 2022-07-18T10:30:15.00) when set -- the
* equivalence harness pins it, so a run can be compared with the original's -- else the system clock. */
* equivalence harness pins it, so a run can be compared with the original's -- else the system clock.
* #3824: the mainframe's zone is `gitgalaxy.zone` (default {zone}, the target's culture.zone), never the JVM's;
* currentDate() carries its UTC offset. A pinned local time that does not exist in the zone (a DST gap, e.g.
* 2022-03-27T02:30 in Europe/Berlin) resolves by ZonedDateTime.ofLocal's rule: it moves forward. */
@Component
public class MainframeClock {

private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSZ");
private final String pinned;
private final ZoneId zone;

public MainframeClock(@Value("${gitgalaxy.clock:}") String pinned) {
public MainframeClock(@Value("${gitgalaxy.clock:}") String pinned,
@Value("${gitgalaxy.zone:{zone}}") String zoneId) {
this.pinned = pinned == null ? "" : pinned.trim();
this.zone = ZoneId.of(zoneId == null || zoneId.trim().isEmpty() ? "{zone}" : zoneId.trim());
}

public LocalDateTime now() {
return pinned.isEmpty() ? LocalDateTime.now() : LocalDateTime.parse(pinned);
return zonedNow().toLocalDateTime();
}

public ZonedDateTime zonedNow() {
return pinned.isEmpty() ? ZonedDateTime.now(zone) : ZonedDateTime.of(LocalDateTime.parse(pinned), zone);
}

public String currentDate() {
return zonedNow().format(FORMATTER);
}
}
""",
Expand Down
6 changes: 3 additions & 3 deletions gitgalaxy/tools/cobol_to_java/cobol_to_java_db2_forge.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,15 @@
"SMALLINT": "Integer", "INTEGER": "Integer", "INT": "Integer", "BIGINT": "Long",
"DECIMAL": "BigDecimal", "DEC": "BigDecimal", "NUMERIC": "BigDecimal",
"REAL": "Double", "FLOAT": "Double", "DOUBLE": "Double", "DOUBLE PRECISION": "Double", "DECFLOAT": "BigDecimal",
"DATE": "LocalDate", "TIME": "LocalTime", "TIMESTAMP": "LocalDateTime",
"DATE": "LocalDate", "TIME": "LocalTime", "TIMESTAMP": "LocalDateTime", "TIMESTAMP WITH TIME ZONE": "OffsetDateTime",
"BLOB": "byte[]", "BINARY": "byte[]", "VARBINARY": "byte[]",
} # fmt: skip
_TIME_IMPORTS = {"LocalDate": "java.time.LocalDate", "LocalTime": "java.time.LocalTime",
"LocalDateTime": "java.time.LocalDateTime"} # fmt: skip
"LocalDateTime": "java.time.LocalDateTime", "OffsetDateTime": "java.time.OffsetDateTime"} # fmt: skip


def sql_java_type(sql_type: str) -> str:
"""The Java type of a declared DB2 column type (`TIMESTAMP WITH TIME ZONE` -> LocalDateTime)."""
"""The Java type of a declared DB2 column type (`TIMESTAMP WITH TIME ZONE` -> OffsetDateTime)."""
t = " ".join(sql_type.upper().split())
for name in sorted(_SQL_TYPES, key=len, reverse=True):
if t == name or t.startswith(name + " ") or t.startswith(name + "("):
Expand Down
5 changes: 3 additions & 2 deletions gitgalaxy/tools/cobol_to_java/cobol_to_java_port_tickets.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,9 @@
"as moved, an edited PICTURE formatted as COBOL formats it."
),
(
"Read the time only from the generated batch runtime's MainframeClock (now()), never from the "
"system clock directly: it is how a run is pinned to be compared with the original."
"Read the time only from the generated batch runtime's MainframeClock (now() for local, currentDate() for "
"FUNCTION CURRENT-DATE), never from the system clock directly or ZoneId.systemDefault(): it is how a run is "
"pinned to be compared with the original."
),
(
"A fact whose field testing is not 'field-tested' is verified on reference estates but still being "
Expand Down
2 changes: 1 addition & 1 deletion gitgalaxy/tools/cobol_to_java/java_target.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ class Culture:

rounding: str = "cobol" # TODO(#3825): ROUNDED / ROUNDED MODE in generated arithmetic and porting rules
overflow: str = "cobol" # TODO(#3825): high-order truncation vs ON SIZE ERROR
zone: str = "UTC" # TODO(#3824): MainframeClock's zone and CURRENT-DATE's UTC offset
zone: str = "UTC" # #3824: MainframeClock's zone and CURRENT-DATE's UTC offset
format_locale: str = "ROOT" # generated formatting / case mapping (#3823 made it Locale.ROOT)
display_locale: str = "ROOT" # TODO(#3819): screens only -- never used for stored data
decimal_point: str = "auto" # TODO(#3827): DECIMAL-POINT IS COMMA in edited PICTUREs and schemas
Expand Down
2 changes: 1 addition & 1 deletion tests/cobol_mainframe/test_db2_repositories.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ def test_to_jdbc_names_parameters_and_drops_the_singleton_into():
@pytest.mark.parametrize(
"sql_type, java",
[("CHAR", "String"), ("VARCHAR", "String"), ("DECIMAL", "BigDecimal"), ("INTEGER", "Integer"),
("BIGINT", "Long"), ("DATE", "LocalDate"), ("TIMESTAMP WITH TIME ZONE", "LocalDateTime"), ("BLOB", "byte[]"),
("BIGINT", "Long"), ("DATE", "LocalDate"), ("TIMESTAMP WITH TIME ZONE", "OffsetDateTime"), ("BLOB", "byte[]"),
("SOMETHING ODD", "String")],
) # fmt: skip
def test_sql_types_map_to_java(sql_type, java):
Expand Down
128 changes: 128 additions & 0 deletions tests/cobol_mainframe/test_time_zones.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import shutil
import subprocess
import sys
from pathlib import Path

import pytest

from gitgalaxy.tools.cobol_to_java.cobol_to_java_batch_forge import BatchForge
from gitgalaxy.tools.cobol_to_java.cobol_to_java_db2_forge import sql_java_type
from gitgalaxy.tools.cobol_to_java.java_target import load_target

sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "tests" / "tools"))
import equivalence as eq
from equivalence_java import equivalence_test


def get_forge(target):
return BatchForge(
{"P": {"program": {"file": "f", "program_ids": ["PROG"]}}},
{
"sections": {
"job_steps": {"facts": [{"file": "f", "steps": [{"step": "s", "ordinal": 1, "program": "PROG"}]}]}
}
},
"com.gitgalaxy",
target,
)


def test_mainframe_clock_generation():
target = load_target(None)
target.culture.zone = "Asia/Kolkata"
forge = get_forge(target)
src = forge.sources()[("base_pkg", "batch")]["MainframeClock"]
assert "gitgalaxy.zone:Asia/Kolkata" in src
assert "ZonedDateTime zonedNow()" in src
assert "String currentDate()" in src


def test_mainframe_clock_default_zone():
target = load_target(None)
forge = get_forge(target)
src = forge.sources()[("base_pkg", "batch")]["MainframeClock"]
assert "gitgalaxy.zone:UTC" in src


def test_db2_timestamp_types():
assert sql_java_type("TIMESTAMP WITH TIME ZONE") == "OffsetDateTime"
assert sql_java_type("TIMESTAMP") == "LocalDateTime"


def test_equivalence_harness_zone_plumbing(tmp_path, monkeypatch):
case_with_zone = {
"name": "c",
"program": "P",
"datasets": {},
"clock": "2022/01/01 12:00:00.00",
"zone": "Europe/Berlin",
}
case_no_zone = {"name": "c", "program": "P", "datasets": {}, "clock": "2022/01/01 12:00:00.00"}

java_src = equivalence_test(case_with_zone)
assert '"gitgalaxy.zone=Europe/Berlin"' in java_src

java_src_no = equivalence_test(case_no_zone)
assert '"gitgalaxy.zone=UTC"' in java_src_no

def fake_run(*args, **kwargs):
work_dir = args[0][4].split(":")[0]
(Path(work_dir) / "RETURN-CODE").write_text("0")

class Proc:
returncode = 0
stdout = ""
stderr = ""

return Proc()

monkeypatch.setattr(eq.subprocess, "run", fake_run)

case_with_zone["program_source"] = "P.cbl"
corpus = tmp_path / "corpus"
corpus.mkdir()
(corpus / "P.cbl").write_text(" ")

work = tmp_path / "work"
eq.run_cobol(case_with_zone, corpus, work)
script = (work / "run.sh").read_text()
assert "TZ='Europe/Berlin'" in script

work2 = tmp_path / "work2"
case_no_zone["program_source"] = "P.cbl"
eq.run_cobol(case_no_zone, corpus, work2)
script2 = (work2 / "run.sh").read_text()
assert "TZ=" not in script2


@pytest.mark.skipif(not shutil.which("javac"), reason="No JDK")
def test_mainframe_clock_compilation_and_run(tmp_path):
target = load_target(None)
target.culture.zone = "Asia/Kolkata"
forge = get_forge(target)
src = forge.sources()[("base_pkg", "batch")]["MainframeClock"]

src = src.replace("@Component", "")
src = src.replace('@Value("${gitgalaxy.clock:}")', "")
src = src.replace('@Value("${gitgalaxy.zone:Asia/Kolkata}")', "")
src = src.replace("import org.springframework.beans.factory.annotation.Value;", "")
src = src.replace("import org.springframework.stereotype.Component;", "")

main = """
public static void main(String[] args) {
MainframeClock clock = new MainframeClock("2022-07-18T10:30:15.00", "Asia/Kolkata");
System.out.println(clock.currentDate());
}
"""
src = src.replace("public class MainframeClock {", "public class MainframeClock {" + main)

pkg_dir = tmp_path / "com" / "gitgalaxy" / "batch"
pkg_dir.mkdir(parents=True)
java_file = pkg_dir / "MainframeClock.java"
java_file.write_text(src)

subprocess.run(["javac", str(java_file)], check=True)
res = subprocess.run(
["java", "-cp", str(tmp_path), "com.gitgalaxy.batch.MainframeClock"], capture_output=True, text=True, check=True
)
assert res.stdout.strip() == "2022071810301500+0530"
3 changes: 2 additions & 1 deletion tests/tools/equivalence.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,9 @@ def run_cobol(case: dict[str, Any], corpus: Path, work: Path) -> dict[str, bytes
script.append(f"cobc -x {flags} -o program src/EQDRIVER.cbl src/PROGRAM.cbl")
env = " ".join(f"{dd}=/work/{dd}.idx" for dd in case["datasets"])
clock = f"COB_CURRENT_DATE='{case['clock']}' " if case.get("clock") else ""
tz = f"TZ='{case['zone']}' " if case.get("zone") else ""
# the step's RETURN-CODE is an output like any other (CBTRN02C sets 4 when it rejects): recorded, not fatal
script.append(f"set +e; {clock}{env} ./program > /work/stdout.txt 2>&1; echo $? > /work/RETURN-CODE; set -e")
script.append(f"set +e; {tz}{clock}{env} ./program > /work/stdout.txt 2>&1; echo $? > /work/RETURN-CODE; set -e")
for dd, spec in case["datasets"].items():
if spec.get("compare") and spec.get("organization") == "indexed":
script.append(f"{dd}=/work/{dd}.idx OUTFILE=/work/{dd}.out ./ul{dd}")
Expand Down
2 changes: 1 addition & 1 deletion tests/tools/equivalence_java.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ def equivalence_test(case: dict[str, Any]) -> str:

/** #3624: the equivalence run of {case["program"]} ({case["name"]}) -- generated by tests/tools/equivalence_java.py. */
@SpringBootTest(properties = {{"spring.jpa.show-sql=false", "gitgalaxy.clock={_clock(case)}",
"gitgalaxy.datasets.directory=${{equivalence.datasets}}"}})
"gitgalaxy.zone={case.get("zone", "UTC")}", "gitgalaxy.datasets.directory=${{equivalence.datasets}}"}})
class EquivalenceRunTest {{

static final Charset TEXT = StandardCharsets.ISO_8859_1;
Expand Down
Loading