Files
gis/test/acceptance/README.md
Bakhtiyar Issakhmetov ca87d6961b test(acceptance): reproducible Stage 8 acceptance package
Add test/acceptance/ so the customer's reviewers can re-run what the
Stage 8 documents claim on an isolated stand built from this branch:

- compose.acceptance.yml + stand.sh: compose project gis-acceptance
  (app 18080, postgres 15432, minio 19000/19001, rabbitmq mgmt 15672),
  POSTGIS_PLATFORM / MINIO_IMAGE knobs for arm64 and amd64 hosts.
- functional.sh: the 89 test-stand checks of Приложение 7 section 5
  (F01-F38, N01-N38, L02-L05, L07-L15), own prefixed test data,
  PostGIS/MinIO confirmation via the stand containers, full cleanup.
- lp_gen.py / lp_run.sh / lp_verify.py: synthetic ЛП inputs, a run from
  stage 0, and an independent numpy re-implementation of every node that
  compares all 15 outputs pixel by pixel; read-only (GET) mode works
  against any stand, in row/column windows for 22219x14977 rasters.
- load.sh + seed.sh: scenarios S1-S6 of Приложение 3 with hey 0.1.5
  running inside the stand network; CSV/JSON summaries, criteria K1-K4.
- scan.sh: govulncheck (source and binary), gosec, trivy fs/image,
  golangci-lint; fails on any High/Critical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 22:37:28 +05:00

26 KiB
Raw Blame History

Пакет приёмочных испытаний (Этап 8)

Пакет позволяет Заказчику самостоятельно воспроизвести то, что заявлено в документах Этапа 8, на изолированном испытательном стенде, развёрнутом из исходного кода этой ветки:

№ Что проверяется Скрипт Документ
1 89 функциональных проверок испытательного стенда (F01–F38, N01–N38, L02–L05, L07–L15) functional.sh Приложение 7 «Протокол функционального тестирования», раздел 5
2 Попиксельная сверка расчёта Ландшафтной программы (ЛП) с независимой эталонной реализацией lp_gen.py, lp_run.sh, lp_verify.py ЛП техописание (правила пикселя, граф узлов); Приложение 7, L07/L08/L12
3 Нагрузочные испытания: 1000 одновременных клиентов, сценарии S1–S6 load.sh (+ seed.sh) Приложение 3 «Протокол нагрузочного тестирования»
4 Повторный анализ безопасности: 0 уязвимостей High/Critical scan.sh Приложение 4 «Отчет об устранении уязвимостей»

Все скрипты — bash (macOS и Linux, set -euo pipefail, совместимы с bash 3.2), curl, jq и Python 3. Результаты прогонов сохраняются в test/acceptance/results/ (каталог не версионируется).

Внимание. functional.sh, lp_run.sh, seed.sh и сценарий S5 из load.sh создают данные (в том числе заполняют слоты ЛП). Их следует запускать только на изолированном стенде gis-acceptance. На рабочем стенде https://dssgis.dwh.kz допускается только lp_verify.py — он выполняет исключительно GET-запросы (lp_run.sh и seed.sh отказываются работать с этим адресом).

Состав пакета

Файл Назначение
compose.acceptance.yml Надстройка над deployments/docker-compose.yml для изолированного стенда (проект gis-acceptance)
stand.sh Развёртывание (up: сборка, миграции, справочник районов, ожидание /readyz) и удаление (down) стенда
functional.sh 89 функциональных проверок, построчный вывод ID | описание | PASS/FAIL | detail
lp_gen.py Генерация 13 синтетических входных растров ЛП и одного невыровненного растра
lp_run.sh Загрузка входов в 13 слотов → прогон {"from_stage":0} → lp_verify.py
lp_verify.py Независимая (numpy) реализация всех узлов ЛП и попиксельная сверка 15 выходов; только GET
load.sh Сценарии нагрузки S1–S6 (hey 0.1.5), сводная таблица, CSV/JSON
seed.sh Наполнение стенда данными для нагрузочных испытаний (seed.sh clean — удаление)
scan.sh govulncheck (исходный код и бинарный файл), gosec, trivy fs, trivy image, golangci-lint
data/ Тестовые векторные данные (участки, население и поголовье КРС по 14 районам СКО с реальными кодами КАТО), SLD-стили
lib/ Вспомогательные скрипты: генерация NDVI, анализ вывода hey, Dockerfile генератора нагрузки, пересоздание data/
requirements.txt Python-зависимости (numpy, rasterio)

Требования

  • Docker Engine с Docker Compose v2.24+ (используется тег !override для портов).
  • curl, jq, Python 3.10+ с numpy и rasterio:
    python3 -m venv .venv && .venv/bin/pip install -r test/acceptance/requirements.txt
    export PYTHON=$PWD/.venv/bin/python   # скрипты используют $PYTHON (по умолчанию python3)
    
  • Для scan.sh: Go (версия toolchain берётся из go.mod), govulncheck, gosec, trivy; golangci-lint — по желанию.

0. Испытательный стенд

git clone https://dssgit.dwh.kz/infureal/gis.git && cd gis && git checkout stage8/security-fixes
test/acceptance/stand.sh up      # сборка образа, запуск, migrate up, import-districts, ожидание /readyz

stand.sh up выполняет те же шаги, что и вручную:

C="docker compose -p gis-acceptance -f deployments/docker-compose.yml -f test/acceptance/compose.acceptance.yml"
$C up -d --build --wait postgres minio rabbitmq
$C run --rm --no-deps app migrate up                                  # 11 миграций 00001…00011
$C run --rm --no-deps app import-districts /stubs/districts.geojson   # 14 районов СКО
$C up -d app worker
curl http://localhost:18080/readyz   # {"components":{"postgres":"ok","rabbitmq":"ok","s3":"ok"},"ready":true}

Порты стенда: API — 18080, PostgreSQL — 15432, MinIO — 19000 (API) / 19001 (консоль), RabbitMQ — 15672 (только веб-интерфейс управления). Сеть контейнеров — gis-acceptance_default. Учётные данные — значения по умолчанию из deployments/docker-compose.yml (стенд изолированный).

Переменные окружения стенда:

Переменная По умолчанию Назначение
POSTGIS_PLATFORM linux/amd64 Образ postgis/postgis:17-3.5 публикуется только для amd64. На Apple Silicon (arm64) он запускается в эмуляции, на amd64-хостах — нативно, поэтому значение по умолчанию работает на обеих платформах
MINIO_IMAGE minio/minio:latest Если образ minio/minio недоступен в реестре, подойдёт любой образ с сервером minio и клиентом mc, например MINIO_IMAGE=cgr.dev/chainguard/minio:latest
APP_IMAGE gis-acceptance-app:local Тег локально собранного образа приложения
HTTP_MAX_UPLOAD_BYTES 2147483648 Предельный размер тела запроса (2 ГиБ)
LP_MAX_WORKERS / LP_MAX_NODES 8 / 2 Параллелизм расчёта ЛП

Удаление стенда вместе с томами: test/acceptance/stand.sh down (= docker compose -p gis-acceptance down -v).

1. Функциональные проверки — functional.sh

test/acceptance/functional.sh                          # стенд http://localhost:18080
BASE_URL=http://host:18080 LOCAL_CHECKS=0 test/acceptance/functional.sh   # удалённый испытательный стенд
Переменная По умолчанию Назначение
BASE_URL http://localhost:18080 Адрес API
LOCAL_CHECKS auto 1 — подтверждение в PostGIS и MinIO через контейнеры проекта (docker exec … psql, mc в контейнере MinIO): F35 (часть MinIO), F36, F37, F38, ресурсы worker в L15. 0 — эти проверки пропускаются (SKIP). auto — 1, если запущены контейнеры проекта gis-acceptance
COMPOSE_PROJECT gis-acceptance Проект Compose для LOCAL_CHECKS
KEEP_DATA 0 1 — не удалять созданные данные
WAIT_TIMEOUT 300 Ожидание асинхронной обработки, с

Скрипт создаёт собственные тестовые данные (все коды имеют уникальный префикс acc<время>-), никогда не изменяет и не удаляет существовавшие объекты и в конце удаляет всё созданное: наборы данных (в том числе входы и выходы ЛП своих прогонов), группы, категории. Записи прогонов ЛП остаются — API не предусматривает их удаления. Порядок и смысл проверок совпадают с таблицами раздела 5 Приложения 7.

Ожидаемый вывод (фрагмент):

F01  | Создание корневой категории POST /categories | PASS | 201; acc1792084680-ecology «Экология», parent_id null
...
N38  | Загрузка 100 МБ мусора с сигнатурой TIFF | PASS | 201 (104 857 604 байт за 0.5 с) → status failed, parse_error от gdal_translate; /readyz 200, сервис доступен
...
L07  | Попиксельная сверка 15 выходов с эталонной реализацией | PASS | 15 выходов × 6 000 000 пикселей; «TOTAL mismatching pixels: 0 -> PASS» …
...
ИТОГО: 89 проверок — PASS 89, FAIL 0, SKIP 0
RESULT: 89/89 PASS

Код возврата ненулевой при любом FAIL. Журнал и вспомогательные выводы (L07_lp_verify.txt, L08_points.txt, L12_lp_verify.txt, F36.txt) — в results/functional-<время>/.

Соответствие Приложению 7 и уточнения методики

Проверки Раздел Приложения 7 Как проверяется в пакете
F01–F09 5.2 Категории HTTP-код и поля ответа, контроль цикла, дубликат, удаление
F10–F34, N01–N13 5.3 Импорт/экспорт, SLD, группы, фильтры Загрузка data/plots_nko.geojson (vector), data/population_kato.geojson и data/livestock_kato.gpkg (vector_with_kato), растра NDVI 600×400 EPSG:4326 Float32 (генерируется lib/helpers.py)
N14–N38, F35 5.4 Негативные проверки 12 некорректных загрузок, 10 эндпоинтов с несуществующим id, 4 — с некорректным id, некорректные тела JSON, параметры ЛП, 100 МБ мусора
F36–F38 5.5 PostGIS и MinIO SQL ST_GeometryType/ST_SRID/ST_IsValid, соединение наблюдений с districts, mc stat объектов
L02–L15 5.6 Ландшафтная программа 13 растров lp_gen.py, прогоны, lp_verify.py, инкрементальный пересчёт, from_stage, параметры, невыровненная сетка

Уточнения относительно протокола (смысл и ожидаемые результаты проверок не изменены):

  • Коды объектов имеют префикс прогона (acc…-ecology вместо ecology), чтобы повторный запуск не конфликтовал с данными прошлых прогонов.
  • F33, F34 — выборки ограничены собственной категорией (category_id), чтобы результат не зависел от других данных стенда; во втором фильтре F34 (file_type + status) добавлен category_id.
  • F12 — охват проверяется по координатам выгруженного GeoJSON (вместо ogrinfo); F22, L05 — формат COG проверяется через rasterio/GDAL (LAYOUT=COG, сжатие, размер, EPSG, тип, NoData) вместо gdalinfo; L08 — 9 точек проверяются lp_verify.py --points вместо gdallocationinfo.
  • F38 — проверяется наличие всех 7 объектов, созданных скриптом (4 исходных файла, COG, SLD набора, SLD группы), а не общее число объектов в бакете.
  • F16, F17 — эталонные значения берутся из data/population_kato.geojson (значения, приведённые в протоколе, в файле сохранены).
  • L05 — перед чтением /landscape/results скрипт дожидается статуса ready всех 15 выходов: конвертация выходов в COG выполняется worker'ом асинхронно после завершения прогона, а /landscape/results возвращает только готовые слои.
  • N38 и ограничение размера тела запроса. В этой ветке (исправления по Приложению 4) тело запроса ограничено HTTP_MAX_UPLOAD_BYTES (по умолчанию 2 ГиБ); загрузка сверх предела отклоняется с 413 request body exceeds the … byte limit. Файл N38 (100 МБ) меньше предела, поэтому ожидание N38 не изменилось: 201, затем status = failed. Проверить 413 можно вручную, запустив стенд с HTTP_MAX_UPLOAD_BYTES=1048576 и загрузив файл больше 1 МиБ.
  • /readyz в этой ветке не раскрывает текст ошибок зависимостей: при сбое компонент помечается "unavailable" (ранее — "error: <текст>"). На исправном стенде ответ не изменился: 200 {"components":{"postgres":"ok","rabbitmq":"ok","s3":"ok"},"ready":true}; N38 проверяет 200 и ready = true.
  • L15 — PASS определяется успешным полным прогоном (202, succeeded, 9 узлов succeeded); время CPU и memory.peak worker (cgroup v2) выводятся справочно и включают COG-конвертацию выходов.

2. Ландшафтная программа: попиксельная сверка

Входные данные — lp_gen.py

$PYTHON test/acceptance/lp_gen.py --out /tmp/lp-in [--width 3000 --height 2000]

13 растров <код слота>.tif: Int16, EPSG:32642, пиксель 100 м, 3000×2000, классы 1…5 из гладких тригонометрических полей (у каждого слота своё), рамка NoData −9999 шириной 40 пикселей. Особые случаи для правил пикселя: в lp-u-crop — блок NoData 200×200 (U считается по water, soil и veg), в lp-u-veg — блок NoData 100×100, наполовину перекрывающий его (там нет ни crop, ни veg → U = NoData), в lp-p-snow — блоки со значениями 0 и 6 (не NoData, но вне 1…5 → недопустимы). Плюс misaligned-lp-u-veg.tif (1500×1000, 200 м) для проверки L13. Генерация детерминирована.

Прогон на стенде — lp_run.sh, режим (a)

test/acceptance/lp_run.sh          # генерация → 13 слотов → {"from_stage":0} → lp_verify.py

Входы и выходы остаются на стенде, прогон можно перепроверить: $PYTHON test/acceptance/lp_verify.py --base-url http://localhost:18080 --run <run-id>.

Проверка любого стенда (только чтение) — lp_verify.py, режим (b)

$PYTHON test/acceptance/lp_verify.py --base-url https://dssgis.dwh.kz --run <run-id> [--cache DIR]

Скрипт выполняет только GET-запросы:

  1. GET /landscape/runs/{id} — параметры прогона, для каждого узла inputs и outputs (группа → набор данных);
  2. GET /datasets/{id}.cog — 13 входов (наборы слотов, которые прогон фактически использовал) и 15 выходов (--inputs-from download читает входы через /datasets/{id}/download — исходный файл, который читал узел).

Ожидаемые значения рассчитываются по всей цепочке только из 13 входов (E — из пересчитанных Z и U, C и V — из пересчитанного E), поэтому ошибка в промежуточном слое не может скрыть ошибку в последующем. Дополнительно проверяется согласованность цепочки самого прогона (узел 10 читал выход узла 00 этого прогона и т. д.) и совпадение сетки всех 28 растров. Растры обрабатываются окнами 512×4096 пикселей (выровнены по тайлам COG 512), поэтому расход памяти ограничен (~0,5 ГБ) и для растров рабочего стенда 22219×14977.

Правила пикселя (источник истины — internal/landscape/kernels.go, они же — в ЛП техописании): значение допустимо при 1 ≤ v ≤ 5; classify в float32 (≤1.8→1, ≤2.6→2, ≤3.4→3, ≤4.2→4, ≤5.0→5); z_integrated — среднее четырёх Z с весом 0,25; u_integrated — water ∧ soil ∧ (crop ∨ veg), веса 0,25 только у допустимых crop/veg; z_max/u_max — максимум; E = max(Z, U); C_j = min(P_j, E); p_max/c_max — максимум по допустимым; func_zoning V1–V3 с порогами v1_min_s_e12, v1_min_s_e3, v2_min_s_e3, v2_min_s_e4 и tie_priority из параметров прогона.

Вывод: по каждому из 15 выходов — число пикселей, допустимых (по эталону) и число несовпадающих пикселей (с координатами первого расхождения), итоговая строка TOTAL mismatching pixels: N -> PASS/FAIL. Код возврата: 0 — полное совпадение, 1 — есть расхождения, 2 — ошибка данных (прогон не succeeded, не найден вход/выход, разные сетки).

3. Нагрузочные испытания — load.sh

test/acceptance/load.sh                               # S1–S6, генератор внутри сети стенда
SCENARIOS="S1 S3 S6" test/acceptance/load.sh          # часть сценариев
GEN=host TARGET_URL=http://localhost:18080 test/acceptance/load.sh   # hey на хосте, через порт стенда
NETWORK=bridge TARGET_URL=https://host BASE_URL=https://host SCENARIOS="S1 S3" test/acceptance/load.sh

Генератор — hey 0.1.5, собирается из исходного кода в образ gis-acceptance-hey:0.1.5 (lib/hey.Dockerfile, на тех же закреплённых образах Go/Alpine, что и приложение) и по умолчанию запускается внутри сети стенда (docker run --network gis-acceptance_default …, цель http://app:8080). Перед нагрузкой seed.sh (идемпотентно) наполняет стенд: 25 vector, 25 vector_with_kato (с маппингом КАТО, 14 районов × 3 года) и 50 raster в категории acc-load; если прогонов ЛП ещё нет, выполняется lp_run.sh. На удалённом стенде без ALLOW_WRITES=1 данные не создаются: id для эндпоинтов определяются GET-запросами, сценарий S5 пропускается.

Эндпоинты (табл. 5 Приложения 3): /datasets?page_size=20, /datasets/{id}, /datasets/{id}.geojson, /datasets/{id}/observations, /categories, /dataset-groups/{id}/latest, /landscape/slots, /landscape/results, /landscape/runs/{id}, /readyz.

Сценарий Параметры hey
S1 -n 100 -c 1 последовательно по 10 эндпоинтам (1000 запросов)
S2 1000 запросов, 100 клиентов: 10 параллельных процессов -n 100 -c 10
S3.1 -n 1000 -c 1000 /datasets?page_size=20, 3 прогона
S3.2 -n 1000 -c 1000 /landscape/results, 3 прогона
S4 -z 60s -q 1, 1000 клиентов в 8 процессах: 300 /datasets?page_size=20, 150 /datasets/{id}, по 100 .geojson, /observations, /categories, /latest, /landscape/results, 50 /readyz
S5 -z 30s -c 10 × 10 эндпоинтов + 20 параллельных POST /datasets (10 GeoJSON + 10 GeoTIFF), все загрузки должны дойти до ready (затем удаляются)
S6 Повтор S1 и GET /readyz после нагрузки

S3.3 протокола (1000 одновременных запросов через опубликованный порт) воспроизводится запуском GEN=host TARGET_URL=http://localhost:18080 SCENARIOS=S3.

Вывод: сводная таблица (запросы, клиенты, длительность, RPS, доля успешных ответов, min/avg/P50/P90/P95/P99/max в мс, коды ответов, транспортные ошибки, итог по критериям), таблицы по эндпоинтам/прогонам для S2–S5, результаты загрузок S5, пиковое потребление CPU/памяти контейнерами (docker stats во время S4/S5) и критерий K4. Как и в протоколе, перцентили сценариев из нескольких процессов/прогонов — средневзвешенные по числу ответов. Файлы: results/load-<время>/summary.csv, summary.json, detail.csv, raw/*.txt (исходные выводы hey). Критерии (табл. 8 Приложения 3): K1 — успешных ответов ≥ 99 %, K2 — ответов 5xx нет, K3 — P95 ≤ 2000 мс, K4 — после нагрузки /readyz 200 и P50 S6 не хуже P50 S1 (допуск ×2 + 1 мс). Код возврата 1 при невыполнении любого критерия.

Генератор и стенд работают на одной машине и делят CPU, поэтому абсолютные значения RPS зависят от оборудования (протокол фиксирует их как нижнюю оценку).

4. Анализ безопасности — scan.sh

test/acceptance/scan.sh
Инструмент Что проверяет Что считается High/Critical
govulncheck ./... Уязвимости Go-модулей и стандартной библиотеки, достижимые из кода Каждая уязвимость с цепочкой вызовов из кода (у govulncheck нет шкалы severity)
govulncheck -mode=binary Бинарный файл /usr/local/bin/gis из собранного образа Каждая уязвимость с затронутыми символами в бинарнике
gosec ./... Статический анализ кода Severity HIGH
trivy fs Зависимости (go.mod), секреты, конфигурации (Dockerfile, compose) HIGH, CRITICAL
trivy image Образ приложения (Alpine + GDAL + бинарник), собирается из build/package/Dockerfile HIGH, CRITICAL
golangci-lint Качество кода (если установлен) Справочно: число замечаний (шкалы severity нет)

Скрипт печатает таблицу «инструмент | цель | High | Critical | примечания» и итог RESULT: 0 High / 0 Critical -> PASS; код возврата 1 при наличии High/Critical. Полные отчёты (JSON) — results/scan-<время>/. Без Docker: SKIP_IMAGE=1 (бинарник собирается локально с cgo и GDAL, trivy image пропускается).

Порядок полного воспроизведения

test/acceptance/stand.sh up
test/acceptance/functional.sh            | tee functional.txt   # RESULT: 89/89 PASS
test/acceptance/lp_run.sh                | tee lp_local.txt     # TOTAL mismatching pixels: 0 -> PASS
$PYTHON test/acceptance/lp_verify.py --base-url https://dssgis.dwh.kz --run <run-id> | tee lp_prod.txt
test/acceptance/load.sh                  | tee load.txt         # критерии K1–K4 выполнены
test/acceptance/scan.sh                  | tee scan.txt         # RESULT: 0 High / 0 Critical -> PASS
test/acceptance/stand.sh down

Тестовые векторные данные data/ пересоздаются командой python3 test/acceptance/lib/make_fixtures.py (нужен ogr2ogr).