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:
150
test/acceptance/lib/make_fixtures.py
Normal file
150
test/acceptance/lib/make_fixtures.py
Normal file
@@ -0,0 +1,150 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Regenerates the committed vector/SLD fixtures in test/acceptance/data/.
|
||||
|
||||
The fixtures are committed, so this script is only needed to rebuild them.
|
||||
Requires the GDAL command-line tool ogr2ogr (GeoPackage conversion).
|
||||
|
||||
python3 test/acceptance/lib/make_fixtures.py
|
||||
|
||||
Values are deterministic: district KATO codes and names come from
|
||||
stubs/districts.geojson (14 districts of North Kazakhstan region), the
|
||||
indicator values follow a fixed formula, so re-running yields identical files.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.abspath(os.path.join(HERE, "..", "..", ".."))
|
||||
DATA = os.path.abspath(os.path.join(HERE, "..", "data"))
|
||||
|
||||
|
||||
def ring_bbox(coords):
|
||||
xs, ys = [], []
|
||||
|
||||
def walk(c):
|
||||
if isinstance(c[0], (int, float)):
|
||||
xs.append(c[0])
|
||||
ys.append(c[1])
|
||||
else:
|
||||
for x in c:
|
||||
walk(x)
|
||||
|
||||
walk(coords)
|
||||
return min(xs), min(ys), max(xs), max(ys)
|
||||
|
||||
|
||||
def population(i, kato, year):
|
||||
# Values quoted in the protocol (Приложение 7, F16/F17) are kept verbatim.
|
||||
fixed = {
|
||||
("591000000", 2022): 47567.6,
|
||||
("591000000", 2023): 48311.2,
|
||||
("591000000", 2024): 49120.5,
|
||||
("595000000", 2022): 46862.7,
|
||||
("595000000", 2023): 48268.6,
|
||||
("595000000", 2024): 49205.8,
|
||||
}
|
||||
if (kato, year) in fixed:
|
||||
return fixed[(kato, year)]
|
||||
return round(20000 + 1750.3 * i + 812.4 * (year - 2022) + (i * 37 % 11) * 13.1, 1)
|
||||
|
||||
|
||||
def livestock(i, year):
|
||||
return int(30000 + 2311 * i + 977 * (year - 2022) + (i * 53 % 17) * 41)
|
||||
|
||||
|
||||
def main():
|
||||
with open(os.path.join(ROOT, "stubs", "districts.geojson"), encoding="utf-8") as f:
|
||||
districts = json.load(f)["features"]
|
||||
os.makedirs(DATA, exist_ok=True)
|
||||
|
||||
# 1. plain vector: 4 plot polygons spanning lon 69.0..71.7, lat 53.6..55.0
|
||||
plots = []
|
||||
boxes = [
|
||||
(69.0, 53.6, 69.6, 54.0),
|
||||
(69.9, 54.2, 70.5, 54.7),
|
||||
(70.6, 53.7, 71.1, 54.1),
|
||||
(71.2, 54.5, 71.7, 55.0),
|
||||
]
|
||||
for n, (x0, y0, x1, y1) in enumerate(boxes, 1):
|
||||
plots.append({
|
||||
"type": "Feature",
|
||||
"properties": {"plot_id": n, "name": "Участок %d" % n, "area_ha": round((x1 - x0) * (y1 - y0) * 7000, 1)},
|
||||
"geometry": {"type": "Polygon", "coordinates": [[[x0, y0], [x1, y0], [x1, y1], [x0, y1], [x0, y0]]]},
|
||||
})
|
||||
write_json("plots_nko.geojson", {"type": "FeatureCollection", "features": plots})
|
||||
|
||||
# 2. vector_with_kato (GeoJSON): population per district, Y2022..Y2024
|
||||
pop = []
|
||||
live = []
|
||||
for i, d in enumerate(districts):
|
||||
p = d["properties"]
|
||||
kato = str(p["kato"])
|
||||
x0, y0, x1, y1 = ring_bbox(d["geometry"]["coordinates"])
|
||||
pt = {"type": "Point", "coordinates": [round((x0 + x1) / 2, 5), round((y0 + y1) / 2, 5)]}
|
||||
props = {"kato": kato, "district": p["name_ru"]}
|
||||
for y in (2022, 2023, 2024):
|
||||
props["Y%d" % y] = population(i, kato, y)
|
||||
pop.append({"type": "Feature", "properties": props, "geometry": pt})
|
||||
lprops = {"KATO_CODE": kato, "district": p["name_ru"]}
|
||||
for y in (2022, 2023, 2024):
|
||||
lprops["H_%d" % y] = livestock(i, y)
|
||||
live.append({"type": "Feature", "properties": lprops, "geometry": pt})
|
||||
write_json("population_kato.geojson", {"type": "FeatureCollection", "features": pop})
|
||||
|
||||
# 3. vector_with_kato (GeoPackage): cattle per district, H_2022..H_2024
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
src = os.path.join(tmp, "livestock.geojson")
|
||||
with open(src, "w", encoding="utf-8") as f:
|
||||
json.dump({"type": "FeatureCollection", "features": live}, f, ensure_ascii=False)
|
||||
dst = os.path.join(DATA, "livestock_kato.gpkg")
|
||||
if os.path.exists(dst):
|
||||
os.remove(dst)
|
||||
subprocess.run(["ogr2ogr", "-f", "GPKG", "-nln", "livestock", "-a_srs", "EPSG:4326", dst, src], check=True)
|
||||
|
||||
# 4. SLD styles
|
||||
write_text("style.sld", sld("ndvi", "NDVI gradient", [("0.0", "#a50026"), ("0.5", "#ffffbf"), ("1.0", "#006837")]))
|
||||
write_text("group_style.sld", sld("group", "Group fallback style", [("0", "#f7fbff"), ("100000", "#08306b")]))
|
||||
write_text("style.txt", "not an SLD file\n")
|
||||
print("fixtures written to", DATA)
|
||||
|
||||
|
||||
def sld(name, title, stops):
|
||||
entries = "\n".join(
|
||||
' <sld:ColorMapEntry color="%s" quantity="%s"/>' % (c, q) for q, c in stops)
|
||||
return """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<sld:StyledLayerDescriptor version="1.0.0" xmlns:sld="http://www.opengis.net/sld" xmlns:ogc="http://www.opengis.net/ogc">
|
||||
<sld:NamedLayer>
|
||||
<sld:Name>%s</sld:Name>
|
||||
<sld:UserStyle>
|
||||
<sld:Title>%s</sld:Title>
|
||||
<sld:FeatureTypeStyle>
|
||||
<sld:Rule>
|
||||
<sld:RasterSymbolizer>
|
||||
<sld:ColorMap>
|
||||
%s
|
||||
</sld:ColorMap>
|
||||
</sld:RasterSymbolizer>
|
||||
</sld:Rule>
|
||||
</sld:FeatureTypeStyle>
|
||||
</sld:UserStyle>
|
||||
</sld:NamedLayer>
|
||||
</sld:StyledLayerDescriptor>
|
||||
""" % (name, title, entries)
|
||||
|
||||
|
||||
def write_json(name, obj):
|
||||
with open(os.path.join(DATA, name), "w", encoding="utf-8") as f:
|
||||
json.dump(obj, f, ensure_ascii=False, indent=1)
|
||||
f.write("\n")
|
||||
|
||||
|
||||
def write_text(name, text):
|
||||
with open(os.path.join(DATA, name), "w", encoding="utf-8") as f:
|
||||
f.write(text)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user