#!/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 <описание> <аргументы…>: вывод — в $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 <описание> <аргументы…>: вывод — в $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 check "$([ "$(tr -d '\n' < "$FR/api/arm_statements/VERSION")" = "$S2" ] && diff -r -x VERSION "$WORK/arc/arm_statements" "$FR/api/arm_statements" >/dev/null && echo 0 || echo 1)" "in-place: fresh — api/arm_statements не тронут" 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" sync 1 "in-place: повтор после защиты .gitignore остаётся 1" --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/" sync 1 "in-place: повтор после защиты путей остаётся 1" --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 — все сценарии пройдены"