mockymock 0.10.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. mockymock/__init__.py +1 -0
  2. mockymock/_lines.py +86 -0
  3. mockymock/analysis/__init__.py +0 -0
  4. mockymock/analysis/assertion_labels.py +132 -0
  5. mockymock/analysis/cics_eib.py +161 -0
  6. mockymock/analysis/dialect_risk.py +389 -0
  7. mockymock/analysis/exact_counts.py +210 -0
  8. mockymock/analysis/flow_mocks.py +411 -0
  9. mockymock/analysis/flow_targets.py +33 -0
  10. mockymock/analysis/linkage.py +357 -0
  11. mockymock/analysis/loops.py +125 -0
  12. mockymock/analysis/mock_surface.py +938 -0
  13. mockymock/analysis/refusal/__init__.py +349 -0
  14. mockymock/analysis/refusal/_explanations.py +372 -0
  15. mockymock/analysis/refusal/_gates.py +1840 -0
  16. mockymock/analysis/refusal/_plans.py +703 -0
  17. mockymock/analysis/refusal/_reachability.py +1369 -0
  18. mockymock/analysis/refusal/_types.py +136 -0
  19. mockymock/analysis/stub_expectations.py +225 -0
  20. mockymock/cli/__init__.py +0 -0
  21. mockymock/cli/ai_mode.py +372 -0
  22. mockymock/cli/checks.py +592 -0
  23. mockymock/cli/config.py +335 -0
  24. mockymock/cli/discovery.py +382 -0
  25. mockymock/cli/doctor.py +203 -0
  26. mockymock/cli/main.py +3990 -0
  27. mockymock/cli/mainframe_export.py +951 -0
  28. mockymock/cli/skills/cobol-analysis/SKILL.md +119 -0
  29. mockymock/cli/skills/cobol-test/SKILL.md +193 -0
  30. mockymock/cli/skills/cobol-test/reference/intent-assertions.md +72 -0
  31. mockymock/cli/skills/cobol-test/reference/report.md +92 -0
  32. mockymock/cli/skills/cobol-test/reference/verification-levels.md +40 -0
  33. mockymock/cli/skills/mockymock/SKILL.md +272 -0
  34. mockymock/cli/skills/mockymock/reference/cli-flags.md +528 -0
  35. mockymock/cli/skills/mockymock/reference/cut-dsl.md +451 -0
  36. mockymock/cli/skills/mockymock/reference/modes.md +179 -0
  37. mockymock/cli/skills/mockymock/reference/paragraph-first-workflow.md +279 -0
  38. mockymock/cli/skills/mockymock/reference/scenario-generation.md +144 -0
  39. mockymock/cli/skills_cmd.py +216 -0
  40. mockymock/debug/__init__.py +0 -0
  41. mockymock/debug/adapter.py +651 -0
  42. mockymock/debug/call_stack.py +227 -0
  43. mockymock/debug/gdb_session.py +671 -0
  44. mockymock/debug/line_map.py +68 -0
  45. mockymock/debug/variable_map.py +560 -0
  46. mockymock/dsl/__init__.py +1 -0
  47. mockymock/dsl/fixture_bundle.py +257 -0
  48. mockymock/dsl/generator.py +2589 -0
  49. mockymock/dsl/model.py +430 -0
  50. mockymock/dsl/parser.py +1317 -0
  51. mockymock/mutation/__init__.py +26 -0
  52. mockymock/mutation/operators.py +615 -0
  53. mockymock/mutation/report.py +227 -0
  54. mockymock/mutation/runner.py +628 -0
  55. mockymock/mutation/zero_baseline.py +251 -0
  56. mockymock/py.typed +0 -0
  57. mockymock/report/__init__.py +0 -0
  58. mockymock/report/coverage.py +377 -0
  59. mockymock/report/formatters.py +259 -0
  60. mockymock/report/html_report.py +1643 -0
  61. mockymock/report/journal.py +93 -0
  62. mockymock/report/json_report.py +327 -0
  63. mockymock/report/program_analysis.py +495 -0
  64. mockymock/report/record_rewrite.py +379 -0
  65. mockymock/report/results.py +185 -0
  66. mockymock/report/test_report.py +1352 -0
  67. mockymock/report/trace.py +136 -0
  68. mockymock/runtime/__init__.py +0 -0
  69. mockymock/runtime/compiler.py +19 -0
  70. mockymock/runtime/docker/Dockerfile +20 -0
  71. mockymock/runtime/docker_toolchain.py +631 -0
  72. mockymock/runtime/executor.py +37 -0
  73. mockymock/runtime/sandbox.py +38 -0
  74. mockymock/runtime/status.py +199 -0
  75. mockymock/splicer/__init__.py +0 -0
  76. mockymock/splicer/captures.py +359 -0
  77. mockymock/splicer/cics_eib.py +118 -0
  78. mockymock/splicer/cics_resp.py +234 -0
  79. mockymock/splicer/driver.py +337 -0
  80. mockymock/splicer/injector.py +519 -0
  81. mockymock/splicer/io_copy.py +167 -0
  82. mockymock/splicer/linkage.py +121 -0
  83. mockymock/splicer/naming.py +66 -0
  84. mockymock/splicer/orchestrator.py +293 -0
  85. mockymock/splicer/reset.py +280 -0
  86. mockymock/splicer/snapshot.py +212 -0
  87. mockymock/splicer/source_map.py +530 -0
  88. mockymock/splicer/sqlca.py +404 -0
  89. mockymock/splicer/stub_writer.py +74 -0
  90. mockymock/web/__init__.py +1 -0
  91. mockymock/web/plugin.py +190 -0
  92. mockymock/web/reports.py +114 -0
  93. mockymock/web/server.py +335 -0
  94. mockymock/web/static/app.js +438 -0
  95. mockymock/web/static/index.html +92 -0
  96. mockymock/web/static/style.css +410 -0
  97. mockymock-0.10.1.dist-info/METADATA +301 -0
  98. mockymock-0.10.1.dist-info/RECORD +102 -0
  99. mockymock-0.10.1.dist-info/WHEEL +5 -0
  100. mockymock-0.10.1.dist-info/entry_points.txt +2 -0
  101. mockymock-0.10.1.dist-info/licenses/LICENSE +235 -0
  102. mockymock-0.10.1.dist-info/top_level.txt +1 -0
mockymock/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.10.1" # x-release-please-version
mockymock/_lines.py ADDED
@@ -0,0 +1,86 @@
1
+ """Record-boundary line splitting for COBOL source text.
2
+
3
+ `str.splitlines()` breaks on every Unicode line boundary: not only `\\n`,
4
+ `\\r\\n` and `\\r` but also form feed (`\\x0c`, a page eject that legacy
5
+ sources carry inside comment lines), vertical tab, the `\\x1c`-`\\x1e`
6
+ separators, NEL (`\\x85`) and U+2028/2029. cobolparser splits a source's
7
+ records on `\\n` / `\\r\\n` / `\\r` only (its own `_lines.split_source_lines`),
8
+ so every `SourceLocation.line` it hands back counts records that way. A
9
+ mockymock stage that split the same text with `splitlines()` saw one more
10
+ line per such character, and every splice span after it landed one line
11
+ early -- the mock replaced the statement BEFORE the boundary and the real
12
+ CALL / file I/O still ran. Every split of COBOL source text (raw or
13
+ normalized) that is later indexed by an AST line number goes through here.
14
+
15
+ The rule that matters is *agreement with the installed cobolparser*, not
16
+ any one splitting rule: cobolparser releases before `cobolparser._lines`
17
+ existed (1.8.x and earlier, still allowed by the `>=1.7.7` floor) number
18
+ records the `str.splitlines()` way themselves. Splitting on record
19
+ delimiters only against one of those would shift every splice the other
20
+ way. So record-delimiter splitting is used exactly when the parser does
21
+ it, and `str.splitlines()` otherwise.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import re
27
+ from importlib.util import find_spec
28
+ from pathlib import Path
29
+
30
+ _LINE_BREAK_RE = re.compile(r"\r\n|\r|\n")
31
+
32
+ #: True when the installed cobolparser numbers records on `\\n` / `\\r\\n` /
33
+ #: `\\r` only (it ships `cobolparser._lines`); False for older releases,
34
+ #: which number them the `str.splitlines()` way.
35
+ PARSER_SPLITS_ON_RECORD_DELIMITERS = find_spec("cobolparser._lines") is not None
36
+
37
+
38
+ def split_source_lines(text: str) -> list[str]:
39
+ """Split `text` into lines the way the installed cobolparser numbers
40
+ them: on `\\n` / `\\r\\n` / `\\r` only when it does
41
+ (`PARSER_SPLITS_ON_RECORD_DELIMITERS`), otherwise `str.splitlines()`.
42
+ Either way: no trailing empty line for a terminating line break, `[]`
43
+ for `""`."""
44
+ if not PARSER_SPLITS_ON_RECORD_DELIMITERS:
45
+ return text.splitlines()
46
+ if not text:
47
+ return []
48
+ parts = _LINE_BREAK_RE.split(text)
49
+ if parts[-1] == "":
50
+ parts.pop()
51
+ return parts
52
+
53
+
54
+ class SourceEncodingError(ValueError):
55
+ """A `.cbl` / `.cut` input that is not valid UTF-8.
56
+
57
+ mockymock refuses rather than guessing a legacy code page: the spliced
58
+ copy is written back as UTF-8 and compiled, so a cp1252 `é` inside a
59
+ `PIC X` literal would silently become two bytes and change the field's
60
+ length (the copybook path refuses a lossy decode the same way, as
61
+ EXPANSION_WARNING). The message names the file, the byte offset and
62
+ the conversion to run, so the CLI can print it instead of a traceback.
63
+ """
64
+
65
+ def __init__(self, path: object, exc: UnicodeDecodeError) -> None:
66
+ self.path = path
67
+ self.offset = exc.start
68
+ super().__init__(
69
+ f"{path} is not valid UTF-8 (byte 0x{exc.object[exc.start]:02x} at "
70
+ f"offset {exc.start}); mockymock reads COBOL sources and .cut "
71
+ f"files as UTF-8 -- convert the file first, e.g. "
72
+ f"`iconv -f cp1252 -t utf-8 {path}` for a Windows code-page source"
73
+ )
74
+
75
+
76
+ def read_utf8_source(path: Path, *, keep_newlines: bool = False) -> str:
77
+ """`path.read_text(encoding="utf-8")`, raising `SourceEncodingError`
78
+ (not a bare UnicodeDecodeError) when the file is not UTF-8.
79
+ `keep_newlines=True` skips universal-newline translation, so a caller
80
+ that rewrites the file in place can see (and keep) its CRLFs."""
81
+ try:
82
+ if keep_newlines:
83
+ return path.read_bytes().decode("utf-8")
84
+ return path.read_text(encoding="utf-8")
85
+ except UnicodeDecodeError as exc:
86
+ raise SourceEncodingError(path, exc) from exc
File without changes
@@ -0,0 +1,132 @@
1
+ """Where each `.cut` assertion's expected value came from.
2
+
3
+ An `EXPECT`/`VERIFY` is only as good as the reason its value is the right
4
+ one. Two reasons are worth telling apart, and the `cobol-test` skill (and
5
+ `mockymock report`) asks for them to be written down as a comment line
6
+ directly above the assertions they cover:
7
+
8
+ *> INTENT: BR-003 orders over 1000.00 get 5% off (ORDRPROC.cbl:212)
9
+ EXPECT WS-NET-AMOUNT TO BE 950.00
10
+
11
+ *> CHARACTERIZATION: recorded by run --record-expectations
12
+ EXPECT WS-AUDIT-COUNT TO BE 3
13
+
14
+ INTENT means the value was derived from a business rule, a spec, or the
15
+ developer -- a mismatch is a suspected program bug. CHARACTERIZATION means
16
+ the value is whatever the program produced on some run -- it pins current
17
+ behavior and proves nothing about correctness.
18
+
19
+ A label covers every EXPECT/VERIFY line that follows it, up to the first
20
+ blank line, the next label, the next TESTCASE, or any other directive.
21
+ Other comment lines in between (a `*> STUB EXPECT`, a note) do not end it.
22
+ An assertion no label covers is reported as unlabelled.
23
+
24
+ Pure text analysis: works on any `.cut` that `parse_cut` accepts, needs no
25
+ program and no Docker.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import re
31
+ from dataclasses import dataclass, field
32
+ from typing import Any
33
+
34
+ INTENT = "intent"
35
+ CHARACTERIZATION = "characterization"
36
+
37
+ _LABEL_RE = re.compile(
38
+ r"^\*>\s*(?P<kind>INTENT|CHARACTERI[SZ]ATION)\b\s*:?\s*(?P<note>.*)$",
39
+ re.IGNORECASE,
40
+ )
41
+ _TESTCASE_RE = re.compile(r'^TESTCASE\s+"(?P<name>(?:[^"]|"")*)"', re.IGNORECASE)
42
+ _ASSERTION_RE = re.compile(r"^(?P<kind>EXPECT|VERIFY)\b", re.IGNORECASE)
43
+ _RULE_ID_RE = re.compile(r"\bBR-\d+\b", re.IGNORECASE)
44
+
45
+ # The comment line `--record-expectations` writes above the EXPECTs it
46
+ # recorded (see report/record_rewrite.py), so recorded values are labelled
47
+ # without anyone having to remember to.
48
+ RECORDED_LABEL = "*> CHARACTERIZATION: recorded by mockymock run --record-expectations"
49
+
50
+
51
+ @dataclass
52
+ class LabelledAssertion:
53
+ case_name: str
54
+ line: int
55
+ kind: str # "expect" | "verify"
56
+ text: str
57
+ label: str | None = None # INTENT | CHARACTERIZATION | None
58
+ note: str = ""
59
+ label_line: int | None = None
60
+ rule_ids: list[str] = field(default_factory=list)
61
+
62
+ def to_dict(self) -> dict[str, Any]:
63
+ return {
64
+ "case": self.case_name,
65
+ "line": self.line,
66
+ "kind": self.kind,
67
+ "text": self.text,
68
+ "label": self.label,
69
+ "note": self.note,
70
+ "labelLine": self.label_line,
71
+ "ruleIds": list(self.rule_ids),
72
+ }
73
+
74
+
75
+ def scan_assertion_labels(cut_text: str) -> list[LabelledAssertion]:
76
+ """Every EXPECT/VERIFY inside a TESTCASE, in file order, with the label
77
+ covering it (if any). Assertions outside any TESTCASE (a malformed file)
78
+ are skipped -- they belong to no case the report could attribute."""
79
+ results: list[LabelledAssertion] = []
80
+ case_name: str | None = None
81
+ label: tuple[str, str, int] | None = None # (kind, note, line)
82
+
83
+ for line_no, raw in enumerate(cut_text.splitlines(), start=1):
84
+ stripped = raw.strip()
85
+ if not stripped:
86
+ label = None
87
+ continue
88
+ testcase = _TESTCASE_RE.match(stripped)
89
+ if testcase:
90
+ case_name = testcase.group("name").replace('""', '"')
91
+ label = None
92
+ continue
93
+ labelled = _LABEL_RE.match(stripped)
94
+ if labelled:
95
+ kind = labelled.group("kind").upper()
96
+ label = (
97
+ INTENT if kind == "INTENT" else CHARACTERIZATION,
98
+ labelled.group("note").strip(),
99
+ line_no,
100
+ )
101
+ continue
102
+ if stripped.startswith("*>"):
103
+ continue
104
+ assertion = _ASSERTION_RE.match(stripped)
105
+ if assertion is None:
106
+ label = None
107
+ continue
108
+ if case_name is None:
109
+ continue
110
+ item = LabelledAssertion(
111
+ case_name=case_name,
112
+ line=line_no,
113
+ kind=assertion.group("kind").lower(),
114
+ text=stripped,
115
+ )
116
+ if label is not None:
117
+ item.label, item.note, item.label_line = label
118
+ item.rule_ids = sorted(
119
+ {match.upper() for match in _RULE_ID_RE.findall(item.note)}
120
+ )
121
+ results.append(item)
122
+ return results
123
+
124
+
125
+ def summarize_labels(items: list[LabelledAssertion]) -> dict[str, int]:
126
+ """Counts by label: the numbers the report's headline shows."""
127
+ return {
128
+ "total": len(items),
129
+ INTENT: sum(1 for i in items if i.label == INTENT),
130
+ CHARACTERIZATION: sum(1 for i in items if i.label == CHARACTERIZATION),
131
+ "unlabelled": sum(1 for i in items if i.label is None),
132
+ }
@@ -0,0 +1,161 @@
1
+ """Detects whether a program references the CICS Execute Interface Block
2
+ (`DFHEIBLK`) or any of its named fields (`EIBCALEN`, `EIBAID`, ...) without
3
+ declaring them anywhere itself -- see
4
+ docs/2026-08-27-cics-eib-linkage-support-design.md.
5
+
6
+ A real CICS translator inserts `DFHEIBLK`'s LINKAGE SECTION layout ahead of
7
+ `cobc`/the native compiler ever seeing the source; mockymock never runs a
8
+ CICS translator (`splicer.injector.build_exec_trap` already avoids needing
9
+ one for `EXEC CICS` command syntax itself, see the design doc's problem
10
+ statement), so a program referencing an EIB field from ordinary COBOL
11
+ outside any `EXEC CICS` block would otherwise fail `cobc` compilation with
12
+ an undeclared-data-name error.
13
+
14
+ Detection here is a pure, side-effect-free query, mirroring
15
+ `analysis.linkage.linkage_plan`'s split from its own guardrail check:
16
+ `cics_eib_plan` answers "is there anything to do" (`None` when the program
17
+ never references any EIB name, or when it already declares its own copy --
18
+ an ordinary non-CICS program, or a program that hand-declares/COPYs
19
+ `DFHEIBLK` itself, either way untouched by this feature). It does not decide
20
+ whether OTHER CICS constructs in the program are safe (`DFHRESP`/`DFHVALUE`
21
+ macro calls, an undeclared `DFHCOMMAREA`) -- that is
22
+ `refusal.check_cics_eib_layout`'s job.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import re
28
+ from dataclasses import dataclass, field
29
+
30
+ from cobolparser.analysis.unused_variables import collect_declared_items
31
+
32
+ DFHEIBLK_NAME = "DFHEIBLK"
33
+
34
+ # The standard IBM CICS Execute Interface Block field names, unchanged
35
+ # across CICS TS releases. See the design doc's "The DFHEIBLK layout"
36
+ # section for the full PICTURE clauses -- this module only needs the names,
37
+ # for reference detection.
38
+ EIB_FIELD_NAMES: frozenset[str] = frozenset(
39
+ {
40
+ "EIBTIME",
41
+ "EIBDATE",
42
+ "EIBTRNID",
43
+ "EIBTASKN",
44
+ "EIBTRMID",
45
+ "EIBCPOSN",
46
+ "EIBCALEN",
47
+ "EIBAID",
48
+ "EIBFN",
49
+ "EIBRCODE",
50
+ "EIBDS",
51
+ "EIBREQID",
52
+ "EIBRSRCE",
53
+ "EIBSYNC",
54
+ "EIBFREE",
55
+ "EIBRECV",
56
+ "EIBATT",
57
+ "EIBEOC",
58
+ "EIBFMH",
59
+ "EIBCOMPL",
60
+ "EIBSIG",
61
+ "EIBCONF",
62
+ "EIBERR",
63
+ "EIBERRCD",
64
+ "EIBSYNRB",
65
+ "EIBNODAT",
66
+ "EIBRESP",
67
+ "EIBRESP2",
68
+ "EIBRLDBK",
69
+ }
70
+ )
71
+
72
+ _EIB_NAMES = EIB_FIELD_NAMES | {DFHEIBLK_NAME}
73
+ # Plain `\b...\b` is wrong here: COBOL identifiers may contain hyphens,
74
+ # which are not `\w` characters, so `\bEIBCALEN\b` would false-match inside
75
+ # an unrelated hyphenated name like `WS-EIBCALEN-SAVED` (a `\b` boundary
76
+ # exists at the transition between the non-word `-` and the word-char `E`).
77
+ # Use an explicit lookaround against the full COBOL identifier character
78
+ # class (letters/digits/hyphen) instead, and match case-insensitively --
79
+ # COBOL identifiers are themselves case-insensitive, so a lowercase or
80
+ # mixed-case source reference is exactly as real a reference as an
81
+ # uppercase one.
82
+ # One combined alternation rather than 29 separate patterns: each pattern
83
+ # starts with a lookbehind, which defeats `re`'s literal-prefix scan
84
+ # optimization, so every name cost a full pass over the program text --
85
+ # measured at 0.73s of a 2.5s static-check pass on a 29k-line program, ~15x
86
+ # `refusal.check_cics_eib_layout`'s two-pattern equivalent. Longest name
87
+ # first so a prefix never shadows a longer sibling (EIBRESP vs EIBRESP2);
88
+ # occurrences cannot overlap, since the lookarounds delimit every match
89
+ # with a non-identifier character on both sides.
90
+ _EIB_NAME_RE = re.compile(
91
+ "(?<![A-Za-z0-9-])(?:"
92
+ + "|".join(sorted((re.escape(n) for n in _EIB_NAMES), key=len, reverse=True))
93
+ + ")(?![A-Za-z0-9-])",
94
+ re.IGNORECASE,
95
+ )
96
+
97
+
98
+ # The EXEC DLI interface block (DIB). Like the EIB, the DL/I translator
99
+ # normally supplies it, so a program testing DIBSTAT after an EXEC DLI call
100
+ # did not compile under mockymock -- behind a clean lint (issue #145).
101
+ DLIDIB_NAME = "DLIDIB"
102
+ DIB_FIELD_NAMES: frozenset[str] = frozenset(
103
+ {
104
+ "DIBVER",
105
+ "DIBSTAT",
106
+ "DIBSEGM",
107
+ "DIBSEGLV",
108
+ "DIBKFBL",
109
+ "DIBDBDNM",
110
+ "DIBDBORG",
111
+ }
112
+ )
113
+ _DIB_NAMES = DIB_FIELD_NAMES | {DLIDIB_NAME}
114
+ _DIB_NAME_RE = re.compile(
115
+ "(?<![A-Za-z0-9-])(?:"
116
+ + "|".join(sorted((re.escape(n) for n in _DIB_NAMES), key=len, reverse=True))
117
+ + ")(?![A-Za-z0-9-])",
118
+ re.IGNORECASE,
119
+ )
120
+
121
+
122
+ @dataclass
123
+ class CicsEibPlan:
124
+ """Everything `splicer.cics_eib`'s synthesis functions need.
125
+
126
+ `referenced_names` is sorted, transparency-message-only: synthesis
127
+ always emits the full fixed `DFHEIBLK` layout regardless of which
128
+ specific fields matched (see module docstring on the design doc's
129
+ reasoning -- a real translator always inserts the whole block).
130
+ """
131
+
132
+ referenced_names: list[str]
133
+ #: DL/I interface block names referenced and undeclared (issue #145);
134
+ #: empty when the program needs no DIB. Synthesized the same way, as a
135
+ #: `01 DLIDIB.` group.
136
+ dib_names: list[str] = field(default_factory=list)
137
+
138
+
139
+ def cics_eib_plan(program, lines: list[str]) -> CicsEibPlan | None:
140
+ """`None` when there is nothing for this feature to do: no EIB name
141
+ referenced anywhere in `lines`, or every referenced name is already
142
+ declared somewhere in the program's own DATA DIVISION (a hand-declared
143
+ or `COPY DFHEIBLK`-expanded copy -- synthesizing a second one would be a
144
+ duplicate-data-name compile error, and the program already has real
145
+ storage for it).
146
+ """
147
+ text = "\n".join(lines)
148
+ referenced = sorted({m.group(0).upper() for m in _EIB_NAME_RE.finditer(text)})
149
+ dib = sorted({m.group(0).upper() for m in _DIB_NAME_RE.finditer(text)})
150
+ if not referenced and not dib:
151
+ return None
152
+
153
+ declared = {item.name.upper() for item in collect_declared_items(program)}
154
+ if declared & set(referenced):
155
+ referenced = []
156
+ if declared & _DIB_NAMES:
157
+ dib = []
158
+ if not referenced and not dib:
159
+ return None
160
+
161
+ return CicsEibPlan(referenced_names=referenced, dib_names=dib)