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>
This commit is contained in:
2026-10-15 22:37:28 +05:00
parent c6229c46f7
commit ca87d6961b
22 changed files with 3136 additions and 0 deletions

View File

@@ -0,0 +1,78 @@
#!/usr/bin/env python3
"""Small helpers for functional.sh (numpy + rasterio).
helpers.py ndvi OUT.tif write the 600x400 EPSG:4326 Float32 NDVI test raster
helpers.py rinfo FILE.tif print raster facts as JSON (driver, size, crs, dtype,
nodata, layout, compression, overviews)
helpers.py durations RUN.json print "total=<s> nodes=<min>..<max>ms" for a run
helpers.py elapsed DATASET.json print updated_at - created_at of a dataset, seconds
"""
import json
import sys
from datetime import datetime
def ndvi(out):
import numpy as np
import rasterio
from rasterio.transform import from_origin
w, h = 600, 400 # 0.01 deg pixels, bbox [68.5, 51.5, 74.5, 55.5]
y, x = np.mgrid[0:h, 0:w].astype(np.float32)
v = 0.45 + 0.35 * np.sin(x / 47.0) * np.cos(y / 31.0) + 0.1 * np.sin((x + y) / 13.0)
v = np.clip(v, -0.2, 1.0).astype(np.float32)
v[:5, :] = -9999
profile = dict(driver="GTiff", width=w, height=h, count=1, dtype="float32", crs="EPSG:4326",
transform=from_origin(68.5, 55.5, 0.01, 0.01), nodata=-9999)
with rasterio.open(out, "w", **profile) as dst:
dst.write(v, 1)
def rinfo(path):
import rasterio
with rasterio.open(path) as d:
st = d.tags(ns="IMAGE_STRUCTURE")
print(json.dumps({
"driver": d.driver, "width": d.width, "height": d.height,
"epsg": d.crs.to_epsg() if d.crs else None, "dtype": d.dtypes[0], "nodata": d.nodata,
"layout": st.get("LAYOUT"), "compression": st.get("COMPRESSION"),
"overviews": len(d.overviews(1)), "bounds": [round(b, 6) for b in d.bounds],
}))
def ts(s):
if not s:
return None
s = s.replace("Z", "+00:00")
if "." in s: # trim nanoseconds to microseconds for fromisoformat
head, rest = s.split(".", 1)
frac = "".join(ch for ch in rest if ch.isdigit())
tz = rest[len(frac):]
s = "%s.%s%s" % (head, frac[:6].ljust(6, "0"), tz)
return datetime.fromisoformat(s)
def durations(path):
with open(path, encoding="utf-8") as f:
run = json.load(f)
total = None
if run.get("started_at") and run.get("finished_at"):
total = (ts(run["finished_at"]) - ts(run["started_at"])).total_seconds()
nd = [(ts(n["finished_at"]) - ts(n["started_at"])).total_seconds() * 1000
for n in run.get("nodes", []) if n.get("started_at") and n.get("finished_at")]
out = "total=%.2fs" % total if total is not None else "total=?"
if nd:
out += " nodes=%d..%dms" % (min(nd), max(nd))
print(out)
def elapsed(path):
with open(path, encoding="utf-8") as f:
d = json.load(f)
print("%.2f" % (ts(d["updated_at"]) - ts(d["created_at"])).total_seconds())
if __name__ == "__main__":
cmd, arg = sys.argv[1], sys.argv[2]
{"ndvi": ndvi, "rinfo": rinfo, "durations": durations, "elapsed": elapsed}[cmd](arg)