forgedock 1.27.0 → 1.28.0
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.
- package/README.md +4 -1
- package/bin/bootstrap.mjs +55 -0
- package/bin/engine/outcome-codes.mjs +10 -0
- package/bin/engine/telemetry-seam.mjs +37 -8
- package/bin/engine/telemetry-seam.test.mjs +31 -0
- package/bin/engine-cli.mjs +542 -38
- package/bin/engine.mjs +8 -4
- package/bin/forgedock.mjs +75 -9
- package/bin/gc.mjs +580 -0
- package/bin/lean/admission.mjs +33 -3
- package/bin/lean/batch.mjs +22 -4
- package/bin/lean/batch.test.mjs +5 -3
- package/bin/lean/checks.mjs +199 -0
- package/bin/lean/checks.test.mjs +183 -0
- package/bin/lean/conformance.mjs +190 -58
- package/bin/lean/conformance.test.mjs +116 -7
- package/bin/lean/credentials.mjs +29 -0
- package/bin/lean/depcache.mjs +312 -0
- package/bin/lean/depcache.test.mjs +298 -0
- package/bin/lean/drift.mjs +198 -0
- package/bin/lean/drift.test.mjs +223 -0
- package/bin/lean/effects.mjs +85 -8
- package/bin/lean/execution.e2e.test.mjs +81 -0
- package/bin/lean/execution.mjs +431 -0
- package/bin/lean/execution.test.mjs +583 -0
- package/bin/lean/heavy.mjs +289 -0
- package/bin/lean/heavy.test.mjs +207 -0
- package/bin/lean/lean.mjs +572 -139
- package/bin/lean/lean.test.mjs +1193 -31
- package/bin/lean/ledger.mjs +27 -2
- package/bin/lean/ledger.test.mjs +36 -1
- package/bin/lean/limits.mjs +58 -0
- package/bin/lean/limits.test.mjs +150 -0
- package/bin/lean/orchestrate.mjs +312 -19
- package/bin/lean/prompts.mjs +89 -15
- package/bin/lean/refactor.mjs +276 -0
- package/bin/lean/refactor.test.mjs +359 -0
- package/bin/lean/render.mjs +24 -5
- package/bin/lean/review-trigger.mjs +134 -0
- package/bin/lean/review-trigger.test.mjs +104 -0
- package/bin/lean/sandbox-provider.contract.test.mjs +257 -0
- package/bin/lean/sandbox-remote.mjs +142 -0
- package/bin/lean/sched.mjs +221 -0
- package/bin/lean/sched.test.mjs +310 -0
- package/bin/lean/schedd.mjs +247 -0
- package/bin/lean/schedd.test.mjs +180 -0
- package/bin/lean/toolchain.mjs +228 -0
- package/bin/lean/toolchain.test.mjs +154 -0
- package/bin/lean/verify.mjs +20 -9
- package/bin/lean/verify.test.mjs +26 -3
- package/bin/lean/worktree.mjs +69 -0
- package/bin/lean/worktree.test.mjs +141 -0
- package/bin/runner.mjs +53 -10
- package/bin/stats/github-source.mjs +1 -1
- package/bin/stats/index.mjs +14 -2
- package/bin/stats/query.mjs +72 -0
- package/bin/stats/stats.test.mjs +54 -0
- package/commands/audit.md +2 -0
- package/commands/autopilot.md +2 -0
- package/commands/bootstrap.md +35 -0
- package/commands/cleanup.md +7 -22
- package/commands/forge-stats.md +2 -1
- package/commands/forgedock-init.md +7 -0
- package/commands/issue.md +89 -830
- package/commands/milestone.md +55 -6
- package/commands/orchestrate.md +10 -2
- package/commands/upgrade-deps.md +3 -3
- package/commands/work-on/build/validate.md +4 -0
- package/commands/work-on/close.md +2 -0
- package/commands/work-on/decompose.md +24 -6
- package/docs/CI.md +3 -3
- package/docs/CONFIG.md +177 -2
- package/docs/OUTCOME-CODES.md +8 -0
- package/docs/TELEMETRY.md +1 -1
- package/docs/site/command-reference.md +22 -0
- package/docs/site/getting-started.md +2 -0
- package/docs/site/start-a-new-project.md +327 -0
- package/package.json +1 -1
- package/scripts/affected-files-check.sh +91 -0
- package/scripts/affected-files-check.test.sh +80 -0
- package/scripts/bootstrap-create.mjs +393 -0
- package/scripts/bootstrap-validate.mjs +36 -2
- package/scripts/calibration.mjs +116 -0
- package/scripts/check-devdocs-freshness.sh +9 -4
- package/scripts/check-devdocs-freshness.test.sh +11 -1
- package/scripts/check-spec-markers.sh +3 -0
- package/scripts/check-spec-size.sh +29 -11
- package/scripts/check-spec-size.test.sh +27 -4
- package/scripts/ci-local/lib.mjs +8 -1
- package/scripts/ci-local/parallelism.test.mjs +27 -0
- package/scripts/ci-local/run.mjs +3 -3
- package/scripts/ci-local/scoping.mjs +82 -0
- package/scripts/ci-local.test.sh +1 -1
- package/scripts/fixtures/symbol-index/other/sample.rb +3 -0
- package/scripts/fixtures/symbol-index/py/pkg/__init__.py +1 -0
- package/scripts/fixtures/symbol-index/py/pkg/a.py +5 -0
- package/scripts/fixtures/symbol-index/py/pkg/b.py +14 -0
- package/scripts/fixtures/symbol-index/py/pkg/c.py +1 -0
- package/scripts/fixtures/symbol-index/py/sample.py +41 -0
- package/scripts/fixtures/symbol-index/ts/sample.ts +13 -0
- package/scripts/fixtures/symbol-move/gen_big.py +201 -0
- package/scripts/fixtures/symbol-move-ts/gen_big.mjs +56 -0
- package/scripts/forge-root.test.sh +1 -1
- package/scripts/hotspots.sh +100 -0
- package/scripts/hotspots.test.sh +101 -0
- package/scripts/issue-create.sh +197 -0
- package/scripts/issue-create.test.sh +166 -0
- package/scripts/maintenance-scan.sh +185 -0
- package/scripts/maintenance-scan.test.sh +89 -0
- package/scripts/milestone-drift.sh +206 -0
- package/scripts/milestone-drift.test.sh +120 -0
- package/scripts/refactor-verify.sh +368 -0
- package/scripts/refactor-verify.test.sh +202 -0
- package/scripts/structure-delta.sh +10 -5
- package/scripts/structure-delta.test.sh +3 -2
- package/scripts/symbol-index.mjs +253 -0
- package/scripts/symbol-index.py +354 -0
- package/scripts/symbol-index.test.sh +119 -0
- package/scripts/symbol-move-ts.test.sh +161 -0
- package/scripts/symbol-move.mjs +379 -0
- package/scripts/symbol-move.py +383 -0
- package/scripts/symbol-move.test.sh +168 -0
- package/templates/bootstrap/interview.md +34 -0
- package/templates/bootstrap/issue-body.md +42 -0
- package/templates/bootstrap/needs-human-issue.md +45 -0
- package/templates/bootstrap/stack-profiles.md +34 -0
- package/templates/bootstrap/technical-spike.md +75 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
3
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
#
|
|
5
|
+
# refactor-verify.sh: deterministic baseline-vs-after proof that a refactor:split only moved code (forge#4209, #4196).
|
|
6
|
+
# No LLM, no network. Never modifies the target repo, its index or its worktree.
|
|
7
|
+
#
|
|
8
|
+
# Usage:
|
|
9
|
+
# refactor-verify.sh baseline --root <dir> --target <file|dir> --test-cmd '<cmd>' --out <file.json>
|
|
10
|
+
# refactor-verify.sh verify --baseline <file.json> --base <ref> [--head <ref>] (head defaults to HEAD)
|
|
11
|
+
#
|
|
12
|
+
# baseline: runs <cmd> via `bash -c` in --root and parses passed/skipped counts from its summary (pytest, unittest,
|
|
13
|
+
# node TAP/spec, jest/vitest; several summaries are summed). Indexes --target with scripts/symbol-index.py (.py) or
|
|
14
|
+
# scripts/symbol-index.mjs and records every public symbol as {module,name}: module = dotted path under --root for
|
|
15
|
+
# Python (pkg/__init__.py -> pkg), the root-relative file path for JS/TS; name may be Class.method. Indexes the
|
|
16
|
+
# target's package directory (the target if a dir, else its parent) and records its import cycles. Writes one
|
|
17
|
+
# key-sorted JSON {test:{exit,passed,skipped},symbols,cycles,root,target,index_dir,lang,test_cmd} to --out.
|
|
18
|
+
# verify: run with --root's worktree checked out at --head. Re-runs test_cmd and the cycle index, resolves every
|
|
19
|
+
# baseline symbol from its OLD module path at runtime (Python importlib + getattr chain, so re-export shims and a
|
|
20
|
+
# module turned into a package resolve; JS/TS node import(); JS/TS interfaces and type aliases, erased at runtime,
|
|
21
|
+
# statically: the old module still exports the name, following `export * from`), and reads
|
|
22
|
+
# `git diff --color-moved=plain --no-renames <base> <head>`. Prints key-sorted JSON {ok,failures:[...]}, kinds:
|
|
23
|
+
# logic-diff {file,line,text} an added/removed line that is not moved, not blank and not an import line
|
|
24
|
+
# (Python import/from-import incl. parenthesised continuations; JS/TS import,
|
|
25
|
+
# export ... from, export * from, incl. braced continuations)
|
|
26
|
+
# missing-symbol {module,name} a baseline public symbol no longer resolves from its old path
|
|
27
|
+
# count-changed {before,after} passed or skipped changed, or the test exit went from 0 to non-zero
|
|
28
|
+
# new-cycle {cycle} an import cycle not present in the baseline (set difference)
|
|
29
|
+
# Exit: 0 ok | 1 verify found failures | 2 usage, missing file, unknown ref, unparseable counts or unsupported language.
|
|
30
|
+
# Requires git, jq, python3; node only for JS/TS targets. Bash 3.2 portable.
|
|
31
|
+
set -uo pipefail
|
|
32
|
+
|
|
33
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
34
|
+
SI_PY="$HERE/symbol-index.py"; SI_MJS="$HERE/symbol-index.mjs"
|
|
35
|
+
|
|
36
|
+
usage() {
|
|
37
|
+
echo "usage: refactor-verify.sh baseline --root <dir> --target <file|dir> --test-cmd '<cmd>' --out <file.json>" >&2
|
|
38
|
+
echo " refactor-verify.sh verify --baseline <file.json> --base <ref> [--head <ref>]" >&2
|
|
39
|
+
exit 2
|
|
40
|
+
}
|
|
41
|
+
die() { echo "refactor-verify.sh: $*" >&2; exit 2; }
|
|
42
|
+
|
|
43
|
+
for tool in git jq python3; do command -v "$tool" >/dev/null 2>&1 || die "missing required tool: $tool"; done
|
|
44
|
+
|
|
45
|
+
SCRATCH="$(mktemp -d "${TMPDIR:-/tmp}/refactor-verify.XXXXXX")" || die "cannot create scratch dir"
|
|
46
|
+
trap 'rm -rf "$SCRATCH"' EXIT
|
|
47
|
+
|
|
48
|
+
# ---- passed/skipped counts from a test-run output file: prints "<passed> <skipped>" or exits 1 ----
|
|
49
|
+
parse_counts() {
|
|
50
|
+
python3 -I - "$1" <<'PY'
|
|
51
|
+
import re, sys
|
|
52
|
+
text = open(sys.argv[1], encoding="utf-8", errors="replace").read()
|
|
53
|
+
text = re.sub(r"\x1b\[[0-9;]*m", "", text)
|
|
54
|
+
lines = text.splitlines()
|
|
55
|
+
|
|
56
|
+
def num(pat, s):
|
|
57
|
+
m = re.search(pat, s)
|
|
58
|
+
return int(m.group(1)) if m else 0
|
|
59
|
+
|
|
60
|
+
def result(p, s):
|
|
61
|
+
print(p, s)
|
|
62
|
+
sys.exit(0)
|
|
63
|
+
|
|
64
|
+
# jest ("Tests: 1 skipped, 3 passed, 4 total") / vitest (" Tests 3 passed | 1 skipped (4)")
|
|
65
|
+
found, p, s = False, 0, 0
|
|
66
|
+
for ln in lines:
|
|
67
|
+
if re.match(r"^\s*Tests:?\s+.*\d+ (passed|skipped|failed|todo)", ln):
|
|
68
|
+
found = True
|
|
69
|
+
p += num(r"(\d+) passed", ln); s += num(r"(\d+) skipped", ln) + num(r"(\d+) todo", ln)
|
|
70
|
+
if found: result(p, s)
|
|
71
|
+
|
|
72
|
+
# node --test TAP ("# pass 3", "# skipped 1" / "# skip 1") and spec reporter ("ℹ pass 3", "ℹ skipped 1")
|
|
73
|
+
found, p, s = False, 0, 0
|
|
74
|
+
for ln in lines:
|
|
75
|
+
m = re.match(r"^\s*(?:#|ℹ) pass (\d+)\s*$", ln)
|
|
76
|
+
if m: found = True; p += int(m.group(1)); continue
|
|
77
|
+
m = re.match(r"^\s*(?:#|ℹ) skip(?:ped)? (\d+)\s*$", ln)
|
|
78
|
+
if m: found = True; s += int(m.group(1))
|
|
79
|
+
if found: result(p, s)
|
|
80
|
+
|
|
81
|
+
# unittest: "Ran N tests" followed by "OK (skipped=M)" / "FAILED (failures=1, errors=2, skipped=3)"
|
|
82
|
+
found, p, s, ran = False, 0, 0, None
|
|
83
|
+
for ln in lines:
|
|
84
|
+
m = re.match(r"^Ran (\d+) tests? in ", ln)
|
|
85
|
+
if m: ran = int(m.group(1)); continue
|
|
86
|
+
m = re.match(r"^(OK|FAILED)\b(?:\s*\((.*)\))?\s*$", ln)
|
|
87
|
+
if m and ran is not None:
|
|
88
|
+
det = m.group(2) or ""
|
|
89
|
+
sk = num(r"skipped=(\d+)", det)
|
|
90
|
+
bad = num(r"failures=(\d+)", det) + num(r"errors=(\d+)", det)
|
|
91
|
+
found = True; p += ran - sk - bad; s += sk; ran = None
|
|
92
|
+
if found: result(p, s)
|
|
93
|
+
|
|
94
|
+
# pytest summary ("==== 3 passed, 1 skipped in 0.12s ====" or -q "3 passed, 1 skipped in 0.12s")
|
|
95
|
+
found, p, s = False, 0, 0
|
|
96
|
+
item = r"\d+ (?:passed|failed|skipped|errors?|deselected|xfailed|xpassed|warnings?)"
|
|
97
|
+
for ln in lines:
|
|
98
|
+
if re.search(r"(?:^|[ =])" + item + r"(?:, " + item + r")* in [\d.]+s", ln):
|
|
99
|
+
found = True
|
|
100
|
+
p += num(r"(\d+) passed", ln); s += num(r"(\d+) skipped", ln)
|
|
101
|
+
if found: result(p, s)
|
|
102
|
+
sys.exit(1)
|
|
103
|
+
PY
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
# run_tests <root> <cmd>: sets T_EXIT T_PASSED T_SKIPPED, or dies when no summary can be parsed.
|
|
107
|
+
run_tests() {
|
|
108
|
+
local out="$SCRATCH/test-output.txt" counts
|
|
109
|
+
(cd "$1" && bash -c "$2") >"$out" 2>&1
|
|
110
|
+
T_EXIT=$?
|
|
111
|
+
counts="$(parse_counts "$out")" || { tail -n 20 "$out" >&2; die "could not parse pass/skip counts from: $2"; }
|
|
112
|
+
T_PASSED="${counts% *}"; T_SKIPPED="${counts#* }"
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
# index <path> <lang>: symbol-index JSON on stdout; dies on unsupported output.
|
|
116
|
+
index() {
|
|
117
|
+
local json
|
|
118
|
+
if [ "$2" = "python" ]; then json="$(python3 -I "$SI_PY" "$1")"; else json="$(node "$SI_MJS" "$1")"; fi
|
|
119
|
+
[ -n "$json" ] || die "symbol index failed for $1"
|
|
120
|
+
if printf '%s' "$json" | jq -e 'has("unsupported")' >/dev/null; then
|
|
121
|
+
die "unsupported: $(printf '%s' "$json" | jq -c .)"
|
|
122
|
+
fi
|
|
123
|
+
printf '%s\n' "$json"
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
lang_of() {
|
|
127
|
+
local t="$1"
|
|
128
|
+
if [ -d "$t" ]; then
|
|
129
|
+
if [ -n "$(find "$t" -name '*.py' -not -path '*/node_modules/*' | head -n 1)" ]; then echo python; else echo js; fi
|
|
130
|
+
else
|
|
131
|
+
case "$t" in *.py) echo python ;; *) echo js ;; esac
|
|
132
|
+
fi
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
cmd_baseline() {
|
|
136
|
+
local root="" target="" test_cmd="" out=""
|
|
137
|
+
while [ $# -gt 0 ]; do
|
|
138
|
+
case "$1" in
|
|
139
|
+
--root) root="${2-}"; shift 2 || usage ;;
|
|
140
|
+
--target) target="${2-}"; shift 2 || usage ;;
|
|
141
|
+
--test-cmd) test_cmd="${2-}"; shift 2 || usage ;;
|
|
142
|
+
--out) out="${2-}"; shift 2 || usage ;;
|
|
143
|
+
*) usage ;;
|
|
144
|
+
esac
|
|
145
|
+
done
|
|
146
|
+
[ -n "$root" ] && [ -n "$target" ] && [ -n "$test_cmd" ] && [ -n "$out" ] || usage
|
|
147
|
+
[ -d "$root" ] || die "root not found: $root"
|
|
148
|
+
root="$(cd "$root" && pwd -P)"
|
|
149
|
+
case "$target" in /*) ;; *) target="$root/$target" ;; esac
|
|
150
|
+
[ -e "$target" ] || die "target not found: $target"
|
|
151
|
+
local lang index_dir sym_json cyc_json
|
|
152
|
+
lang="$(lang_of "$target")"
|
|
153
|
+
if [ -d "$target" ]; then index_dir="$(cd "$target" && pwd -P)"; else index_dir="$(cd "$(dirname "$target")" && pwd -P)"; target="$index_dir/$(basename "$target")"; fi
|
|
154
|
+
|
|
155
|
+
run_tests "$root" "$test_cmd"
|
|
156
|
+
sym_json="$(index "$target" "$lang")" || exit 2
|
|
157
|
+
cyc_json="$(index "$index_dir" "$lang")" || exit 2
|
|
158
|
+
|
|
159
|
+
printf '%s' "$sym_json" > "$SCRATCH/symbols.json"
|
|
160
|
+
local symbols
|
|
161
|
+
symbols="$(python3 -I - "$SCRATCH/symbols.json" "$root" "$target" "$lang" <<'PY'
|
|
162
|
+
import json, os, sys
|
|
163
|
+
data = json.load(open(sys.argv[1])); root, target, lang = sys.argv[2], sys.argv[3], sys.argv[4]
|
|
164
|
+
def module(path):
|
|
165
|
+
rel = os.path.relpath(os.path.realpath(path), root).replace(os.sep, "/")
|
|
166
|
+
if lang != "python":
|
|
167
|
+
return rel
|
|
168
|
+
rel = rel[:-3] if rel.endswith(".py") else rel
|
|
169
|
+
if rel.endswith("/__init__") or rel == "__init__":
|
|
170
|
+
rel = rel[: -len("__init__")].rstrip("/")
|
|
171
|
+
return rel.replace("/", ".")
|
|
172
|
+
# JS/TS interfaces and type aliases are erased at runtime: flagged "type" so verify resolves them statically (#4212).
|
|
173
|
+
erased = lambda s: lang != "python" and s.get("kind") in ("interface", "type")
|
|
174
|
+
out = sorted({(module(s.get("file", target)), s["name"], erased(s)) for s in data["symbols"] if s.get("public")})
|
|
175
|
+
print(json.dumps([dict({"module": m, "name": n}, **({"type": True} if t else {})) for m, n, t in out]))
|
|
176
|
+
PY
|
|
177
|
+
)" || die "cannot build symbol list"
|
|
178
|
+
|
|
179
|
+
jq -nS --argjson exit "$T_EXIT" --argjson passed "$T_PASSED" --argjson skipped "$T_SKIPPED" \
|
|
180
|
+
--argjson symbols "$symbols" --argjson cycles "$(printf '%s' "$cyc_json" | jq -c .cycles)" \
|
|
181
|
+
--arg root "$root" --arg target "$target" --arg index_dir "$index_dir" --arg lang "$lang" --arg test_cmd "$test_cmd" \
|
|
182
|
+
'{test:{exit:$exit,passed:$passed,skipped:$skipped},symbols:$symbols,cycles:$cycles,root:$root,target:$target,
|
|
183
|
+
index_dir:$index_dir,lang:$lang,test_cmd:$test_cmd}' > "$out" || die "cannot write $out"
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
# resolve_symbols <baseline.json>: prints a JSON array of unresolved {module,name}.
|
|
187
|
+
resolve_symbols() {
|
|
188
|
+
local root lang
|
|
189
|
+
root="$(jq -r .root "$1")"; lang="$(jq -r .lang "$1")"
|
|
190
|
+
jq -c .symbols "$1" > "$SCRATCH/want.json"
|
|
191
|
+
if [ "$lang" = "python" ]; then
|
|
192
|
+
(cd "$root" && python3 -I -B - "$SCRATCH/want.json" "$root" <<'PY'
|
|
193
|
+
import importlib, io, json, sys, contextlib
|
|
194
|
+
want = json.load(open(sys.argv[1])); sys.path.insert(0, sys.argv[2])
|
|
195
|
+
missing = []
|
|
196
|
+
for s in want:
|
|
197
|
+
try:
|
|
198
|
+
with contextlib.redirect_stdout(io.StringIO()):
|
|
199
|
+
obj = importlib.import_module(s["module"])
|
|
200
|
+
for part in s["name"].split("."):
|
|
201
|
+
obj = getattr(obj, part)
|
|
202
|
+
except Exception:
|
|
203
|
+
missing.append(s)
|
|
204
|
+
print(json.dumps(missing))
|
|
205
|
+
PY
|
|
206
|
+
)
|
|
207
|
+
else
|
|
208
|
+
command -v node >/dev/null 2>&1 || die "node is required to resolve JS/TS symbols"
|
|
209
|
+
(cd "$root" && node --input-type=module - "$SCRATCH/want.json" "$root" <<'JS'
|
|
210
|
+
import { readFileSync } from "node:fs";
|
|
211
|
+
import path from "node:path";
|
|
212
|
+
import { pathToFileURL } from "node:url";
|
|
213
|
+
import { existsSync, statSync } from "node:fs";
|
|
214
|
+
import { createRequire } from "node:module";
|
|
215
|
+
const [want, root] = [JSON.parse(readFileSync(process.argv[2], "utf8")), process.argv[3]];
|
|
216
|
+
// Type-only names (interface/type alias, erased at runtime) resolve statically: the old module still exports the
|
|
217
|
+
// name by declaration, `export { ... }` list or `export ... from`, following `export * from` relative modules.
|
|
218
|
+
let ts = null;
|
|
219
|
+
for (const m of ["typescript", "ts-morph"]) {
|
|
220
|
+
try { const r = createRequire(path.join(root, "noop.js"))(m); ts = m === "typescript" ? r : r.ts; if (typeof ts?.createSourceFile === "function") break; ts = null; } catch { /* next */ }
|
|
221
|
+
}
|
|
222
|
+
const exportsOf = (file, seen = new Set()) => {
|
|
223
|
+
const names = new Set();
|
|
224
|
+
if (!ts || seen.has(file) || !existsSync(file) || !statSync(file).isFile()) return names;
|
|
225
|
+
seen.add(file);
|
|
226
|
+
const src = ts.createSourceFile(file, readFileSync(file, "utf8"), ts.ScriptTarget.Latest, true);
|
|
227
|
+
const exported = (n) => (ts.getCombinedModifierFlags(n) & ts.ModifierFlags.Export) !== 0;
|
|
228
|
+
const resolve = (spec) => {
|
|
229
|
+
const base = path.resolve(path.dirname(file), spec), stem = base.replace(/\.(m|c)?jsx?$/, "");
|
|
230
|
+
for (const c of [base, ...[".ts", ".tsx", ".mts", ".cts", ".d.ts"].flatMap((x) => [stem + x, path.join(base, "index" + x)])]) if (existsSync(c) && statSync(c).isFile()) return c;
|
|
231
|
+
return null;
|
|
232
|
+
};
|
|
233
|
+
for (const st of src.statements) {
|
|
234
|
+
if (ts.isExportDeclaration(st)) {
|
|
235
|
+
if (st.exportClause && ts.isNamedExports(st.exportClause)) for (const el of st.exportClause.elements) names.add(el.name.text);
|
|
236
|
+
else if (!st.exportClause && st.moduleSpecifier?.text?.startsWith(".")) { const f = resolve(st.moduleSpecifier.text); if (f) for (const n of exportsOf(f, seen)) names.add(n); }
|
|
237
|
+
} else if (ts.isVariableStatement(st) && exported(st)) {
|
|
238
|
+
for (const d of st.declarationList.declarations) if (ts.isIdentifier(d.name)) names.add(d.name.text);
|
|
239
|
+
} else if (st.name && ts.isIdentifier(st.name) && exported(st)) names.add(st.name.text);
|
|
240
|
+
}
|
|
241
|
+
return names;
|
|
242
|
+
};
|
|
243
|
+
const missing = [];
|
|
244
|
+
for (const s of want) {
|
|
245
|
+
if (s.type) {
|
|
246
|
+
if (!exportsOf(path.join(root, s.module)).has(s.name)) missing.push(s);
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
try {
|
|
250
|
+
let obj = await import(pathToFileURL(path.join(root, s.module)).href);
|
|
251
|
+
for (const part of s.name.split(".")) {
|
|
252
|
+
const next = obj?.[part] ?? obj?.prototype?.[part];
|
|
253
|
+
if (next === undefined) throw new Error("unresolved");
|
|
254
|
+
obj = next;
|
|
255
|
+
}
|
|
256
|
+
} catch { missing.push(s); }
|
|
257
|
+
}
|
|
258
|
+
process.stdout.write(JSON.stringify(missing) + "\n");
|
|
259
|
+
JS
|
|
260
|
+
)
|
|
261
|
+
fi
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
# logic_diff <root> <base> <head>: prints a JSON array of {file,line,text} for non-moved, non-import changed lines.
|
|
265
|
+
logic_diff() {
|
|
266
|
+
local diff="$SCRATCH/diff.txt"
|
|
267
|
+
git -C "$1" -c color.diff.old=red -c color.diff.new=green -c color.diff.oldMoved=magenta -c color.diff.newMoved=cyan \
|
|
268
|
+
-c color.diff.oldMovedAlternative=magenta -c color.diff.newMovedAlternative=cyan \
|
|
269
|
+
-c color.diff.whitespace=normal -c core.whitespace=-blank-at-eol,-blank-at-eof,-space-before-tab \
|
|
270
|
+
diff --color=always --color-moved=plain --no-renames --no-ext-diff "$2" "$3" -- > "$diff" \
|
|
271
|
+
|| die "git diff failed for $2..$3"
|
|
272
|
+
python3 -I - "$diff" <<'PY'
|
|
273
|
+
import json, re, sys
|
|
274
|
+
ESC = re.compile(r"\x1b\[[0-9;]*m")
|
|
275
|
+
MOVED = ("\x1b[35m", "\x1b[36m")
|
|
276
|
+
PY_IMPORT = re.compile(r"^\s*(import\s+\S|from\s+\S+\s+import\b)")
|
|
277
|
+
JS_IMPORT = re.compile(r"^\s*(import\b|export\s+(type\s+)?(\*|\{).*\bfrom\b|export\s+\*)")
|
|
278
|
+
def opens_block(s):
|
|
279
|
+
if re.match(r"^\s*from\s+\S+\s+import\s*\(", s) and ")" not in s:
|
|
280
|
+
return ")"
|
|
281
|
+
if re.match(r"^\s*(import|export)\s+(type\s+)?\{", s) and "}" not in s:
|
|
282
|
+
return "}"
|
|
283
|
+
return None
|
|
284
|
+
fails, path, old, in_hunk = [], None, None, False
|
|
285
|
+
lineno = {"-": 0, "+": 0}
|
|
286
|
+
block = {"-": None, "+": None} # closing char of an open multi-line import, per side
|
|
287
|
+
for raw in open(sys.argv[1], encoding="utf-8", errors="replace").read().split("\n"):
|
|
288
|
+
plain = ESC.sub("", raw)
|
|
289
|
+
if plain.startswith("diff --git "):
|
|
290
|
+
in_hunk, path, old = False, None, None; block = {"-": None, "+": None}; continue
|
|
291
|
+
if not in_hunk and plain.startswith("--- "):
|
|
292
|
+
old = plain[4:]; old = old[2:] if old.startswith("a/") else old; continue
|
|
293
|
+
if not in_hunk and plain.startswith("+++ "):
|
|
294
|
+
new = plain[4:]; path = old if new == "/dev/null" else (new[2:] if new.startswith("b/") else new); continue
|
|
295
|
+
m = re.match(r"^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@", plain)
|
|
296
|
+
if m:
|
|
297
|
+
in_hunk = True; lineno = {"-": int(m.group(1)), "+": int(m.group(2))}; continue
|
|
298
|
+
if not in_hunk or not plain or plain.startswith("\\"):
|
|
299
|
+
continue
|
|
300
|
+
sign, body = plain[0], plain[1:]
|
|
301
|
+
sides = ("-", "+") if sign == " " else (sign,)
|
|
302
|
+
is_import = False
|
|
303
|
+
for side in sides:
|
|
304
|
+
if block[side]:
|
|
305
|
+
is_import = True
|
|
306
|
+
if block[side] in body:
|
|
307
|
+
block[side] = None
|
|
308
|
+
elif PY_IMPORT.match(body) or JS_IMPORT.match(body):
|
|
309
|
+
is_import = True
|
|
310
|
+
block[side] = opens_block(body)
|
|
311
|
+
if sign in "+-":
|
|
312
|
+
if not raw.startswith(MOVED) and body.strip() and not is_import:
|
|
313
|
+
fails.append({"kind": "logic-diff", "file": path, "line": lineno[sign], "text": body.strip()[:200]})
|
|
314
|
+
for side in sides:
|
|
315
|
+
lineno[side] += 1
|
|
316
|
+
print(json.dumps(fails))
|
|
317
|
+
PY
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
cmd_verify() {
|
|
321
|
+
local baseline="" base="" head="HEAD"
|
|
322
|
+
while [ $# -gt 0 ]; do
|
|
323
|
+
case "$1" in
|
|
324
|
+
--baseline) baseline="${2-}"; shift 2 || usage ;;
|
|
325
|
+
--base) base="${2-}"; shift 2 || usage ;;
|
|
326
|
+
--head) head="${2-}"; shift 2 || usage ;;
|
|
327
|
+
*) usage ;;
|
|
328
|
+
esac
|
|
329
|
+
done
|
|
330
|
+
[ -n "$baseline" ] && [ -n "$base" ] || usage
|
|
331
|
+
[ -f "$baseline" ] || die "baseline file not found: $baseline"
|
|
332
|
+
jq -e '.test and .symbols and .cycles and .root and .index_dir and .lang and .test_cmd' "$baseline" >/dev/null 2>&1 \
|
|
333
|
+
|| die "not a refactor-verify baseline: $baseline"
|
|
334
|
+
local root index_dir lang
|
|
335
|
+
root="$(jq -r .root "$baseline")"; index_dir="$(jq -r .index_dir "$baseline")"; lang="$(jq -r .lang "$baseline")"
|
|
336
|
+
[ -d "$root" ] || die "baseline root not found: $root"
|
|
337
|
+
git -C "$root" rev-parse --verify --quiet "$base^{commit}" >/dev/null || die "unknown ref: $base"
|
|
338
|
+
git -C "$root" rev-parse --verify --quiet "$head^{commit}" >/dev/null || die "unknown ref: $head"
|
|
339
|
+
|
|
340
|
+
local diff_fails missing cycles_after
|
|
341
|
+
diff_fails="$(logic_diff "$root" "$base" "$head")" || exit 2
|
|
342
|
+
run_tests "$root" "$(jq -r .test_cmd "$baseline")"
|
|
343
|
+
missing="$(resolve_symbols "$baseline")" || die "symbol resolution failed"
|
|
344
|
+
if [ -d "$index_dir" ]; then
|
|
345
|
+
cycles_after="$(index "$index_dir" "$lang" | jq -c .cycles)" || exit 2
|
|
346
|
+
else
|
|
347
|
+
cycles_after='[]'
|
|
348
|
+
fi
|
|
349
|
+
|
|
350
|
+
jq -nS --slurpfile b "$baseline" --argjson diff "$diff_fails" --argjson missing "$missing" \
|
|
351
|
+
--argjson cycles "$cycles_after" --argjson exit "$T_EXIT" --argjson passed "$T_PASSED" --argjson skipped "$T_SKIPPED" '
|
|
352
|
+
($b[0].test) as $before | {exit:$exit,passed:$passed,skipped:$skipped} as $after
|
|
353
|
+
| ( $diff
|
|
354
|
+
+ [$missing[] | {kind:"missing-symbol",module,name}]
|
|
355
|
+
+ (if $before.passed != $after.passed or $before.skipped != $after.skipped or ($before.exit == 0 and $after.exit != 0)
|
|
356
|
+
then [{kind:"count-changed",before:$before,after:$after}] else [] end)
|
|
357
|
+
+ [($cycles - $b[0].cycles)[] | {kind:"new-cycle",cycle:.}] ) as $f
|
|
358
|
+
| {ok:($f|length == 0),failures:$f}' > "$SCRATCH/result.json" || die "cannot build result"
|
|
359
|
+
cat "$SCRATCH/result.json"
|
|
360
|
+
jq -e .ok "$SCRATCH/result.json" >/dev/null && return 0
|
|
361
|
+
return 1
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
case "${1-}" in
|
|
365
|
+
baseline) shift; cmd_baseline "$@" ;;
|
|
366
|
+
verify) shift; cmd_verify "$@" ;;
|
|
367
|
+
*) usage ;;
|
|
368
|
+
esac
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# refactor-verify.test.sh — cases for scripts/refactor-verify.sh (forge#4209).
|
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
# Builds a throwaway git repo (Python package + unittest suite) and checks baseline/verify on a pure move plus shim
|
|
6
|
+
# and on each failure kind.
|
|
7
|
+
set -uo pipefail
|
|
8
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
9
|
+
RV="$HERE/refactor-verify.sh"
|
|
10
|
+
PASS=0; FAILN=0
|
|
11
|
+
TMP="$(mktemp -d "${TMPDIR:-/tmp}/refactor-verify-test.XXXXXX")"; trap 'rm -rf "$TMP"' EXIT
|
|
12
|
+
ok() { PASS=$((PASS+1)); }
|
|
13
|
+
bad() { FAILN=$((FAILN+1)); echo "FAIL: $1"; }
|
|
14
|
+
eq() { if [ "$2" = "$3" ]; then ok; else bad "$1 (got: $2 want: $3)"; fi }
|
|
15
|
+
q() { printf '%s' "$OUT" | jq -c "$1"; }
|
|
16
|
+
run() { OUT="$("$@" 2>"$TMP/err")"; RC=$?; }
|
|
17
|
+
export PYTHONDONTWRITEBYTECODE=1
|
|
18
|
+
|
|
19
|
+
R="$TMP/repo"; B="$TMP/baseline.json"
|
|
20
|
+
TEST_CMD='python3 -m unittest discover -s tests -t .'
|
|
21
|
+
g() { git -C "$R" -c user.name=t -c user.email=t@example.invalid "$@"; }
|
|
22
|
+
commit() { g add -A && g commit -qm "$1"; }
|
|
23
|
+
|
|
24
|
+
# ---- fixture: pkg/big.py + 4 unittest tests (1 skipped) ----
|
|
25
|
+
mkdir -p "$R/pkg" "$R/tests"
|
|
26
|
+
git init -q "$R"
|
|
27
|
+
printf '__pycache__/\n' > "$R/.gitignore"
|
|
28
|
+
: > "$R/pkg/__init__.py"
|
|
29
|
+
: > "$R/tests/__init__.py"
|
|
30
|
+
cat > "$R/pkg/big.py" <<'EOF'
|
|
31
|
+
import os
|
|
32
|
+
|
|
33
|
+
LIMIT = 3
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _helper(x):
|
|
37
|
+
return x * 2
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def double(x):
|
|
41
|
+
return _helper(x)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def env_name():
|
|
45
|
+
return os.name
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Counter:
|
|
49
|
+
def __init__(self):
|
|
50
|
+
self.n = 0
|
|
51
|
+
|
|
52
|
+
def bump(self):
|
|
53
|
+
self.n += 1
|
|
54
|
+
return self.n
|
|
55
|
+
EOF
|
|
56
|
+
cat > "$R/tests/test_big.py" <<'EOF'
|
|
57
|
+
import unittest
|
|
58
|
+
|
|
59
|
+
from pkg.big import LIMIT, Counter, double
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class BigTest(unittest.TestCase):
|
|
63
|
+
def test_double(self):
|
|
64
|
+
self.assertEqual(double(2), 4)
|
|
65
|
+
|
|
66
|
+
def test_limit(self):
|
|
67
|
+
self.assertEqual(LIMIT, 3)
|
|
68
|
+
|
|
69
|
+
def test_counter(self):
|
|
70
|
+
c = Counter()
|
|
71
|
+
c.bump()
|
|
72
|
+
self.assertEqual(c.bump(), 2)
|
|
73
|
+
|
|
74
|
+
@unittest.skip("not yet")
|
|
75
|
+
def test_later(self):
|
|
76
|
+
pass
|
|
77
|
+
EOF
|
|
78
|
+
commit base; g tag base
|
|
79
|
+
|
|
80
|
+
# ---- 1. baseline: counts, public symbols, cycles ----
|
|
81
|
+
run bash "$RV" baseline --root "$R" --target pkg/big.py --test-cmd "$TEST_CMD" --out "$B"
|
|
82
|
+
eq "baseline rc" "$RC" 0
|
|
83
|
+
OUT="$(cat "$B" 2>/dev/null)"
|
|
84
|
+
eq "baseline keys" "$(q 'keys')" '["cycles","index_dir","lang","root","symbols","target","test","test_cmd"]'
|
|
85
|
+
eq "baseline counts" "$(q '.test')" '{"exit":0,"passed":3,"skipped":1}'
|
|
86
|
+
eq "baseline public symbols" "$(q '[.symbols[]|"\(.module):\(.name)"]')" \
|
|
87
|
+
'["pkg.big:Counter","pkg.big:Counter.__init__","pkg.big:Counter.bump","pkg.big:LIMIT","pkg.big:double","pkg.big:env_name"]'
|
|
88
|
+
eq "baseline excludes private" "$(q '[.symbols[]|select(.name=="_helper")]|length')" 0
|
|
89
|
+
eq "baseline cycles" "$(q '.cycles')" '[]'
|
|
90
|
+
eq "baseline lang" "$(q '.lang')" '"python"'
|
|
91
|
+
|
|
92
|
+
# pure move: symbols verbatim into pkg/big_parts/{core,counter}.py, pkg/big.py becomes an import-only shim.
|
|
93
|
+
pure_move() {
|
|
94
|
+
mkdir -p "$R/pkg/big_parts"
|
|
95
|
+
: > "$R/pkg/big_parts/__init__.py"
|
|
96
|
+
cat > "$R/pkg/big_parts/core.py" <<'EOF'
|
|
97
|
+
import os
|
|
98
|
+
|
|
99
|
+
LIMIT = 3
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _helper(x):
|
|
103
|
+
return x * 2
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def double(x):
|
|
107
|
+
return _helper(x)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def env_name():
|
|
111
|
+
return os.name
|
|
112
|
+
EOF
|
|
113
|
+
cat > "$R/pkg/big_parts/counter.py" <<'EOF'
|
|
114
|
+
class Counter:
|
|
115
|
+
def __init__(self):
|
|
116
|
+
self.n = 0
|
|
117
|
+
|
|
118
|
+
def bump(self):
|
|
119
|
+
self.n += 1
|
|
120
|
+
return self.n
|
|
121
|
+
EOF
|
|
122
|
+
cat > "$R/pkg/big.py" <<'EOF'
|
|
123
|
+
from pkg.big_parts.core import (
|
|
124
|
+
LIMIT,
|
|
125
|
+
_helper,
|
|
126
|
+
double,
|
|
127
|
+
env_name,
|
|
128
|
+
)
|
|
129
|
+
from pkg.big_parts.counter import Counter
|
|
130
|
+
EOF
|
|
131
|
+
}
|
|
132
|
+
case_branch() { g checkout -q -B "$1" base; }
|
|
133
|
+
verify() { run bash "$RV" verify --baseline "$B" --base base; }
|
|
134
|
+
|
|
135
|
+
# ---- 2. pure move plus shim passes ----
|
|
136
|
+
case_branch pure; pure_move; commit pure
|
|
137
|
+
verify
|
|
138
|
+
eq "pure move rc" "$RC" 0
|
|
139
|
+
eq "pure move ok" "$(q '.ok')" true
|
|
140
|
+
eq "pure move no failures" "$(q '.failures')" '[]'
|
|
141
|
+
|
|
142
|
+
# ---- 3. logic diff in a moved body ----
|
|
143
|
+
case_branch logic; pure_move
|
|
144
|
+
sed -i.bak 's/return _helper(x)/return _helper(x) + 0/' "$R/pkg/big_parts/core.py"; rm -f "$R/pkg/big_parts/core.py.bak"
|
|
145
|
+
commit logic
|
|
146
|
+
verify
|
|
147
|
+
eq "logic diff rc" "$RC" 1
|
|
148
|
+
eq "logic diff kinds" "$(q '[.failures[].kind]|unique')" '["logic-diff"]'
|
|
149
|
+
eq "logic diff location" "$(q '[.failures[]|select(.file=="pkg/big_parts/core.py")|.text]')" '["return _helper(x) + 0"]'
|
|
150
|
+
|
|
151
|
+
# ---- 4. a public symbol no longer resolves from its old path ----
|
|
152
|
+
case_branch missing; pure_move
|
|
153
|
+
printf 'from pkg.big_parts.core import LIMIT, double\nfrom pkg.big_parts.counter import Counter\n' > "$R/pkg/big.py"
|
|
154
|
+
commit missing
|
|
155
|
+
verify
|
|
156
|
+
eq "missing symbol rc" "$RC" 1
|
|
157
|
+
eq "missing symbol" "$(q '[.failures[]|select(.kind=="missing-symbol")|"\(.module):\(.name)"]')" '["pkg.big:env_name"]'
|
|
158
|
+
eq "missing symbol: shim stays import-only" "$(q '[.failures[]|select(.kind=="logic-diff")]|length')" 0
|
|
159
|
+
|
|
160
|
+
# ---- 5. changed pass/skip count ----
|
|
161
|
+
case_branch counts; pure_move
|
|
162
|
+
sed -i.bak 's/def test_limit/@unittest.skip("x")\n def test_limit/' "$R/tests/test_big.py"; rm -f "$R/tests/test_big.py.bak"
|
|
163
|
+
commit counts
|
|
164
|
+
verify
|
|
165
|
+
eq "count change rc" "$RC" 1
|
|
166
|
+
eq "count change" "$(q '[.failures[]|select(.kind=="count-changed")|[.before.passed,.before.skipped,.after.passed,.after.skipped]]')" \
|
|
167
|
+
'[[3,1,2,2]]'
|
|
168
|
+
|
|
169
|
+
# ---- 6. new import cycle (import lines only, so no logic diff) ----
|
|
170
|
+
case_branch cycle; pure_move
|
|
171
|
+
printf 'import pkg.big_parts.counter\n' | cat - "$R/pkg/big_parts/core.py" > "$TMP/core.py" && mv "$TMP/core.py" "$R/pkg/big_parts/core.py"
|
|
172
|
+
printf 'import pkg.big_parts.core\n' | cat - "$R/pkg/big_parts/counter.py" > "$TMP/counter.py" && mv "$TMP/counter.py" "$R/pkg/big_parts/counter.py"
|
|
173
|
+
commit cycle
|
|
174
|
+
verify
|
|
175
|
+
eq "new cycle rc" "$RC" 1
|
|
176
|
+
eq "new cycle kinds" "$(q '[.failures[].kind]|unique')" '["new-cycle"]'
|
|
177
|
+
eq "new cycle" "$(q '[.failures[].cycle]')" '[["pkg.big_parts.core","pkg.big_parts.counter"]]'
|
|
178
|
+
|
|
179
|
+
# ---- 7. summary parsers (pytest, TAP, node spec, jest) and unparseable output ----
|
|
180
|
+
g checkout -q base
|
|
181
|
+
counts_of() { run bash "$RV" baseline --root "$R" --target pkg/big.py --test-cmd "$1" --out "$TMP/c.json"; jq -c .test "$TMP/c.json"; }
|
|
182
|
+
eq "pytest summary" "$(counts_of "printf '===== 3 passed, 2 skipped in 0.05s =====\n'")" '{"exit":0,"passed":3,"skipped":2}'
|
|
183
|
+
eq "pytest -q summary" "$(counts_of "printf '7 passed, 1 skipped, 2 warnings in 1.20s\n'")" '{"exit":0,"passed":7,"skipped":1}'
|
|
184
|
+
eq "tap summary" "$(counts_of "printf 'ok 1 - a\n# tests 6\n# pass 5\n# skipped 1\n# fail 0\n'")" '{"exit":0,"passed":5,"skipped":1}'
|
|
185
|
+
eq "node spec summary" "$(counts_of "printf 'ℹ tests 4\nℹ pass 4\nℹ skipped 0\n'")" '{"exit":0,"passed":4,"skipped":0}'
|
|
186
|
+
eq "jest summary" "$(counts_of "printf 'Tests: 1 skipped, 4 passed, 5 total\n'")" '{"exit":0,"passed":4,"skipped":1}'
|
|
187
|
+
run bash "$RV" baseline --root "$R" --target pkg/big.py --test-cmd "echo nothing to see" --out "$TMP/u.json"
|
|
188
|
+
eq "unparseable counts rc" "$RC" 2
|
|
189
|
+
eq "unparseable counts writes nothing" "$([ -e "$TMP/u.json" ] && echo written || echo none)" none
|
|
190
|
+
|
|
191
|
+
# ---- 8. usage ----
|
|
192
|
+
bash "$RV" >/dev/null 2>&1; eq "no args" "$?" 2
|
|
193
|
+
bash "$RV" frobnicate >/dev/null 2>&1; eq "unknown subcommand" "$?" 2
|
|
194
|
+
bash "$RV" verify --baseline "$TMP/none.json" --base base >/dev/null 2>&1; eq "missing baseline" "$?" 2
|
|
195
|
+
bash "$RV" verify --baseline "$B" --base no-such-ref >/dev/null 2>&1; eq "unknown ref" "$?" 2
|
|
196
|
+
bash "$RV" baseline --root "$R" --target missing.py --test-cmd true --out "$TMP/x.json" >/dev/null 2>&1; eq "missing target" "$?" 2
|
|
197
|
+
|
|
198
|
+
# ---- 9. read-only on the target repo ----
|
|
199
|
+
eq "worktree clean after runs" "$(git -C "$R" status --porcelain)" ""
|
|
200
|
+
|
|
201
|
+
echo "refactor-verify.test.sh: $PASS passed, $FAILN failed"
|
|
202
|
+
[ "$FAILN" -eq 0 ]
|
|
@@ -9,11 +9,12 @@
|
|
|
9
9
|
# Output (stdout, one JSON object):
|
|
10
10
|
# {} when the repo declares no workspace packages (root package.json `workspaces` / pnpm-workspace.yaml), else
|
|
11
11
|
# {"package_edges_added":[{from,to,kind,manifest}], "package_edges_removed":[...],
|
|
12
|
-
# "import_pairs":[{from,to,files_added,files_removed}],
|
|
12
|
+
# "import_pairs":[{from,to,files_added,files_removed,files[]}],
|
|
13
13
|
# "boundary_config_changed":{changed,files[],loosens,tightens}}
|
|
14
14
|
# A package edge is a dependencies/devDependencies/peerDependencies key naming another workspace package. An import
|
|
15
15
|
# pair counts changed non-test source files whose added (removed) lines quote an exact workspace name, optionally with
|
|
16
|
-
# a subpath ('@s/x' or '@s/x/sub'); `import type` / `export type` lines are ignored. Counts only report.
|
|
16
|
+
# a subpath ('@s/x' or '@s/x/sub'); `import type` / `export type` lines are ignored. Counts only report. `files` names the
|
|
17
|
+
# files that added the import (sorted, at most 20; forge#4165 telemetry and trajectory).
|
|
17
18
|
# Exit: 0 ok | 2 usage or bad ref. bash 3.2 portable.
|
|
18
19
|
set -uo pipefail
|
|
19
20
|
|
|
@@ -122,14 +123,18 @@ IMPORTS=$(awk -F'\t' '
|
|
|
122
123
|
END {
|
|
123
124
|
for (k in hit) {
|
|
124
125
|
split(k, a, SUBSEP); f = a[1]; to = a[2]; s = a[3]
|
|
125
|
-
if (s == "+" && !((f SUBSEP to SUBSEP "-") in hit)) add[fromof[f] SUBSEP to]
|
|
126
|
+
if (s == "+" && !((f SUBSEP to SUBSEP "-") in hit)) { add[fromof[f] SUBSEP to]++; files[fromof[f] SUBSEP to, add[fromof[f] SUBSEP to]] = f }
|
|
126
127
|
if (s == "-" && !((f SUBSEP to SUBSEP "+") in hit)) rem[fromof[f] SUBSEP to]++
|
|
127
128
|
}
|
|
128
129
|
for (k in add) pair[k] = 1
|
|
129
130
|
for (k in rem) pair[k] = 1
|
|
130
|
-
for (k in pair) {
|
|
131
|
+
for (k in pair) {
|
|
132
|
+
split(k, a, SUBSEP); fl = ""
|
|
133
|
+
for (i = 1; i <= add[k] + 0; i++) fl = fl (i > 1 ? "\037" : "") files[k, i]
|
|
134
|
+
print a[1] "\t" a[2] "\t" (add[k] + 0) "\t" (rem[k] + 0) "\t" fl
|
|
135
|
+
}
|
|
131
136
|
}
|
|
132
|
-
' "$T/dirs" "$T/wsnames" "$T/diff" | sort | jq -R -s -c 'split("\n") | map(select(length > 0) | split("\t") | {from: .[0], to: .[1], files_added: (.[2] | tonumber), files_removed: (.[3] | tonumber)})')
|
|
137
|
+
' "$T/dirs" "$T/wsnames" "$T/diff" | sort | jq -R -s -c 'split("\n") | map(select(length > 0) | split("\t") | {from: .[0], to: .[1], files_added: (.[2] | tonumber), files_removed: (.[3] | tonumber), files: ((.[4] // "") | split("\u001f") | map(select(length > 0)) | sort | .[:20])})')
|
|
133
138
|
|
|
134
139
|
# ---- boundary config (eslint-plugin-boundaries, dependency-cruiser, Nx depConstraints) ----
|
|
135
140
|
: > "$T/bfiles"; LOOSE=false; TIGHT=false
|
|
@@ -68,7 +68,7 @@ eq "monorepo rc" "$rc" 0
|
|
|
68
68
|
eq "edges added" "$(q '.package_edges_added | sort_by(.from)')" '[{"from":"@scope/db","to":"@scope/core","kind":"dependencies","manifest":"packages/db/package.json"},{"from":"@scope/x","to":"@scope/db","kind":"devDependencies","manifest":"packages/x/package.json"}]'
|
|
69
69
|
eq "edges removed" "$(q '.package_edges_removed')" '[{"from":"@scope/web","to":"@scope/old","kind":"dependencies","manifest":"apps/web/package.json"}]'
|
|
70
70
|
eq "non-workspace dep ignored" "$(q '[.package_edges_added[] | select(.to == "zod" or .to == "pg")] | length')" 0
|
|
71
|
-
eq "import pairs" "$(q '.import_pairs | sort_by(.to)')" '[{"from":"@scope/web","to":"@scope/db","files_added":1,"files_removed":0},{"from":"@scope/web","to":"@scope/old","files_added":0,"files_removed":1},{"from":"@scope/web","to":"@scope/x","files_added":2,"files_removed":0}]'
|
|
71
|
+
eq "import pairs" "$(q '.import_pairs | sort_by(.to)')" '[{"from":"@scope/web","to":"@scope/db","files_added":1,"files_removed":0,"files":["apps/web/src/value.ts"]},{"from":"@scope/web","to":"@scope/old","files_added":0,"files_removed":1,"files":[]},{"from":"@scope/web","to":"@scope/x","files_added":2,"files_removed":0,"files":["apps/web/src/dyn.ts","apps/web/src/sub.ts"]}]'
|
|
72
72
|
eq "import type never counted" "$(q '[.import_pairs[] | select(.to == "@scope/core")] | length')" 0
|
|
73
73
|
eq "no boundary change" "$(q '.boundary_config_changed')" '{"changed":false,"files":[],"loosens":false,"tightens":false}'
|
|
74
74
|
# the two-arg form agrees
|
|
@@ -127,7 +127,8 @@ for t in gh curl wget npm npx pnpm yarn nc ssh; do printf '#!/bin/sh\necho "%s"
|
|
|
127
127
|
start=$(date +%s%N 2>/dev/null)
|
|
128
128
|
OUT="$(PATH="$TMP/bin:$PATH" bash "$S" master...big 2>/dev/null || PATH="$TMP/bin:$PATH" bash "$S" main...big)"
|
|
129
129
|
end=$(date +%s%N 2>/dev/null)
|
|
130
|
-
eq "300-file count" "$(q '.import_pairs')" '[{"from":"@s/a","to":"@s/b","files_added":300,"files_removed":0}]'
|
|
130
|
+
eq "300-file count" "$(q '.import_pairs | map(del(.files))')" '[{"from":"@s/a","to":"@s/b","files_added":300,"files_removed":0}]'
|
|
131
|
+
eq "files capped at 20, sorted (forge#4165)" "$(q '.import_pairs[0].files | [length, .[0]]')" '[20,"packages/a/f0.ts"]'
|
|
131
132
|
case "$start$end" in *N*|"") ok ;; *) ms=$(( (end - start) / 1000000 )); if [ "$ms" -lt 2000 ]; then ok; else bad "300-file diff took ${ms} ms (limit 2000)"; fi ;; esac
|
|
132
133
|
if [ -e "$TMP/net.log" ]; then bad "network tool invoked: $(tr '\n' ' ' < "$TMP/net.log")"; else ok; fi
|
|
133
134
|
|