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>
Пакет приёмочных испытаний (Этап 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 ГиБ); загрузка сверх предела отклоняется с 413request 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.peakworker (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-запросы:
GET /landscape/runs/{id}— параметры прогона, для каждого узлаinputsиoutputs(группа → набор данных);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).