"""Characterization tests for the POSTTRAN port: every expected value comes from the fixture tools/characterize.mjs captured on SteelFrame (the legacy side ran; nothing here is derived from reading the COBOL). python -m unittest carddemo-posttran/python/test_posttran.py # stdlib pytest carddemo-posttran/python # also fine FIXTURE=path/to/fixture.json python -m unittest ... # a fixture you captured yourself """ import json import os import unittest from decimal import Decimal from posttran import D, post_transactions, proc_ts_from_clock, reject_record_bytes, tcat_key HERE = os.path.dirname(os.path.abspath(__file__)) FIXTURE = os.environ.get("FIXTURE") or os.path.join(HERE, "..", "fixtures", "posttran-d002", "fixture.json") # the fixture keys datasets by the program's DD names (CBTRN02C's SELECT ... ASSIGN TO), # so a capture with the written files under any HLQ reads the same DALYTRAN = "DALYTRAN" XREF = "XREFFILE" ACCT = "ACCTFILE" TCATBAL = "TCATBALF" TRANSACT = "TRANFILE" REJECTS = "DALYREJS" def flat(row): """Nested groups -> one level; numbers -> Decimal; text right-trimmed (as the engine decodes).""" out = {} for k, v in row.items(): if isinstance(v, dict): out.update(flat(v)) elif isinstance(v, (int, float, Decimal)) or (isinstance(v, str) and v.strip().lstrip("-").replace(".", "", 1).isdigit() and k not in TEXT_FIELDS): out[k] = D(v) else: out[k] = v.rstrip() if isinstance(v, str) else v return out # fields that look numeric but are PIC X in the copybooks (keep them as text) TEXT_FIELDS = {"TRAN-ID", "DALYTRAN-ID", "TRAN-CARD-NUM", "DALYTRAN-CARD-NUM", "XREF-CARD-NUM", "TRAN-MERCHANT-ZIP", "DALYTRAN-MERCHANT-ZIP", "ACCT-ADDR-ZIP", "TRAN-TYPE-CD", "DALYTRAN-TYPE-CD", "TRANCAT-TYPE-CD"} class PosttranCharacterization(unittest.TestCase): @classmethod def setUpClass(cls): with open(FIXTURE, encoding="utf-8") as f: cls.fx = json.load(f) assert cls.fx["format"] == "steelframe-fixture/1", "not a steelframe-fixture/1" ds = cls.fx["datasets"] cls.daily = ds[DALYTRAN]["before"]["rows"] cls.daily_hex = ds[DALYTRAN]["before"]["hex"] assert REJECTS in ds and not ds[REJECTS].get("missing"), "the fixture has no DALYREJS generation" cls.rejs_key = REJECTS cls.result = post_transactions( daily=cls.daily, xref=ds[XREF]["before"]["rows"], accounts=ds[ACCT]["before"]["rows"], tcatbal=ds[TCATBAL]["before"]["rows"], proc_ts=proc_ts_from_clock(cls.fx["source"]["clock"]), ) def test_return_code(self): self.assertEqual(self.result.return_code, int(self.fx["rc"]["maxcc"])) self.assertEqual([s["rc"] for s in self.fx["rc"]["steps"] if s["name"] == "STEP15"], [self.result.return_code]) def test_sysout_lines(self): expected = [l.rstrip() for l in self.fx["joblog"]["STEP15.SYSOUT"] if l.strip()] self.assertEqual(self.result.sysout, expected) def test_transactions_written(self): expected = [flat(r) for r in self.fx["datasets"][TRANSACT]["after"]["rows"]] got = [flat(r) for r in self.result.transactions] self.assertEqual(got, expected) def test_account_master_after(self): expected = {int(flat(r)["ACCT-ID"]): flat(r) for r in self.fx["datasets"][ACCT]["after"]["rows"]} got = {k: flat(v) for k, v in self.result.accounts.items()} self.assertEqual(set(got), set(expected)) for k in expected: self.assertEqual(got[k], expected[k], f"account {k}") def test_category_balances_after(self): expected = {} for r in self.fx["datasets"][TCATBAL]["after"]["rows"]: k = r["TRAN-CAT-KEY"] expected[tcat_key(k["TRANCAT-ACCT-ID"], k["TRANCAT-TYPE-CD"], k["TRANCAT-CD"])] = flat(r) got = {k: flat(v) for k, v in self.result.tcatbal.items()} self.assertEqual(got, expected) def test_reject_file_bytes(self): """DALYREJS has no copybook on the estate, so the oracle gave bytes: compare bytes.""" rejs = self.fx["datasets"][self.rejs_key] self.assertEqual(rejs["charset"], "ebcdic") expected = rejs["after"]["hex"] got = [reject_record_bytes(bytes.fromhex(self.daily_hex[i]), reason, desc).hex().upper() for i, reason, desc in self.result.rejects] self.assertEqual([h.upper() for h in expected], got) self.assertEqual(len(got), self.result.rejected) def test_roles_the_oracle_reported(self): ds = self.fx["datasets"] self.assertEqual(ds[XREF]["role"], "input") # read only self.assertEqual(ds[DALYTRAN]["role"], "input") # read only self.assertEqual(ds[ACCT]["role"], "updated") self.assertEqual(ds[TCATBAL]["role"], "updated") self.assertEqual(ds[self.rejs_key]["role"], "output") # the new GDG generation if __name__ == "__main__": unittest.main()