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
|
@@ -114,7 +114,17 @@ run "no relevant changes pass" 0
|
|
|
114
114
|
|
|
115
115
|
reset
|
|
116
116
|
env PR_BODY="" bash "$S" no/such-ref >"$TMP/out" 2>&1; [ $? = 2 ] && PASS=$((PASS+1)) || bad "unknown base ref exits 2"
|
|
117
|
-
env bash "$S" >"$TMP/out" 2>&1; [ $? = 2 ] && PASS=$((PASS+1)) || bad "missing base ref exits 2"
|
|
117
|
+
env bash "$S" >"$TMP/out" 2>&1; [ $? = 2 ] && PASS=$((PASS+1)) || bad "missing base ref and no upstream exits 2"
|
|
118
|
+
|
|
119
|
+
# #4220: without a base ref the branch's upstream is the base (CI always passes it explicitly).
|
|
120
|
+
git branch -q --set-upstream-to=origin/staging
|
|
121
|
+
printf '#!/bin/sh\necho bar\n' > scripts/foo.sh; commit
|
|
122
|
+
env PR_BODY="" bash "$S" >"$TMP/out" 2>&1; [ $? = 1 ] && PASS=$((PASS+1)) || bad "no base ref: upstream diff, docs missing fails"
|
|
123
|
+
printf 'docs-not-needed: fixture\n' > "$TMP/body"
|
|
124
|
+
env PR_BODY="" bash "$S" --body-file "$TMP/body" >"$TMP/out" 2>&1; [ $? = 0 ] && PASS=$((PASS+1)) || bad "no base ref + --body-file uses the upstream"
|
|
125
|
+
printf '# ADR\n' > devdocs/decisions/4220-x.md; commit
|
|
126
|
+
env PR_BODY="" bash "$S" >"$TMP/out" 2>&1; [ $? = 0 ] && PASS=$((PASS+1)) || bad "no base ref: upstream diff with a new decision passes"
|
|
127
|
+
git branch -q --unset-upstream
|
|
118
128
|
|
|
119
129
|
echo "check-devdocs-freshness.test.sh: $PASS passed, $FAILN failed"
|
|
120
130
|
[ "$FAILN" = 0 ]
|
|
@@ -91,7 +91,9 @@ MARKER_REGISTRY="
|
|
|
91
91
|
BENCH_SCORECARD
|
|
92
92
|
BLAST_RADIUS
|
|
93
93
|
BLOCKED_ON_HUMAN_MERGE
|
|
94
|
+
BOOTSTRAP_PLAN
|
|
94
95
|
BODY
|
|
96
|
+
BOOTSTRAP_PLAN
|
|
95
97
|
BUILDER
|
|
96
98
|
BUILD_BLOCKED
|
|
97
99
|
CALIBRATION_CHECK
|
|
@@ -106,6 +108,7 @@ MARKER_REGISTRY="
|
|
|
106
108
|
COORD_ISSUE
|
|
107
109
|
CRITIQUE
|
|
108
110
|
DECISION_RECORD
|
|
111
|
+
BOOTSTRAP_PLAN
|
|
109
112
|
DECOMPOSED
|
|
110
113
|
DECOMPOSE_BLOCKED
|
|
111
114
|
DEGRADED_REREVIEW
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# check-spec-size.sh — PR spec-size budget (forge#3762).
|
|
2
|
+
# check-spec-size.sh — PR spec-size budget for every command spec (forge#3762, #4164).
|
|
3
3
|
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
4
4
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
5
|
#
|
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
# Exit: 0 pass/exempt, 1 budget violation, 2 usage/base-ref error (fail closed)
|
|
14
14
|
#
|
|
15
15
|
# Rules (against the merge-base of HEAD and the PR base, compared in bytes):
|
|
16
|
-
# 1.
|
|
16
|
+
# 1. Every commands/**/*.md spec (head files plus files deleted at head) may not grow by more
|
|
17
|
+
# than 2% or more than 4096 bytes. A new spec (absent at the base) may be at most 4096 bytes.
|
|
18
|
+
# CEILINGS adds a per-file hard size cap (path:max_bytes) checked whatever the growth.
|
|
17
19
|
# 2. The NET count of canonical block markers across all of commands/**/*.md
|
|
18
20
|
# may not rise (moving a block between files, or replacing one with a stub,
|
|
19
21
|
# is allowed). Count-based until the shared helper lands (#3493).
|
|
@@ -25,8 +27,9 @@ set -uo pipefail
|
|
|
25
27
|
|
|
26
28
|
BASE="${1:-${BASE_REF:-origin/staging}}"
|
|
27
29
|
PCT_NUM=2 # 2%
|
|
28
|
-
ABS_MAX=4096 # bytes
|
|
29
|
-
|
|
30
|
+
ABS_MAX=4096 # bytes; also the size cap for a new spec
|
|
31
|
+
# Per-file hard ceilings, one path:max_bytes per line (a spec converted to a script stub stays small).
|
|
32
|
+
CEILINGS="commands/issue.md:15360"
|
|
30
33
|
|
|
31
34
|
SUMMARY="${GITHUB_STEP_SUMMARY:-/dev/stdout}"
|
|
32
35
|
out() { printf '%s\n' "$*" >> "$SUMMARY"; }
|
|
@@ -67,27 +70,42 @@ out "## Spec-size budget (base: \`$BASE\`)"
|
|
|
67
70
|
out ""
|
|
68
71
|
out "| File | Base | Head | Delta | Delta % | Status |"
|
|
69
72
|
out "|------|-----:|-----:|------:|--------:|--------|"
|
|
70
|
-
|
|
71
|
-
|
|
73
|
+
ceiling_of() { printf '%s\n' "$CEILINGS" | awk -F: -v f="$1" '$1 == f {print $2; exit}'; }
|
|
74
|
+
HEAD_FILES=$(git ls-files 'commands/*.md' | sort -u)
|
|
75
|
+
BASE_FILES=$(git ls-tree -r --name-only "$BASE" -- commands | grep -E '\.md$' | sort -u)
|
|
76
|
+
ALL_FILES=$(printf '%s\n%s\n' "$HEAD_FILES" "$BASE_FILES" | sort -u)
|
|
77
|
+
SPECS=0
|
|
78
|
+
for f in $ALL_FILES; do
|
|
79
|
+
[ -n "$f" ] || continue
|
|
80
|
+
SPECS=$((SPECS + 1))
|
|
72
81
|
b=$(base_size "$f")
|
|
73
82
|
h=0; [ -f "$f" ] && h=$(wc -c < "$f" | tr -d ' ')
|
|
74
83
|
g=$((h - b))
|
|
84
|
+
cap=$(ceiling_of "$f")
|
|
75
85
|
status="ok"
|
|
76
|
-
if [ "$
|
|
77
|
-
status="OVER BUDGET
|
|
86
|
+
if [ "$b" -eq 0 ]; then
|
|
87
|
+
[ "$h" -gt "$ABS_MAX" ] && status="OVER BUDGET (new spec > $ABS_MAX bytes)"
|
|
88
|
+
elif [ "$g" -gt 0 ] && { [ $((g * 100)) -gt $((b * PCT_NUM)) ] || [ "$g" -gt "$ABS_MAX" ]; }; then
|
|
89
|
+
status="OVER BUDGET"
|
|
90
|
+
fi
|
|
91
|
+
if [ -n "$cap" ] && [ "$h" -gt "$cap" ]; then
|
|
92
|
+
status="OVER CEILING ($cap bytes)"
|
|
78
93
|
fi
|
|
94
|
+
# Only changed or failing specs get a row; the table would otherwise list every spec.
|
|
95
|
+
[ "$g" -ne 0 ] || [ "$status" != "ok" ] || continue
|
|
96
|
+
[ "$status" = "ok" ] || FAILURES=$((FAILURES + 1))
|
|
79
97
|
if [ "$b" -gt 0 ]; then
|
|
80
98
|
p=$(awk -v g="$g" -v b="$b" 'BEGIN{printf "%+.2f%%", g*100/b}')
|
|
81
99
|
else p="n/a"; fi
|
|
82
100
|
out "| \`$f\` | $b | $h | $g | $p | $status |"
|
|
83
101
|
done
|
|
102
|
+
out ""
|
|
103
|
+
out "$SPECS spec(s) budgeted; unchanged specs within budget are not listed."
|
|
84
104
|
|
|
85
105
|
# Net marker total across every commands/**/*.md: head (working tree, incl. new
|
|
86
106
|
# files) vs base (incl. files deleted at head). Fail only when the total rises.
|
|
87
107
|
HEAD_TOTAL=0; BASE_TOTAL=0; MARKER_ROWS=""
|
|
88
|
-
|
|
89
|
-
BASE_FILES=$(git ls-tree -r --name-only "$BASE" -- commands | grep -E '\.md$' | sort -u)
|
|
90
|
-
for f in $(printf '%s\n%s\n' "$HEAD_FILES" "$BASE_FILES" | sort -u); do
|
|
108
|
+
for f in $ALL_FILES; do
|
|
91
109
|
[ -n "$f" ] || continue
|
|
92
110
|
hc=0; [ -f "$f" ] && hc=$(grep -cE "$MARKERS" "$f" 2>/dev/null || true); hc=${hc:-0}
|
|
93
111
|
bc=$(git show "$BASE:$f" 2>/dev/null | grep -cE "$MARKERS" 2>/dev/null || true); bc=${bc:-0}
|
|
@@ -45,8 +45,30 @@ run "over 4 KB fails" 1
|
|
|
45
45
|
reset; gen 5 >> commands/review-pr.md # small growth under both limits
|
|
46
46
|
run "small growth passes" 0
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
# Every spec is budgeted (#4164): top-level and nested specs outside the old review list
|
|
49
|
+
reset; gen 5000 >> commands/other.md
|
|
50
|
+
run "formerly unbudgeted spec growth fails" 1; has "other spec flagged" "| \`commands/other.md\`"
|
|
51
|
+
reset; gen 1000 > commands/work-on/nested.md; git add -A; git commit -qm nested; git push -q origin HEAD:staging 2>/dev/null; git fetch -q origin staging
|
|
52
|
+
gen 100 >> commands/work-on/nested.md
|
|
53
|
+
run "nested spec over 2% fails" 1
|
|
54
|
+
git reset -q --hard HEAD~1; git push -q -f origin HEAD:staging 2>/dev/null; git fetch -q origin staging; reset
|
|
55
|
+
reset; gen 2 >> commands/other.md
|
|
56
|
+
run "unbudgeted-before small growth passes" 0
|
|
57
|
+
|
|
58
|
+
# New specs: up to 4096 bytes
|
|
59
|
+
reset; gen 100 > commands/brand-new.md; git add -A # ~2.8 KB
|
|
60
|
+
run "new spec under 4 KB passes" 0
|
|
61
|
+
reset; gen 200 > commands/brand-new.md; git add -A # ~5.6 KB
|
|
62
|
+
run "new spec over 4 KB fails" 1; has "new spec flagged" "new spec >"
|
|
63
|
+
|
|
64
|
+
# Per-file ceiling: commands/issue.md may not exceed 15360 bytes whatever the growth
|
|
65
|
+
reset; head -c 15300 /dev/zero | tr '\0' 'x' > commands/issue.md; git add -A; git commit -qm issue; git push -q origin HEAD:staging 2>/dev/null; git fetch -q origin staging
|
|
66
|
+
printf '%050d' 0 >> commands/issue.md # 15350 bytes: +0.3%, under the cap
|
|
67
|
+
run "ceiling: under the cap passes" 0
|
|
68
|
+
printf '%050d' 0 >> commands/issue.md # 15400 bytes: +0.65% (within 2%/4 KB) but over the cap
|
|
69
|
+
run "ceiling: over the cap fails" 1; has "ceiling flagged" "OVER CEILING (15360 bytes)"
|
|
70
|
+
run "ceiling: exempt label waives" 0 PR_LABELS="spec-size-exempt"
|
|
71
|
+
git reset -q --hard HEAD~1; git push -q -f origin HEAD:staging 2>/dev/null; git fetch -q origin staging; reset
|
|
50
72
|
|
|
51
73
|
reset; gen 100 > commands/review-pr.md # shrink
|
|
52
74
|
run "shrink passes" 0
|
|
@@ -89,9 +111,10 @@ run "copy without removal still fails" 1
|
|
|
89
111
|
# Shared-helper stubs (forge#3778): the sourced-helper stub shape carries none of the markers, so converting an inlined block to a stub
|
|
90
112
|
# is a net reduction, while re-pasting the full block still fails.
|
|
91
113
|
STUB_LOC='type forge_resolve >/dev/null 2>&1 || for _r in "${FORGEDOCK_HOME:-}"; do . "$_r/scripts/lib/forge-resolve.sh"; done'
|
|
92
|
-
|
|
114
|
+
# (appended to the large work-on.md so the byte budget stays out of the marker cases)
|
|
115
|
+
reset; printf '%s\n' "$STUB_LOC" '. "$(forge_resolve lib/fence-bootstrap.sh)" || exit 1 # FENCE_BOOTSTRAP' 'HELPER_SCRIPT=$(forge_resolve x.sh)' >> commands/work-on.md
|
|
93
116
|
run "one-tag-per-stub design passes" 0
|
|
94
|
-
reset; : > commands/donor.md; printf '%s\n' "$STUB_LOC" 'HELPER_SCRIPT=$(forge_resolve x.sh)' >> commands/
|
|
117
|
+
reset; : > commands/donor.md; printf '%s\n' "$STUB_LOC" 'HELPER_SCRIPT=$(forge_resolve x.sh)' >> commands/work-on.md
|
|
95
118
|
run "block converted to a stub passes" 0
|
|
96
119
|
reset; printf '%s\n' "$STUB_LOC" 'HELPER_SCRIPT=""' >> commands/other.md
|
|
97
120
|
run "stub plus a re-pasted resolver block fails" 1
|
package/scripts/ci-local/lib.mjs
CHANGED
|
@@ -327,7 +327,7 @@ export function shellQuote(s) {
|
|
|
327
327
|
// An allowlist, not a blacklist: remote CI starts from a clean environment, so ambient operator state (agent
|
|
328
328
|
// credentials, install-root overrides, git hook variables such as GIT_DIR/GIT_INDEX_FILE, FORGE_*/CLAUDE_* settings)
|
|
329
329
|
// must not reach the jobs. Toolchain variables are passed through; credential-looking names are always dropped.
|
|
330
|
-
const ENV_EXACT = new Set(['PATH', 'HOME', 'USER', 'LOGNAME', 'SHELL', 'LANG', 'LANGUAGE', 'TERM', 'TMPDIR', 'TMP', 'TEMP', 'TZ', 'COLORTERM', 'NO_COLOR', 'SSL_CERT_FILE', 'SSL_CERT_DIR', 'NODE_EXTRA_CA_CERTS', 'REQUESTS_CA_BUNDLE', 'CURL_CA_BUNDLE', 'HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY', 'http_proxy', 'https_proxy', 'no_proxy', 'DOCKER_HOST', 'JAVA_HOME', 'GOPATH', 'GOROOT', 'GOFLAGS', 'GOCACHE', 'GOMODCACHE', 'GOPROXY', 'GOTOOLCHAIN', 'VIRTUAL_ENV', 'PYTHONPATH', 'PYTHONDONTWRITEBYTECODE', 'RUSTFLAGS', 'RUSTUP_HOME', 'SDKMAN_DIR', 'PNPM_HOME', 'BUN_INSTALL', 'NVM_DIR', 'NVM_BIN', 'NVM_INC', 'RBENV_ROOT', 'PYENV_ROOT', 'ASDF_DIR', 'ASDF_DATA_DIR', 'MISE_DATA_DIR', 'HOMEBREW_PREFIX', 'ComSpec', 'SystemRoot', 'USERPROFILE', 'APPDATA', 'LOCALAPPDATA', 'PROGRAMFILES', 'MSYSTEM']);
|
|
330
|
+
const ENV_EXACT = new Set(['PATH', 'HOME', 'USER', 'LOGNAME', 'SHELL', 'LANG', 'LANGUAGE', 'TERM', 'TMPDIR', 'TMP', 'TEMP', 'TZ', 'COLORTERM', 'NO_COLOR', 'SSL_CERT_FILE', 'SSL_CERT_DIR', 'NODE_EXTRA_CA_CERTS', 'REQUESTS_CA_BUNDLE', 'CURL_CA_BUNDLE', 'HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY', 'http_proxy', 'https_proxy', 'no_proxy', 'DOCKER_HOST', 'JAVA_HOME', 'GOPATH', 'GOROOT', 'GOFLAGS', 'GOCACHE', 'GOMODCACHE', 'GOPROXY', 'GOTOOLCHAIN', 'VIRTUAL_ENV', 'PYTHONPATH', 'PYTHONDONTWRITEBYTECODE', 'RUSTFLAGS', 'RUSTUP_HOME', 'SDKMAN_DIR', 'PNPM_HOME', 'BUN_INSTALL', 'NVM_DIR', 'NVM_BIN', 'NVM_INC', 'RBENV_ROOT', 'PYENV_ROOT', 'ASDF_DIR', 'ASDF_DATA_DIR', 'MISE_DATA_DIR', 'HOMEBREW_PREFIX', 'ComSpec', 'SystemRoot', 'USERPROFILE', 'APPDATA', 'LOCALAPPDATA', 'PROGRAMFILES', 'MSYSTEM', 'FORGE_HOST_PARALLELISM']);
|
|
331
331
|
const ENV_PREFIX = ['LC_', 'XDG_', 'CARGO_', 'MAVEN_', 'M2_', 'GRADLE_', 'DOTNET_', 'npm_config_', 'NPM_CONFIG_', 'YARN_', 'UV_', 'POETRY_', 'PIP_CACHE', 'FORGE_CI_LOCAL_'];
|
|
332
332
|
// Substring tokens are unambiguous; short ones (PASS, PAT, KEY, IDENT, CRED, SESSION, COOKIE) match only as whole
|
|
333
333
|
// name segments (or a trailing _KEY) so benign names such as FORGE_CI_LOCAL_PASSTHROUGH or PATH are kept.
|
|
@@ -347,6 +347,13 @@ export function scrubbedEnv(extra = {}) {
|
|
|
347
347
|
return Object.assign(env, extra);
|
|
348
348
|
}
|
|
349
349
|
|
|
350
|
+
// Host-derived worker cap a ForgeDock lane exports (#4182): never run more jobs/slots than FORGE_HOST_PARALLELISM.
|
|
351
|
+
export function capParallel(n, env = process.env) {
|
|
352
|
+
const s = String(env?.FORGE_HOST_PARALLELISM ?? '').trim();
|
|
353
|
+
const p = /^\d+$/.test(s) ? Number(s) : 0;
|
|
354
|
+
return p > 0 ? Math.min(n, p) : n;
|
|
355
|
+
}
|
|
356
|
+
|
|
350
357
|
export function homeForgeDir() {
|
|
351
358
|
return process.env.FORGE_CI_LOCAL_HOME || path.join(process.env.HOME || os.homedir(), '.forge');
|
|
352
359
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
import { describe, it } from "node:test";
|
|
4
|
+
import assert from "node:assert/strict";
|
|
5
|
+
import { scrubbedEnv, capParallel } from "./lib.mjs";
|
|
6
|
+
|
|
7
|
+
describe("ci-local host parallelism (#4182)", () => {
|
|
8
|
+
it("scrubbedEnv passes FORGE_HOST_PARALLELISM through and still drops secrets", () => {
|
|
9
|
+
const keep = { FORGE_HOST_PARALLELISM: process.env.FORGE_HOST_PARALLELISM, FD_TEST_API_TOKEN: process.env.FD_TEST_API_TOKEN, FORGE_OTHER_SETTING: process.env.FORGE_OTHER_SETTING };
|
|
10
|
+
process.env.FORGE_HOST_PARALLELISM = "2"; process.env.FD_TEST_API_TOKEN = "s3cret"; process.env.FORGE_OTHER_SETTING = "x";
|
|
11
|
+
try {
|
|
12
|
+
const env = scrubbedEnv({});
|
|
13
|
+
assert.equal(env.FORGE_HOST_PARALLELISM, "2");
|
|
14
|
+
assert.equal(env.FD_TEST_API_TOKEN, undefined);
|
|
15
|
+
assert.equal(env.FORGE_OTHER_SETTING, undefined, "other FORGE_* settings stay out");
|
|
16
|
+
} finally {
|
|
17
|
+
for (const [k, v] of Object.entries(keep)) { if (v === undefined) delete process.env[k]; else process.env[k] = v; }
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("capParallel caps slots/job parallelism at a positive FORGE_HOST_PARALLELISM, else leaves them alone", () => {
|
|
22
|
+
assert.equal(capParallel(4, { FORGE_HOST_PARALLELISM: "2" }), 2);
|
|
23
|
+
assert.equal(capParallel(1, { FORGE_HOST_PARALLELISM: "2" }), 1);
|
|
24
|
+
assert.equal(capParallel(4, {}), 4);
|
|
25
|
+
for (const bad of ["0", "abc", "-1", "", " "]) assert.equal(capParallel(4, { FORGE_HOST_PARALLELISM: bad }), 4, JSON.stringify(bad));
|
|
26
|
+
});
|
|
27
|
+
});
|
package/scripts/ci-local/run.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import fs from 'node:fs';
|
|
|
11
11
|
import net from 'node:net';
|
|
12
12
|
import os from 'node:os';
|
|
13
13
|
import path from 'node:path';
|
|
14
|
-
import { run as runSync, commandExists, scrubbedEnv, homeForgeDir, sha256, fileHash, toolVersion, shellSplit } from './lib.mjs';
|
|
14
|
+
import { run as runSync, commandExists, scrubbedEnv, capParallel, homeForgeDir, sha256, fileHash, toolVersion, shellSplit } from './lib.mjs';
|
|
15
15
|
import { shellArgv } from './adapters/common.mjs';
|
|
16
16
|
import { checkPortability } from './adapters/portability.mjs';
|
|
17
17
|
|
|
@@ -25,7 +25,7 @@ for (const sig of ['SIGINT', 'SIGTERM', 'SIGHUP']) process.on(sig, () => { clean
|
|
|
25
25
|
// ---- machine-wide slot semaphore ----
|
|
26
26
|
function slotCount() {
|
|
27
27
|
const n = Number(process.env.FORGE_CI_LOCAL_SLOTS);
|
|
28
|
-
return n > 0 ? n : Math.max(2, Math.min(8, Math.floor(os.cpus().length / 2)));
|
|
28
|
+
return capParallel(n > 0 ? n : Math.max(2, Math.min(8, Math.floor(os.cpus().length / 2))));
|
|
29
29
|
}
|
|
30
30
|
function alive(pid) { try { process.kill(pid, 0); return true; } catch (e) { return e.code === 'EPERM'; } }
|
|
31
31
|
|
|
@@ -372,7 +372,7 @@ export async function runPlan(plan, o = {}) {
|
|
|
372
372
|
// (DEGRADED) and a re-run resumes from the result cache.
|
|
373
373
|
const maxInvS = Number(process.env.FORGE_CI_LOCAL_MAX_INVOCATION_SECONDS) || 540;
|
|
374
374
|
const budgetS = Math.min(requestedS, maxInvS); const capped = requestedS > maxInvS;
|
|
375
|
-
const maxParallel = o.maxParallel || plan.config.max_parallel || Number(process.env.FORGE_CI_LOCAL_MAX_PARALLEL) || 4;
|
|
375
|
+
const maxParallel = capParallel(o.maxParallel || plan.config.max_parallel || Number(process.env.FORGE_CI_LOCAL_MAX_PARALLEL) || 4);
|
|
376
376
|
const tmpRoot = fs.mkdtempSync(path.join(process.env.TMPDIR || os.tmpdir(), 'forge-ci-local.'));
|
|
377
377
|
cleanups.add(() => { try { fs.rmSync(tmpRoot, { recursive: true, force: true }); } catch { /* ignore */ } });
|
|
378
378
|
const ctx = {
|
|
@@ -342,6 +342,7 @@ export function scopeCommand(cmd, cwdRel, sctx, depth = 0) {
|
|
|
342
342
|
|
|
343
343
|
let r = null;
|
|
344
344
|
if (LINTERS[tool] && !(LINTERS[tool].sub && !LINTERS[tool].sub.has(rest[1]))) r = scopeLint(tool, rest, cmd, cwdRel, sctx, join);
|
|
345
|
+
else if (tool === 'tsc') r = scopeTsc(rest, cmd, cwdRel, sctx, join);
|
|
345
346
|
else if (tool === 'jest') r = scopeJest(rest, cmd, cwdRel, sctx, join);
|
|
346
347
|
else if (tool === 'vitest') r = scopeVitest(rest, cmd, cwdRel, sctx, join);
|
|
347
348
|
else if (tool === 'pytest' || tool === 'py.test' || (/^python[0-9.]*$/.test(tool) && rest[1] === '-m' && rest[2] === 'pytest')) r = scopeRunner('pytest', tool === 'pytest' || tool === 'py.test' ? rest.slice(1) : rest.slice(3), tool === 'pytest' || tool === 'py.test' ? [rest[0]] : rest.slice(0, 3), cmd, cwdRel, sctx, join);
|
|
@@ -507,6 +508,87 @@ function scopeMaven(rest, cmd, cwdRel, sctx, join) {
|
|
|
507
508
|
return scoped(join([...rest, '-pl', [...mods].sort().join(','), '-amd']), mods.size, null, 'maven-affected');
|
|
508
509
|
}
|
|
509
510
|
|
|
511
|
+
// ---- TypeScript compiler: the owning tsconfig project(s) of the changed files and of the files that reference them ----
|
|
512
|
+
const TS_EXT = set('ts tsx mts cts js jsx mjs cjs');
|
|
513
|
+
const TSC_VALUE = set('-p --project -t --target -m --module --outDir --outFile --rootDir --lib --jsx --tsBuildInfoFile --declarationDir --moduleResolution --baseUrl --types --typeRoots --locale --generateTrace --maxNodeModuleJsDepth --moduleDetection --verbatimModuleSyntax');
|
|
514
|
+
|
|
515
|
+
// tsconfig.json is JSON with comments and trailing commas; null when unreadable.
|
|
516
|
+
function readTsconfig(sctx, dir) {
|
|
517
|
+
sctx.cache.tsconfig = sctx.cache.tsconfig || new Map();
|
|
518
|
+
if (sctx.cache.tsconfig.has(dir)) return sctx.cache.tsconfig.get(dir);
|
|
519
|
+
let cfg = null;
|
|
520
|
+
try {
|
|
521
|
+
const raw = fs.readFileSync(path.join(sctx.repo, dir, 'tsconfig.json'), 'utf8');
|
|
522
|
+
const noComments = raw.replace(/("(?:[^"\\]|\\.)*")|\/\/[^\n]*|\/\*[\s\S]*?\*\//g, (m, q) => q || '');
|
|
523
|
+
cfg = JSON.parse(noComments.replace(/,(\s*[}\]])/g, '$1'));
|
|
524
|
+
} catch { cfg = null; }
|
|
525
|
+
sctx.cache.tsconfig.set(dir, cfg);
|
|
526
|
+
return cfg;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// The directory of the nearest tsconfig.json above `file` ('.' for the repo root), or '' when none owns it.
|
|
530
|
+
export function tsProjectOf(sctx, file) {
|
|
531
|
+
for (let d = px.dirname(file); ; d = px.dirname(d)) {
|
|
532
|
+
if (sctx.present.has(d === '.' ? 'tsconfig.json' : `${d}/tsconfig.json`)) return d;
|
|
533
|
+
if (d === '.' || d === '/') return '';
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// A project `tsc -b` can build: composite itself, or listed in another tsconfig's `references`.
|
|
538
|
+
function tsBuildable(sctx, dir) {
|
|
539
|
+
const cfg = readTsconfig(sctx, dir);
|
|
540
|
+
if (cfg && cfg.compilerOptions && cfg.compilerOptions.composite === true) return true;
|
|
541
|
+
if (!sctx.cache.tsRefs) {
|
|
542
|
+
sctx.cache.tsRefs = new Set();
|
|
543
|
+
for (const f of sctx.tracked) {
|
|
544
|
+
if (px.basename(f) !== 'tsconfig.json' || !sctx.present.has(f)) continue;
|
|
545
|
+
const from = px.dirname(f); const c = readTsconfig(sctx, from);
|
|
546
|
+
for (const r of (c && Array.isArray(c.references) ? c.references : [])) {
|
|
547
|
+
if (!r || typeof r.path !== 'string') continue;
|
|
548
|
+
const to = px.normalize(px.join(from, r.path));
|
|
549
|
+
sctx.cache.tsRefs.add(to.endsWith('.json') ? px.dirname(to) : to);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
return sctx.cache.tsRefs.has(dir);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function scopeTsc(rest, cmd, cwdRel, sctx, join) {
|
|
557
|
+
const flags = []; let project = ''; let build = false; const buildTargets = [];
|
|
558
|
+
for (let i = 1; i < rest.length; i++) {
|
|
559
|
+
const t = rest[i];
|
|
560
|
+
if (t === '-p' || t === '--project') { if (i + 1 >= rest.length) return full(cmd, '', { selector: 'unscoped:tsc-args' }); project = rest[++i]; continue; }
|
|
561
|
+
if (t.startsWith('--project=')) { project = t.slice(10); continue; }
|
|
562
|
+
if (t === '-b' || t === '--build') { build = true; continue; }
|
|
563
|
+
if (t === '-w' || t === '--watch') return full(cmd, '', { selector: 'unscoped:tsc-watch' });
|
|
564
|
+
if (t.startsWith('-')) { flags.push(t); if (TSC_VALUE.has(t) && i + 1 < rest.length) flags.push(rest[++i]); continue; }
|
|
565
|
+
if (!build) return full(cmd, '', { selector: 'unscoped:tsc-explicit-files' });
|
|
566
|
+
buildTargets.push(t);
|
|
567
|
+
}
|
|
568
|
+
if (/[$`*?]/.test(project) || buildTargets.some((t) => /[$`*?]/.test(t))) return full(cmd, '', { selector: 'unscoped:complex-target' });
|
|
569
|
+
// The project this command checks: -p X, the single -b target, else the tsconfig in its cwd.
|
|
570
|
+
const targetOf = (t) => { const r = rel(cwdRel, t); return r.endsWith('.json') ? px.dirname(r) : r; };
|
|
571
|
+
if (buildTargets.length > 1) return full(cmd, '', { selector: 'unscoped:explicit-targets' });
|
|
572
|
+
const target = project ? targetOf(project) : buildTargets.length ? targetOf(buildTargets[0]) : rel(cwdRel, '.');
|
|
573
|
+
if (sctx.widen) return full(cmd, sctx.widen, { selector: 'tsc-project' });
|
|
574
|
+
const un = unmappedFile(sctx.changed, TS_EXT);
|
|
575
|
+
if (un) return full(cmd, `unknown-language: ${un}`, { selector: 'tsc-project' });
|
|
576
|
+
const r = reach(sctx);
|
|
577
|
+
if (r.widen) return full(cmd, r.widen, { selector: 'tsc-project' });
|
|
578
|
+
const tsFiles = [...r.files].filter((f) => TS_EXT.has(extOf(f)));
|
|
579
|
+
if (!tsFiles.length) return skipNo(0, null, 'tsc-project', 'no changed TS/JS file');
|
|
580
|
+
const owners = dedupe(tsFiles.map((f) => tsProjectOf(sctx, f)).filter(Boolean))
|
|
581
|
+
.filter((d) => target === '.' || d === target || d.startsWith(target + '/')).sort();
|
|
582
|
+
if (!owners.length) return skipNo(0, null, 'tsc-project', `no changed TS/JS file in ${target}`);
|
|
583
|
+
// A command already aimed at one non-root project is that project's check: keep it as is.
|
|
584
|
+
if (target !== '.') return scoped(cmd, owners.length, null, 'tsc-project', { projects: owners });
|
|
585
|
+
if (owners.includes('.')) return full(cmd, 'root-project', { selector: 'tsc-project', projects: owners });
|
|
586
|
+
if (owners.length === 1) return scoped(join([rest[0], build ? '-b' : '-p', fromCwd(cwdRel, owners[0]), ...flags]), 1, null, 'tsc-project', { projects: owners });
|
|
587
|
+
if (!owners.every((d) => tsBuildable(sctx, d))) return full(cmd, 'multi-project', { selector: 'tsc-project', projects: owners });
|
|
588
|
+
// Build mode rejects --noEmit; composite projects emit their declarations as the repo's own build does.
|
|
589
|
+
return scoped(join([rest[0], '-b', ...owners.map((d) => fromCwd(cwdRel, d)), ...flags.filter((f) => f !== '--noEmit')]), owners.length, null, 'tsc-project', { projects: owners });
|
|
590
|
+
}
|
|
591
|
+
|
|
510
592
|
function scopeMono(tool, rest, cmd, sctx, join) {
|
|
511
593
|
if (sctx.widen) return full(cmd, sctx.widen, { selector: `${tool}-affected` });
|
|
512
594
|
const base = sctx.from || sctx.baseRef;
|
package/scripts/ci-local.test.sh
CHANGED
|
@@ -432,7 +432,7 @@ SHIM
|
|
|
432
432
|
chmod +x "$T/bin/docker"
|
|
433
433
|
export FAKE_DOCKER_LOG="$T/docker.log"
|
|
434
434
|
: > "$FAKE_DOCKER_LOG"
|
|
435
|
-
OUT=$(PATH="$T/bin:$PATH" bash "$S" run --repo "$r" --base main --base-branch main --head-branch feat 2>&1); RC=$?
|
|
435
|
+
OUT=$(env -u FORGE_DOCKER_LABELS PATH="$T/bin:$PATH" bash "$S" run --repo "$r" --base main --base-branch main --head-branch feat 2>&1); RC=$?
|
|
436
436
|
eq "a service job runs when docker is available" 0 "$RC"
|
|
437
437
|
LOG=$(cat "$FAKE_DOCKER_LOG")
|
|
438
438
|
has "container is started with the same image" "$LOG" "postgres:16"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .c import value
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
2
|
+
|
|
3
|
+
from . import c
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from . import a
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def step():
|
|
10
|
+
from .a import run # function-local: not a module-level edge
|
|
11
|
+
return c.value + (0 if run else 1)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
from .a import run as _run_alias # noqa: E402 module-level a <-> b cycle
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
value = 1
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Fixture module for symbol-index.test.sh. SECRET_DOCSTRING_MARKER"""
|
|
2
|
+
import os
|
|
3
|
+
from collections import OrderedDict as OD
|
|
4
|
+
|
|
5
|
+
__all__ = ["Greeter", "greet", "fetch", "LIMIT"]
|
|
6
|
+
|
|
7
|
+
LIMIT = 10
|
|
8
|
+
_cache = OD()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _normalise(name):
|
|
12
|
+
return name.strip().lower()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def greet(name):
|
|
16
|
+
return "hi " + _normalise(name)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def fetch(key):
|
|
20
|
+
return _cache.get(key, LIMIT)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def helper():
|
|
24
|
+
return os.getcwd()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Greeter:
|
|
28
|
+
"""SECRET_CLASS_DOC_MARKER"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, name):
|
|
31
|
+
self.name = name
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def text(self):
|
|
35
|
+
return greet(self.name)
|
|
36
|
+
|
|
37
|
+
def _shout(self):
|
|
38
|
+
return self.text.upper()
|
|
39
|
+
|
|
40
|
+
def shout(self):
|
|
41
|
+
return self._shout()
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
export const LIMIT = 10;
|
|
4
|
+
|
|
5
|
+
export function load(path: string): string {
|
|
6
|
+
return readFileSync(path, "utf8").slice(0, LIMIT);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class Loader {
|
|
10
|
+
read(path: string): string {
|
|
11
|
+
return load(path);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
3
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
#
|
|
5
|
+
# gen_big.py: deterministic fixture for scripts/symbol-move.test.sh (forge#4210). stdlib only, no randomness.
|
|
6
|
+
# Usage: python3 gen_big.py <out_root>
|
|
7
|
+
# Writes <out_root>/pkg/__init__.py, pkg/big.py (~3k lines) and tests/__init__.py, tests/test_big.py.
|
|
8
|
+
# big.py groups (the test's plan): consts (LIMIT_*, _SEPARATOR), helpers (_scale_*, compute_*, join_parts),
|
|
9
|
+
# aio (fetch_*), models (Item_*); VERSION, describe() and DEFAULT_TOTAL stay behind and use moved names.
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
N_FUNCS = 90
|
|
15
|
+
N_ASYNC = 30
|
|
16
|
+
N_CLASSES = 30
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def big():
|
|
20
|
+
out = ['"""Fixture module for the symbol-move codemod test."""', "",
|
|
21
|
+
"from __future__ import annotations", "",
|
|
22
|
+
"import functools", "import os", "", "", "# ---- constants ----", ""]
|
|
23
|
+
for i in range(N_FUNCS):
|
|
24
|
+
out.append("LIMIT_%02d = %d # upper bound for compute_%02d" % (i, 10 + i, i))
|
|
25
|
+
out += ["", '_SEPARATOR = os.sep + "|"', "", ""]
|
|
26
|
+
out += ["# ---- helpers ----", ""]
|
|
27
|
+
for i in range(N_FUNCS):
|
|
28
|
+
out += [
|
|
29
|
+
"",
|
|
30
|
+
"def _scale_%02d(value: int) -> int:" % i,
|
|
31
|
+
" # private helper: scales by the group factor",
|
|
32
|
+
" factor = %d" % (i % 7 + 1),
|
|
33
|
+
" if value < 0:",
|
|
34
|
+
" return -value * factor",
|
|
35
|
+
" return value * factor",
|
|
36
|
+
"",
|
|
37
|
+
"",
|
|
38
|
+
"def compute_%02d(value: int) -> int:" % i,
|
|
39
|
+
' """Scale value and clamp it to LIMIT_%02d."""' % i,
|
|
40
|
+
" scaled = _scale_%02d(value)" % i,
|
|
41
|
+
" if scaled > LIMIT_%02d:" % i,
|
|
42
|
+
" return LIMIT_%02d" % i,
|
|
43
|
+
" total = 0",
|
|
44
|
+
" for step in range(scaled):",
|
|
45
|
+
" total += step % 3",
|
|
46
|
+
" return scaled + total - total",
|
|
47
|
+
"",
|
|
48
|
+
]
|
|
49
|
+
out += [
|
|
50
|
+
"",
|
|
51
|
+
"def join_parts(*parts: str) -> str:",
|
|
52
|
+
" # joins with the module separator",
|
|
53
|
+
" return _SEPARATOR.join(str(p) for p in parts)",
|
|
54
|
+
"",
|
|
55
|
+
"",
|
|
56
|
+
"# ---- async ----",
|
|
57
|
+
"",
|
|
58
|
+
]
|
|
59
|
+
for i in range(N_ASYNC):
|
|
60
|
+
out += [
|
|
61
|
+
"",
|
|
62
|
+
"async def fetch_%02d(value: int) -> dict:" % i,
|
|
63
|
+
' """Pretend to fetch: returns the computed value in a record."""',
|
|
64
|
+
" result = compute_%02d(value)" % i,
|
|
65
|
+
' record = {"id": %d, "value": result}' % i,
|
|
66
|
+
" if result % 2:",
|
|
67
|
+
' record["odd"] = True',
|
|
68
|
+
" return record",
|
|
69
|
+
"",
|
|
70
|
+
]
|
|
71
|
+
out += ["", "# ---- models ----", ""]
|
|
72
|
+
for i in range(N_CLASSES):
|
|
73
|
+
out += [
|
|
74
|
+
"",
|
|
75
|
+
"@functools.total_ordering",
|
|
76
|
+
"class Item_%02d:" % i,
|
|
77
|
+
' """A comparable item backed by compute_%02d."""' % i,
|
|
78
|
+
"",
|
|
79
|
+
' kind = "item-%02d"' % i,
|
|
80
|
+
"",
|
|
81
|
+
" def __init__(self, raw: int) -> None:",
|
|
82
|
+
" self.raw = raw",
|
|
83
|
+
"",
|
|
84
|
+
" @property",
|
|
85
|
+
" def value(self) -> int:",
|
|
86
|
+
" return compute_%02d(self.raw)" % i,
|
|
87
|
+
"",
|
|
88
|
+
" def label(self) -> str:",
|
|
89
|
+
' return join_parts(self.kind, self.value, LIMIT_%02d)' % i,
|
|
90
|
+
"",
|
|
91
|
+
" def __eq__(self, other: object) -> bool:",
|
|
92
|
+
" if not isinstance(other, Item_%02d):" % i,
|
|
93
|
+
" return NotImplemented",
|
|
94
|
+
" return self.value == other.value",
|
|
95
|
+
"",
|
|
96
|
+
" def __lt__(self, other: Item_%02d) -> bool:" % i,
|
|
97
|
+
" return self.value < other.value",
|
|
98
|
+
"",
|
|
99
|
+
" def __hash__(self) -> int:",
|
|
100
|
+
" return hash((self.kind, self.value))",
|
|
101
|
+
"",
|
|
102
|
+
]
|
|
103
|
+
out += [
|
|
104
|
+
"",
|
|
105
|
+
"# ---- kept in place ----",
|
|
106
|
+
"",
|
|
107
|
+
'VERSION = "1.0"',
|
|
108
|
+
"",
|
|
109
|
+
"",
|
|
110
|
+
"def describe() -> str:",
|
|
111
|
+
" return join_parts(VERSION, compute_00(1), Item_01(2).label())",
|
|
112
|
+
"",
|
|
113
|
+
"",
|
|
114
|
+
"DEFAULT_TOTAL = LIMIT_03 * 2",
|
|
115
|
+
]
|
|
116
|
+
return "\n".join(out) + "\n"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
TESTS = '''import asyncio
|
|
120
|
+
import os
|
|
121
|
+
import unittest
|
|
122
|
+
|
|
123
|
+
from pkg.big import (
|
|
124
|
+
DEFAULT_TOTAL,
|
|
125
|
+
LIMIT_05,
|
|
126
|
+
VERSION,
|
|
127
|
+
Item_03,
|
|
128
|
+
_scale_02,
|
|
129
|
+
_SEPARATOR,
|
|
130
|
+
compute_04,
|
|
131
|
+
compute_89,
|
|
132
|
+
describe,
|
|
133
|
+
fetch_08,
|
|
134
|
+
join_parts,
|
|
135
|
+
)
|
|
136
|
+
import pkg.big as big
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class BigTest(unittest.TestCase):
|
|
140
|
+
def test_constant(self):
|
|
141
|
+
self.assertEqual(LIMIT_05, 15)
|
|
142
|
+
|
|
143
|
+
def test_private_helper(self):
|
|
144
|
+
self.assertEqual(_scale_02(-2), 6)
|
|
145
|
+
|
|
146
|
+
def test_compute_clamps(self):
|
|
147
|
+
self.assertEqual(compute_04(100), 14)
|
|
148
|
+
|
|
149
|
+
def test_compute_last(self):
|
|
150
|
+
self.assertEqual(compute_89(1), 6)
|
|
151
|
+
|
|
152
|
+
def test_join(self):
|
|
153
|
+
self.assertEqual(join_parts("a", 1), "a" + os.sep + "|1")
|
|
154
|
+
self.assertEqual(_SEPARATOR, os.sep + "|")
|
|
155
|
+
|
|
156
|
+
def test_async(self):
|
|
157
|
+
self.assertEqual(asyncio.run(fetch_08(1)), {"id": 8, "value": 2})
|
|
158
|
+
|
|
159
|
+
def test_item_order(self):
|
|
160
|
+
self.assertLess(Item_03(1), Item_03(2))
|
|
161
|
+
self.assertEqual(Item_03(1), Item_03(1))
|
|
162
|
+
self.assertGreaterEqual(Item_03(3), Item_03(2))
|
|
163
|
+
|
|
164
|
+
def test_item_label(self):
|
|
165
|
+
self.assertEqual(Item_03(1).label(), join_parts("item-03", 4, 13))
|
|
166
|
+
|
|
167
|
+
def test_kept_names(self):
|
|
168
|
+
self.assertEqual(VERSION, "1.0")
|
|
169
|
+
self.assertEqual(DEFAULT_TOTAL, 26)
|
|
170
|
+
self.assertTrue(describe().startswith("1.0"))
|
|
171
|
+
|
|
172
|
+
def test_module_attributes(self):
|
|
173
|
+
self.assertIs(big.os, os)
|
|
174
|
+
self.assertEqual(big.Item_29(1).value, 2)
|
|
175
|
+
|
|
176
|
+
@unittest.skip("documents a skipped case for the count check")
|
|
177
|
+
def test_skipped(self):
|
|
178
|
+
pass
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
if __name__ == "__main__":
|
|
182
|
+
unittest.main()
|
|
183
|
+
'''
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def main(argv):
|
|
187
|
+
if len(argv) != 1:
|
|
188
|
+
sys.stderr.write("usage: gen_big.py <out_root>\n")
|
|
189
|
+
return 2
|
|
190
|
+
root = argv[0]
|
|
191
|
+
for d in ("pkg", "tests"):
|
|
192
|
+
os.makedirs(os.path.join(root, d), exist_ok=True)
|
|
193
|
+
files = {"pkg/__init__.py": "", "pkg/big.py": big(), "tests/__init__.py": "", "tests/test_big.py": TESTS}
|
|
194
|
+
for rel, text in files.items():
|
|
195
|
+
with open(os.path.join(root, rel), "w", encoding="utf-8", newline="\n") as fh:
|
|
196
|
+
fh.write(text)
|
|
197
|
+
return 0
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
if __name__ == "__main__":
|
|
201
|
+
sys.exit(main(sys.argv[1:]))
|