arm-ka-docs/scripts/test_sync_statements.sh
Andrey Epifancev b5ad8a0f4c feat(scripts): sync-statements.sh --in-place — раскладка копии в рабочее дерево хоста с защитой от .gitignore и путей для контура (#120)
Также: evaluate при коммите VERSION без arm_statements/ даёт код 20 вместо падения с 128 (T2.1).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 19:13:16 +04:00

296 lines
17 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# Тест sync-statements.sh на синтетических репозиториях (bare origin + клон), без tea и сети.
# Разделы: помощник statements_check.py; дальше — сценарии самого скрипта (дописываются по задачам плана).
# Запуск: make test-sync-statements (или напрямую из корня docs; интерпретатор — STATEMENTS_PY, по умолчанию python3).
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
PY=${STATEMENTS_PY:-python3}
WORK=$(mktemp -d)
trap 'rm -rf "$WORK"' EXIT
export GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@t GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@t
if ! "$PY" -c 'import tomllib, yaml' 2>/dev/null; then
echo "нужен Python >= 3.11 с PyYAML: выполните make venv (или задайте STATEMENTS_PY)" >&2
exit 1
fi
fail=0
check() { if [ "$1" = 0 ]; then echo "ok: $2"; else echo "FAIL: $2" >&2; fail=1; fi; }
put() { mkdir -p "$(dirname "$1")"; printf '%s\n' "${2:-x}" > "$1"; }
# shellcheck disable=SC2317
commit_push() { git -C "$1" add -A; git -C "$1" commit -qm "$2"; git -C "$1" push -q origin "HEAD:$3" 2>/dev/null; git -C "$1" fetch -q origin; }
# --- помощник statements_check.py ---------------------------------------------------------------------------------
H="$WORK/h"; mkdir -p "$H"
CHK="$HERE/statements_check.py"
# run <rc-ожидаемый> <описание> <аргументы…>: вывод — в $OUT
run() {
local want=$1 desc=$2 rc=0; shift 2
OUT=$("$PY" "$CHK" "$@" 2>&1) || rc=$?
check "$([ "$rc" = "$want" ] && echo 0 || echo 1)" "$desc (rc=$rc, ожидался $want)"
}
has() { grep -q -- "$1" <<<"$OUT"; }
put "$H/pp.toml" $'[project]\ndependencies = ["PyYAML==6.0.3"]'
put "$H/r1.txt" $'aiohttp==3.14.3\nPyYAML==6.0.3'
run 0 "deps: есть" deps "$H/pp.toml" "$H/r1.txt"
check "$([ -z "$OUT" ] && echo 0 || echo 1)" "deps: есть — вывод пустой"
put "$H/r2.txt" $'aiohttp==3.14.3\npyyaml==6.0.3'
run 0 "deps: регистр" deps "$H/pp.toml" "$H/r2.txt"
put "$H/pp_u.toml" $'[project]\ndependencies = ["Py_YAML==6.0.3"]'
put "$H/r2u.txt" $'py-yaml==6.0.3'
run 0 "deps: подчёркивание и дефис равны" deps "$H/pp_u.toml" "$H/r2u.txt"
put "$H/r3.txt" $'aiohttp==3.14.3\n# PyYAML==6.0.3\n-r other.txt'
run 20 "deps: нет" deps "$H/pp.toml" "$H/r3.txt"
check "$(has PyYAML && echo 0 || echo 1)" "deps: нет — в выводе PyYAML"
put "$H/r4.txt" $'PyYAML==6.0.4'
run 0 "deps: другая версия" deps "$H/pp.toml" "$H/r4.txt"
check "$(has 'примечание:' && has 6.0.4 && echo 0 || echo 1)" "deps: другая версия — примечание с 6.0.4"
put "$H/pp_none.toml" $'[tool.ruff]\nline-length = 120'
run 0 "deps: без [project]" deps "$H/pp_none.toml" "$H/r1.txt"
put "$H/pp_jwt.toml" $'[project]\ndependencies = ["PyJWT==2.8.0"]'
put "$H/r5.txt" $'PyJWT[crypto]==2.8.0'
run 0 "deps: экстры хоста" deps "$H/pp_jwt.toml" "$H/r5.txt"
put "$H/orders.yaml" 'version: 1
repeats: {}
editions:
66n:
BALANCE: {full: ["1110", "1100"], simplified: ["1100"]}
INCOME: {full: ["2110"], simplified: ["2110"]}
fsbu4:
BALANCE: {full: ["1110", "1100"], simplified: ["1100"]}
INCOME: {full: ["2110"], simplified: ["2110"]}'
put "$H/lines.yaml" 'editions:
- id: 66n
forms:
BALANCE:
- {code: "1110", name: "Основные средства"}
- {code: "1100", name: "Итого"}
- id: fsbu4
forms:
BALANCE:
- {code: "1110", name: "Основные средства"}'
run 0 "lines: все коды есть" lines "$H/lines.yaml" "$H/orders.yaml"
put "$H/lines_x.yaml" 'editions:
- id: 66n
forms:
BALANCE:
- {code: "1110", name: "а"}
- id: fsbu4
forms:
BALANCE:
- {code: "1110", name: "а"}
- {code: "1120", name: "б"}'
run 20 "lines: код вне перечня" lines "$H/lines_x.yaml" "$H/orders.yaml"
check "$(has 1120 && has fsbu4 && echo 0 || echo 1)" "lines: код вне перечня — в выводе 1120 и fsbu4"
put "$H/orders_s.yaml" 'version: 1
editions:
fsbu4:
BALANCE: {full: ["1110"], simplified: ["1100"]}'
put "$H/lines_s.yaml" 'editions:
- id: fsbu4
forms:
BALANCE:
- {code: "1100", name: "а"}'
run 0 "lines: код только в simplified" lines "$H/lines_s.yaml" "$H/orders_s.yaml"
put "$H/bad.yaml" 'editions: ['
run 1 "битый YAML" lines "$H/bad.yaml" "$H/orders.yaml"
run 2 "аргументы"
# --- sync-statements.sh --check -----------------------------------------------------------------------------------
SYNC="$HERE/sync-statements.sh"
export STATEMENTS_PY="$PY"
SRC="$WORK/arm-statements"
git init -q --bare "$WORK/arm-statements.git"
git clone -q "$WORK/arm-statements.git" "$SRC" 2>/dev/null
git -C "$SRC" checkout -q -b main
put "$SRC/pyproject.toml" $'[project]\nname = "arm-statements"\ndependencies = ["PyYAML==6.0.3"]'
put "$SRC/README.md" "пакет"
put "$SRC/arm_statements/__init__.py" "# init"
put "$SRC/arm_statements/steps.py" "def step(): return 1"
mkdir -p "$SRC/arm_statements/rules"; cp "$H/orders.yaml" "$SRC/arm_statements/rules/statement_code_orders.yaml"
put "$SRC/arm_statements/tests/__init__.py" "# t"
put "$SRC/arm_statements/tests/test_steps.py" "def test_step(): pass"
commit_push "$SRC" "S1" main
S1=$(git -C "$SRC" rev-parse HEAD)
mkdir -p "$WORK/docs/contracts" "$WORK/docs-empty" "$WORK/wt"
cp "$H/orders.yaml" "$WORK/docs/contracts/statement-code-orders.yaml"
# make_host <каталог> <имя origin>: bare + клон с копией S1 в api/arm_statements, requirements и (для arm-core) словарём
make_host() {
local dir=$1 name=$2
git init -q --bare "$WORK/$name.git"
git clone -q "$WORK/$name.git" "$dir" 2>/dev/null
git -C "$dir" checkout -q -b main
put "$dir/api/requirements.txt" $'aiohttp==3.14.3\nPyYAML==6.0.3'
mkdir -p "$dir/api"
git -C "$SRC" archive "$S1" arm_statements | tar -x -C "$dir/api"
printf '%s\n' "$S1" > "$dir/api/arm_statements/VERSION"
}
make_host "$WORK/arm-core" arm-core
mkdir -p "$WORK/arm-core/api/rules"; cp "$H/lines.yaml" "$WORK/arm-core/api/rules/statement_lines.yaml"
commit_push "$WORK/arm-core" "copy S1" main
make_host "$WORK/tabular-parser" arm-tabular-parser
commit_push "$WORK/tabular-parser" "copy S1" main
make_host "$WORK/scan-gateway" arm-scan-gateway
mkdir -p "$WORK/scan-gateway/api/rules"
put "$WORK/scan-gateway/api/rules/statement_lines.yaml" $'editions:\n - id: fsbu4\n forms:\n BALANCE:\n - {code: "9999", name: "х"}'
commit_push "$WORK/scan-gateway" "copy S1" main
COMMON=(--source "$WORK/arm-statements" --docs "$WORK/docs" --wt-root "$WORK/wt" --core-dir "$WORK/arm-core")
AC="$WORK/arm-core/api"
reset_hosts() { local h; for h in arm-core tabular-parser scan-gateway; do git -C "$WORK/$h" checkout -q -- . && git -C "$WORK/$h" clean -fdq; done; }
# sync <rc> <описание> <аргументы…>: вывод — в $OUT
sync() {
local want=$1 desc=$2 rc=0; shift 2
OUT=$("$SYNC" "${COMMON[@]}" "$@" 2>&1) || rc=$?
check "$([ "$rc" = "$want" ] && echo 0 || echo 1)" "$desc (rc=$rc, ожидался $want)"
}
sync 0 "check: синхронно" --check --host-dir "$WORK/arm-core"
check "$(has 'statements-check arm-core: 0' && echo 0 || echo 1)" "check: синхронно — итоговая строка"
sync 0 "check: имя по origin, словарь не у scan-gateway" --check --host-dir "$WORK/scan-gateway"
check "$(has 'scan-gateway' && ! has 9999 && echo 0 || echo 1)" "check: scan-gateway — в выводе имя, нет 9999"
OUT=$(cd "$WORK/tabular-parser" && "$SYNC" --check --host-dir . "${COMMON[@]}" 2>&1) && rc=0 || rc=$?
check "$([ "$rc" = 0 ] && has 'statements-check tabular-parser: 0' && echo 0 || echo 1)" "check: --host-dir . из каталога хоста (rc=$rc)"
sync 0 "check: --host вместо origin" --check --host scan-gateway --host-dir "$WORK/scan-gateway"
sync 2 "check: неизвестное имя --host" --check --host foo --host-dir "$WORK/arm-core"
TP="$WORK/tabular-parser/api/arm_statements"
mkdir -p "$TP/__pycache__" "$TP/tests/.pytest_cache" "$TP/.mypy_cache"
: > "$TP/__pycache__/steps.cpython-311.pyc"; : > "$TP/tests/.pytest_cache/x"; : > "$TP/.mypy_cache/x"; : > "$TP/.DS_Store"
sync 0 "check: кэши в копии игнорируются" --check --host-dir "$WORK/tabular-parser"
reset_hosts
echo "# правка" >> "$AC/arm_statements/steps.py"
sync 20 "check: изменён файл" --check --host-dir "$WORK/arm-core"
check "$(has steps.py && has 'прямая правка' && echo 0 || echo 1)" "check: изменён файл — steps.py и «прямая правка»"
reset_hosts
put "$AC/arm_statements/extra.py" "x = 1"
sync 20 "check: добавлен файл" --check --host-dir "$WORK/arm-core"
check "$(has extra.py && echo 0 || echo 1)" "check: добавлен файл — extra.py"
reset_hosts
rm "$AC/arm_statements/tests/test_steps.py"
sync 20 "check: удалён файл" --check --host-dir "$WORK/arm-core"
check "$(has test_steps.py && echo 0 || echo 1)" "check: удалён файл — test_steps.py"
reset_hosts
rm "$AC/arm_statements/VERSION"
sync 20 "check: нет VERSION" --check --host-dir "$WORK/arm-core"
reset_hosts
printf '%s\n' 0000000000000000000000000000000000000000 > "$AC/arm_statements/VERSION"
sync 20 "check: неизвестный VERSION" --check --host-dir "$WORK/arm-core"
reset_hosts
rm -rf "$AC/arm_statements"
sync 20 "check: нет копии" --check --host-dir "$WORK/arm-core"
check "$(has 'копии нет' && echo 0 || echo 1)" "check: нет копии — сообщение"
reset_hosts
git -C "$SRC" checkout -q -b feat/x
echo "# F" >> "$SRC/arm_statements/steps.py"
commit_push "$SRC" "F" feat/x
F=$(git -C "$SRC" rev-parse HEAD)
git -C "$SRC" checkout -q main
git -C "$SRC" archive "$F" arm_statements | tar -x -C "$AC"
printf '%s\n' "$F" > "$AC/arm_statements/VERSION"
sync 20 "check: чужая версия" --check --host-dir "$WORK/arm-core"
check "$(has 'чужая версия' && echo 0 || echo 1)" "check: чужая версия — сообщение"
reset_hosts
put "$AC/requirements.txt" "aiohttp==3.14.3"
sync 20 "check: нет зависимости" --check --host-dir "$WORK/arm-core"
check "$(has PyYAML && echo 0 || echo 1)" "check: нет зависимости — PyYAML"
reset_hosts
cp "$H/lines_x.yaml" "$AC/rules/statement_lines.yaml"
sync 20 "check: код словаря вне перечня" --check --host-dir "$WORK/arm-core"
check "$(has 1120 && has fsbu4 && echo 0 || echo 1)" "check: код вне перечня — 1120 и fsbu4"
reset_hosts
echo "# новый код" >> "$WORK/docs/contracts/statement-code-orders.yaml"
sync 10 "check: перечень в docs новее" --check --host-dir "$WORK/arm-core"
check "$(has contracts/statement-code-orders.yaml && has 'statements-check arm-core: 10' && echo 0 || echo 1)" "check: перечень в docs — в выводе файл и итог 10"
cp "$H/orders.yaml" "$WORK/docs/contracts/statement-code-orders.yaml"
sync 10 "check: перечня нет в docs" --check --host-dir "$WORK/arm-core" --docs "$WORK/docs-empty"
sync 2 "check: нет источника" --check --host-dir "$WORK/arm-core" --source "$WORK/nope"
sync 2 "check: нет хоста" --check --host-dir "$WORK/nope"
sync 2 "check: неизвестный аргумент" --check --bogus
sync 2 "check: нет --host/--host-dir" --check
sync 2 "check: --ref не разрешается" --check --host-dir "$WORK/arm-core" --ref origin/nope
sync 2 "--in-place вместе с --dry-run" --in-place --dry-run --host-dir "$WORK/arm-core"
sync 0 "check важнее --dry-run и --in-place" --in-place --dry-run --check --host-dir "$WORK/arm-core"
# проверки с S2 — последними
put "$SRC/README.md" "пакет, правка"
commit_push "$SRC" "S2 правка README" main
sync 10 "check: отстала" --check --host-dir "$WORK/arm-core"
check "$(has 'копия отстала' && has 'S2 правка README' && echo 0 || echo 1)" "check: отстала — сообщение и тема S2"
echo "# правка" >> "$AC/arm_statements/steps.py"
sync 20 "check: правка + отставание = 20" --check --host-dir "$WORK/arm-core"
reset_hosts
# коммит без arm_statements/ (T2.1): код 20 с итоговой строкой, а не 128
NOARCH=$(git -C "$SRC" commit-tree "$(git -C "$SRC" hash-object -t tree /dev/null)" -m "без пакета")
printf '%s\n' "$NOARCH" > "$AC/arm_statements/VERSION"
sync 20 "check: в коммите VERSION нет arm_statements/" --check --host-dir "$WORK/arm-core"
check "$(has 'нет arm_statements/' && has 'statements-check arm-core: 20' && echo 0 || echo 1)" "check: нет arm_statements/ — сообщение и итог"
reset_hosts
# --- sync-statements.sh --in-place --------------------------------------------------------------------------------
S2=$(git -C "$SRC" rev-parse origin/main)
FR="$WORK/fresh/tabular-parser"
mkdir -p "$WORK/fresh"
git init -q --bare "$WORK/fresh/arm-tabular-parser.git"
git clone -q "$WORK/fresh/arm-tabular-parser.git" "$FR" 2>/dev/null
git -C "$FR" checkout -q -b main
put "$FR/api/requirements.txt" $'PyYAML==6.0.3'
commit_push "$FR" "fresh" main
sync 0 "in-place: первичная раскладка" --in-place --host-dir "$FR" --ref origin/main
check "$([ "$(tr -d '\n' < "$FR/api/arm_statements/VERSION")" = "$S2" ] && echo 0 || echo 1)" "in-place: VERSION = полный хэш S2"
rm -rf "$WORK/arc"; mkdir -p "$WORK/arc"; git -C "$SRC" archive "$S2" arm_statements | tar -x -C "$WORK/arc"
check "$(diff -r -x VERSION "$WORK/arc/arm_statements" "$FR/api/arm_statements" >/dev/null && echo 0 || echo 1)" "in-place: копия равна git archive S2"
check "$(git -C "$FR" status --porcelain | grep -qv '^?? api/arm_statements' && echo 1 || echo 0)" "in-place: хост менялся только в api/arm_statements"
check "$(has закоммитьте && echo 0 || echo 1)" "in-place: подсказка про коммит"
sync 0 "in-place: повтор" --in-place --host-dir "$FR" --ref origin/main
check "$(has 'раскладывать нечего' && echo 0 || echo 1)" "in-place: повтор — раскладывать нечего"
sync 0 "in-place: обновление arm-core S1 -> S2" --in-place --host-dir "$WORK/arm-core"
check "$([ "$(tr -d '\n' < "$AC/arm_statements/VERSION")" = "$S2" ] && echo 0 || echo 1)" "in-place: arm-core VERSION = S2"
echo "# правка" >> "$AC/arm_statements/steps.py"
sync 20 "in-place: отказ при правке" --in-place --host-dir "$WORK/arm-core" --ref origin/main
check "$(grep -q '^# правка' "$AC/arm_statements/steps.py" && [ "$(tr -d '\n' < "$AC/arm_statements/VERSION")" = "$S2" ] && echo 0 || echo 1)" "in-place: правка на месте, VERSION прежний"
reset_hosts
TPR="$WORK/tabular-parser/api"
put "$TPR/requirements.txt" "aiohttp==3.14.3"
sync 20 "in-place: отказ без зависимости (обновление)" --in-place --host-dir "$WORK/tabular-parser"
check "$([ "$(tr -d '\n' < "$TPR/arm_statements/VERSION")" = "$S1" ] && has PyYAML && echo 0 || echo 1)" "in-place: копия не тронута, в выводе PyYAML"
reset_hosts
cp "$FR/api/requirements.txt" "$WORK/req.bak"
put "$FR/api/requirements.txt" "aiohttp==3.14.3"
sync 20 "in-place: отказ без зависимости (fresh)" --in-place --host-dir "$FR" --ref origin/main
cp "$WORK/req.bak" "$FR/api/requirements.txt"
cp "$H/lines_x.yaml" "$AC/rules/statement_lines.yaml"
sync 20 "in-place: код словаря вне перечня" --in-place --host-dir "$WORK/arm-core" --ref origin/main
check "$([ "$(tr -d '\n' < "$AC/arm_statements/VERSION")" = "$S1" ] && has 1120 && echo 0 || echo 1)" "in-place: словарь — копия не тронута, 1120 в выводе"
reset_hosts
# защиты: коммиты S3/S4 на ветке test-guards, чтобы origin/main (S2) остался прежним
git -C "$SRC" checkout -q -b test-guards
put "$SRC/arm_statements/tests/golden/case.json" '{}'
commit_push "$SRC" "S3 json" test-guards
put "$FR/.gitignore" '*.json'
sync 1 "in-place: .gitignore прячет файл" --in-place --host-dir "$FR" --ref origin/test-guards
check "$(has case.json && has .gitignore && echo 0 || echo 1)" "in-place: в выводе case.json и .gitignore"
rm "$FR/.gitignore"
echo "# /Users/someone" >> "$SRC/arm_statements/steps.py"
commit_push "$SRC" "S4 путь" test-guards
sync 1 "in-place: содержимое для контура" --in-place --host-dir "$FR" --ref origin/test-guards
check "$(has '/Users/' && echo 0 || echo 1)" "in-place: в выводе /Users/"
git -C "$SRC" checkout -q main
[ "$fail" = 0 ] || exit 1
echo "ok: sync-statements.sh — все сценарии пройдены"