forge: check_lint DRY refactor - extract _write_json_file from 2 duplicated json.dump blocks, verify behavior-preservation (8/8 checks pass)
This commit is contained in:
1 parent
a960d5a700
commit
6880cab214
2 files changed
+337
No files matched your search
@@ -0,0 +1,121 @@
|
||||
# check_lint.py DRY refactor verification (forge lane: Refactor)
|
||||
|
||||
## The shipped artifact
|
||||
`tools/check_lint.py` -- the per-tool lint/check runner. Its `--selftest`
|
||||
runs a 37-cell census (fixtures + portability + `--json` + `--check` arms); the
|
||||
`--check --json` path writes a classification dict to disk; the `report()`
|
||||
`--json` path writes the full run summary to disk.
|
||||
|
||||
## The smell (what the refactor fixes)
|
||||
JSON file-writing was inlined at two sites with the SAME try/except block,
|
||||
copied verbatim:
|
||||
|
||||
1. `report()` --json path (writes the full run summary to `as_json`)
|
||||
2. `main()` `--check --json` branch (writes the single-classification dict to `a.json`)
|
||||
|
||||
Both blocks were byte-identical in structure:
|
||||
|
||||
```python
|
||||
try:
|
||||
with open(<path>, "w", encoding="utf-8") as fh:
|
||||
json.dump(<obj>, fh, indent=2)
|
||||
fh.write("\n")
|
||||
except OSError as exc:
|
||||
print("cannot write --json " + <path> + ": " + str(exc), file=sys.stderr)
|
||||
return 2
|
||||
```
|
||||
|
||||
Two copies of one 7-line statement is the canonical DRY smell; each new JSON
|
||||
output site would be one more freehand copy away from drifting. The block is
|
||||
context-free (no closure over loop state), making it a safe extraction target.
|
||||
|
||||
## The refactor (working tree, uncommitted)
|
||||
A single module-level helper replaces both inlined blocks:
|
||||
|
||||
```python
|
||||
def _write_json_file(path: str, obj) -> int:
|
||||
"""Write obj as indented JSON to path. Returns 0 on success, 2 on OSError.
|
||||
|
||||
Extracted from the two sites that previously hand-duplicated the
|
||||
open/dump/write/newline/except block: report() and the --check/--json
|
||||
branch in main(). Both used indent=2 with default sort_keys=False.
|
||||
"""
|
||||
try:
|
||||
with open(path, "w", encoding="utf-8") as fh:
|
||||
json.dump(obj, fh, indent=2, sort_keys=False)
|
||||
fh.write("\n")
|
||||
except OSError as exc:
|
||||
print("cannot write --json " + path + ": " + str(exc), file=sys.stderr)
|
||||
return 2
|
||||
return 0
|
||||
```
|
||||
|
||||
Both call sites are replaced by:
|
||||
|
||||
```python
|
||||
rc = _write_json_file(<path>, <obj>)
|
||||
if rc:
|
||||
return rc
|
||||
```
|
||||
|
||||
Same open mode, same `indent=2`, same `sort_keys=False` (the default
|
||||
`--check` branch relied on), same `"\n"` terminator, same error message, same
|
||||
rc 2. Behavior is unchanged; the duplicated statement is collapsed into one
|
||||
definition.
|
||||
|
||||
## ORIGINAL vs REFACTORED
|
||||
- ORIGINAL = `git show HEAD:tools/check_lint.py` (the committed pre-refactor
|
||||
source). Anchored on HEAD because the refactor is uncommitted; this pin is
|
||||
robust to any number of commits landing before the run.
|
||||
- REFACTORED = working tree.
|
||||
|
||||
## Verifier output (8 checks, all pass, exit 0)
|
||||
```
|
||||
[ok ] [1] selftest: original (HEAD) 37/37 rc 0
|
||||
[ok ] [2] selftest: refactored (working tree) 37/37 rc 0
|
||||
[ok ] [3] selftest: stdout BYTE-IDENTICAL (original == refactored)
|
||||
[ok ] [4] code-level: refactored HAS _write_json_file; original NONE
|
||||
[ok ] [5] code-level: json.dump( count 2 in original, 1 in refactored
|
||||
[ok ] [6] non-vacuity: live run produced a real 37-cell census
|
||||
[ok ] [7] --check --json: stdout + JSON file BYTE-IDENTICAL (rc match)
|
||||
[ok ] [8] --check --json unwritable path: both return rc 2
|
||||
|
||||
verify: 8 checks, 8 passed, 0 failed
|
||||
```
|
||||
|
||||
## Legs
|
||||
- [1-2] **selftest parity.** Both original (HEAD) and refactored (working tree)
|
||||
pass 37/37 cells with rc 0. The selftest is the comprehensive equivalence
|
||||
surface: it drives `--check` with a `CATCH_ALL`-class fault, exercises the
|
||||
`--json` write path, the `--fail-on-fault` rc-1 arm, portability cells, and
|
||||
the full fixture census.
|
||||
- [3] **byte-identical stdout.** Every character of both selftests matches --
|
||||
proving no cell's verdict, detail line, or formatted output changed.
|
||||
- [4-5] **non-faculty code-level checks.** The helper
|
||||
(`def _write_json_file(`) exists only in the refactored source; the two
|
||||
inlined `json.dump(...)` call sites exist only in the original. Together
|
||||
[4]+[5] prove the smell genuinely existed (count 2) and was genuinely
|
||||
collapsed (count 1) -- not a no-op refactor.
|
||||
- [6] **non-vacuity.** The live `--selftest --verbose` run produced a real
|
||||
37-cell census (`cell 37:` present, `cell ` count >= 37), not a stub.
|
||||
- [7] **`--check --json` equivalence.** On a real faulting check
|
||||
(`CHECK: python3 x.py || true`, `--fail-on-fault`), both versions produce
|
||||
BYTE-IDENTICAL stdout AND a BYTE-IDENTICAL JSON file on disk, with a
|
||||
matching rc (the `--fail-on-fault` -> rc 1 arm).
|
||||
- [8] **error-path parity.** On an unwritable `--json` path
|
||||
(`/nonexistent/does/not/exist.json`), both versions return rc 2 (the
|
||||
`OSError -> return 2` branch is preserved through the extraction).
|
||||
|
||||
## Non-tautology proof
|
||||
Against the pinned original (HEAD), the refactor markers are absent:
|
||||
`def _write_json_file` is NOT in HEAD, and there are TWO `json.dump(` call
|
||||
sites in HEAD but ONE in the refactored source. The helper's body is a literal
|
||||
lift of the original try/except (same `indent=2`, same `sort_keys=False`,
|
||||
same `"\n"`, same message), so it behaves identically whether inlined or
|
||||
extracted.
|
||||
|
||||
## Artifacts
|
||||
- Refactored tool: `tools/check_lint.py` (working tree)
|
||||
- Equivalence verifier: `wizard-work/forge/verify_check_lint_dry_refactor.py`
|
||||
- Post-check CHECK: `python3 /root/troupe-standup/wizard-work/forge/verify_check_lint_dry_refactor.py`
|
||||
- Report: this file
|
||||
@@ -0,0 +1,216 @@
|
||||
#!/usr/bin/env python3
|
||||
"""verify_check_lint_dry_refactor.py
|
||||
|
||||
Equivalence proof for a behavior-preserving DRY refactor in tools/check_lint.py
|
||||
(forge lane: Refactor).
|
||||
|
||||
THE SMELL
|
||||
check_lint.py wrote JSON to disk in two places with the SAME try/except block,
|
||||
hand-duplicated verbatim:
|
||||
|
||||
1. report() --json path (writing the full run summary)
|
||||
2. main() --check --json branch (writing the single-classification dict)
|
||||
|
||||
Both blocks were identical in structure:
|
||||
try:
|
||||
with open(<path>, "w", encoding="utf-8") as fh:
|
||||
json.dump(<obj>, fh, indent=2[, sort_keys=False])
|
||||
fh.write("\n")
|
||||
except OSError as exc:
|
||||
print("cannot write --json " + <path> + ": " + str(exc), file=sys.stderr)
|
||||
return 2
|
||||
|
||||
Two copies of one 7-line statement is the canonical DRY smell; each new JSON
|
||||
output site would be one more freehand copy away from drifting.
|
||||
|
||||
THE REFACTOR (committed? NO -- working tree only)
|
||||
A single module-level helper replaces both inlined blocks:
|
||||
|
||||
def _write_json_file(path: str, obj) -> int:
|
||||
'Write obj as indented JSON to path. Returns 0 on success, 2 on OSError.'
|
||||
try:
|
||||
with open(path, "w", encoding="utf-8") as fh:
|
||||
json.dump(obj, fh, indent=2, sort_keys=False)
|
||||
fh.write("\\n")
|
||||
except OSError as exc:
|
||||
print("cannot write --json " + path + ": " + str(exc), file=sys.stderr)
|
||||
return 2
|
||||
return 0
|
||||
|
||||
Both call sites are replaced by:
|
||||
rc = _write_json_file(<path>, <obj>)
|
||||
if rc:
|
||||
return rc
|
||||
|
||||
Same open mode, same indent=2, same sort_keys=False (the default --check branch
|
||||
relied on), same "\\n" terminator, same error message, same rc 2.
|
||||
|
||||
WHAT THIS VERIFIER PROVES
|
||||
The refactor is behavior-preserving, on faithful copies of each version run in
|
||||
an isolated temp file (self-contained selftest; no sibling tools needed).
|
||||
|
||||
LEGS
|
||||
[1] selftest: original (HEAD) 37/37 rc 0
|
||||
[2] selftest: refactored (working tree) 37/37 rc 0
|
||||
[3] selftest: stdout is BYTE-IDENTICAL (original == refactored)
|
||||
[4] code-level: refactored HAS the helper _write_json_file; original has none
|
||||
[5] code-level: json.dump( count is 2 in original, 1 in refactored
|
||||
(proves the smell existed and was collapsed into the helper)
|
||||
[6] non-vacuity: the live run produced a real 37-cell census (not a stub)
|
||||
[7] --check --json: stdout BYTE-IDENTICAL + JSON file content BYTE-IDENTICAL
|
||||
on both versions ( CATCH_ALL case with --fail-on-fault -> rc 1, JSON written)
|
||||
[8] --check --json unwritable path: both return rc 2 (OSError path preserved)
|
||||
|
||||
Exit 0 iff all checks pass, else 1.
|
||||
|
||||
NOTE ON PROVENANCE
|
||||
The refactor is an uncommitted working-tree change, so the genuine
|
||||
pre-refactor original is `git show HEAD:tools/check_lint.py` (HEAD is the
|
||||
committed original since the refactor has not been committed). That pin is
|
||||
robust to any number of commits landing before the run.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
REPO = "/root/troupe-standup"
|
||||
TOOL = "tools/check_lint.py"
|
||||
SUMMARY = "selftest: 37 cells, 37 passed, 0 failed"
|
||||
TMPDIR = os.environ.get("TMPDIR", tempfile.gettempdir())
|
||||
|
||||
|
||||
class Mismatch(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def orig_text():
|
||||
"""The committed pre-refactor source (HEAD of tools/check_lint.py)."""
|
||||
proc = subprocess.run(["git", "-C", REPO, "show", "HEAD:" + TOOL],
|
||||
capture_output=True, text=True, timeout=60)
|
||||
if proc.returncode != 0:
|
||||
raise Mismatch("cannot read HEAD:" + TOOL + ": " + proc.stderr)
|
||||
return proc.stdout
|
||||
|
||||
|
||||
def ref_text():
|
||||
"""The working-tree (refactored) source."""
|
||||
with open(os.path.join(REPO, TOOL), encoding="utf-8") as fh:
|
||||
return fh.read()
|
||||
|
||||
|
||||
def run_script(source_text, args):
|
||||
"""Write source_text to an isolated temp file and run it with args.
|
||||
Returns (rc, stdout, stderr)."""
|
||||
fd, path = tempfile.mkstemp(suffix=".py", dir=TMPDIR)
|
||||
try:
|
||||
with os.fdopen(fd, "w") as fh:
|
||||
fh.write(source_text)
|
||||
proc = subprocess.run([sys.executable, path] + args,
|
||||
capture_output=True, text=True, timeout=120)
|
||||
return proc.returncode, proc.stdout, proc.stderr
|
||||
finally:
|
||||
os.unlink(path)
|
||||
|
||||
|
||||
def count_json_dump(text):
|
||||
"""Count direct json.dump( calls in source text (file-writing, not json.dumps)."""
|
||||
return text.count("json.dump(")
|
||||
|
||||
|
||||
def has_helper(text):
|
||||
return "def _write_json_file(" in text
|
||||
|
||||
|
||||
def main():
|
||||
cells, failed = [], []
|
||||
|
||||
def check(label, ok, detail=""):
|
||||
cells.append(label)
|
||||
if not ok:
|
||||
failed.append("%s%s" % (label, (": " + detail) if detail else ""))
|
||||
print(" [%s] %s%s"
|
||||
% ("ok " if ok else "FAIL", label,
|
||||
(" [%s]" % detail) if detail and not ok else ""))
|
||||
|
||||
orig = orig_text()
|
||||
ref = ref_text()
|
||||
|
||||
# ---- [1-3] selftest equivalence ---------------------------------------
|
||||
rc_o, out_o, err_o = run_script(orig, ["--selftest"])
|
||||
rc_r, out_r, err_r = run_script(ref, ["--selftest"])
|
||||
|
||||
check("[1] selftest: original (HEAD) 37/37 rc 0",
|
||||
rc_o == 0 and SUMMARY in out_o,
|
||||
"rc=%d %s" % (rc_o, (out_o + err_o)[-240:]))
|
||||
check("[2] selftest: refactored (working tree) 37/37 rc 0",
|
||||
rc_r == 0 and SUMMARY in out_r,
|
||||
"rc=%d %s" % (rc_r, (out_r + err_r)[-240:]))
|
||||
check("[3] selftest: stdout BYTE-IDENTICAL (original == refactored)",
|
||||
out_o == out_r,
|
||||
"len_o=%d len_r=%d" % (len(out_o), len(out_r)))
|
||||
|
||||
# ---- [4-5] code-level: smell existed and was removed --------------------
|
||||
check("[4] code-level: refactored HAS _write_json_file; original NONE",
|
||||
has_helper(ref) and not has_helper(orig),
|
||||
"ref=%s orig=%s" % (has_helper(ref), has_helper(orig)))
|
||||
check("[5] code-level: json.dump( count 2 in original, 1 in refactored",
|
||||
count_json_dump(orig) == 2 and count_json_dump(ref) == 1,
|
||||
"orig=%d ref=%d" % (count_json_dump(orig), count_json_dump(ref)))
|
||||
|
||||
# ---- [6] non-vacuity ---------------------------------------------------
|
||||
# Run --selftest --verbose: a real census prints each cell line, proving
|
||||
# the fixtures are actually iterated and classified (not a stub that only
|
||||
# prints the summary).
|
||||
rc_v, out_v, _ = run_script(ref, ["--selftest", "--verbose"])
|
||||
check("[6] non-vacuity: live run produced a real 37-cell census",
|
||||
rc_v == 0 and "cell 37:" in out_v and SUMMARY in out_v
|
||||
and out_v.count("cell ") >= 37,
|
||||
"rc=%d cells_seen=%d" % (rc_v, out_v.count("cell ")))
|
||||
|
||||
# ---- [7] --check --json equivalence (CATCH_ALL + faults + JSON file) --
|
||||
json_path_o = os.path.join(TMPDIR, "cl_orig.json")
|
||||
json_path_r = os.path.join(TMPDIR, "cl_ref.json")
|
||||
try:
|
||||
args = ["--check", "CHECK: python3 x.py || true", "--json", None,
|
||||
"--fail-on-fault"]
|
||||
rc_o2, out_o2, err_o2 = run_script(orig, [a if a is not None else json_path_o
|
||||
for a in args])
|
||||
rc_r2, out_r2, err_r2 = run_script(ref, [a if a is not None else json_path_r
|
||||
for a in args])
|
||||
with open(json_path_o) as fh:
|
||||
json_o = fh.read()
|
||||
with open(json_path_r) as fh:
|
||||
json_r = fh.read()
|
||||
check("[7] --check --json: stdout + JSON file BYTE-IDENTICAL (rc match)",
|
||||
rc_o2 == rc_r2 and out_o2 == out_r2 and json_o == json_r,
|
||||
"rc_o=%d rc_r=%d stdout_match=%s json_match=%s" %
|
||||
(rc_o2, rc_r2, out_o2 == out_r2, json_o == json_r))
|
||||
finally:
|
||||
for p in (json_path_o, json_path_r):
|
||||
if os.path.exists(p):
|
||||
os.unlink(p)
|
||||
|
||||
# ---- [8] --check --json unwritable path -> rc 2 both -------------------
|
||||
rc_o3, _, _ = run_script(orig, ["--check", "CHECK: true", "--json",
|
||||
"/nonexistent/does/not/exist.json"])
|
||||
rc_r3, _, _ = run_script(ref, ["--check", "CHECK: true", "--json",
|
||||
"/nonexistent/does/not/exist.json"])
|
||||
check("[8] --check --json unwritable path: both return rc 2",
|
||||
rc_o3 == 2 and rc_r3 == 2,
|
||||
"rc_o=%d rc_r=%d" % (rc_o3, rc_r3))
|
||||
|
||||
print("\nverify: %d checks, %d passed, %d failed"
|
||||
% (len(cells), len(cells) - len(failed), len(failed)))
|
||||
for f in failed:
|
||||
print(" FAIL %s" % f)
|
||||
return 1 if failed else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
sys.exit(main())
|
||||
except Mismatch as e:
|
||||
print("\nVERDICT: MISMATCH -- %s" % e)
|
||||
sys.exit(1)
|
||||
Reference in new issue
Block a user