test(simulation): golden-seed regression for the Layer 0->1 cascade (#952, D-200)
End-to-end determinism guard. cascade_golden.rs pins two artifacts for a real committed body heightmap (GJ1c) in one diffable JSON golden (server/tests/golden/cascade_layer1.json): - Layer 0: SHA-256 of the source heightmap.png bytes (flips if the Python heightmap generator or the file changes) - Layer 1: the serialized Layer1Output of run_cascade on a 128x64 downsample (flips if the Rust drainage/feature/sub-biome code changes) JSON (not the msgpack discussed in refinement) to match the existing golden_suite.rs convention and stay diffable — a failure shows what drifted. UPDATE_GOLDEN=1 regenerates; wired into `make golden-update`. Adds Serialize/Deserialize to RiverNetwork/DrainageBasin/Layer1Output and a sha2 dev-dependency. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -209,8 +209,9 @@ golden-diff:
|
||||
fi
|
||||
|
||||
golden-update:
|
||||
@echo "Regenerating golden file..."
|
||||
@echo "Regenerating golden files..."
|
||||
@cd server && UPDATE_GOLDEN=1 cargo test --test golden_suite -- proof_room_tick_10_matches_golden
|
||||
@cd server && UPDATE_GOLDEN=1 cargo test --test cascade_golden
|
||||
@git add server/tests/golden/
|
||||
@echo "--- Golden file updated and staged ---"
|
||||
@echo "Review with: git diff --cached -- server/tests/golden/"
|
||||
|
||||
Generated
+66
@@ -352,6 +352,15 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block2"
|
||||
version = "0.6.2"
|
||||
@@ -480,6 +489,15 @@ dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.5.0"
|
||||
@@ -538,6 +556,16 @@ version = "0.8.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ctrlc"
|
||||
version = "3.5.2"
|
||||
@@ -584,6 +612,16 @@ dependencies = [
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dispatch2"
|
||||
version = "0.3.1"
|
||||
@@ -776,6 +814,16 @@ dependencies = [
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.4"
|
||||
@@ -1443,6 +1491,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_norway",
|
||||
"sha2",
|
||||
"sysinfo",
|
||||
"thiserror",
|
||||
"toml",
|
||||
@@ -1450,6 +1499,17 @@ dependencies = [
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sharded-slab"
|
||||
version = "0.1.7"
|
||||
@@ -1727,6 +1787,12 @@ version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
|
||||
@@ -42,6 +42,8 @@ gauntlet = []
|
||||
# Maintained drop-in fork of the deprecated serde_yaml 0.9 (#966). Test-only:
|
||||
# the YAML round-trip in poi.rs and the trait-config fixture in trait_modifiers.rs.
|
||||
serde_norway = "0.9"
|
||||
# SHA-256 of the source heightmap.png in the cascade golden test (#952).
|
||||
sha2 = "0.10"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Explicit test target for the Layer 3 integration module (D-030, ticket #200).
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use bevy_ecs::prelude::Resource;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::simulation::generator::GeographicAttractor;
|
||||
|
||||
@@ -26,7 +27,7 @@ pub const CACHE_CAPACITY: usize = 50;
|
||||
|
||||
/// River network extracted by the D8 drainage algorithm (D-208).
|
||||
/// Stub — replaced when #918 is implemented.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct RiverNetwork {
|
||||
/// Pixel positions (row, col) of all river cells (flow_accumulation > 200).
|
||||
pub river_cells: Vec<(u16, u16)>,
|
||||
@@ -38,7 +39,7 @@ pub struct RiverNetwork {
|
||||
|
||||
/// One drainage basin / province derived from watershed analysis (D-205).
|
||||
/// Stub — boundary polyline data comes from atlas_province_boundaries.
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DrainageBasin {
|
||||
pub basin_id: u32,
|
||||
/// Boundary polyline as pixel-space (row, col) points.
|
||||
|
||||
@@ -21,9 +21,10 @@ use crate::atlas::features::{self, TerrainAnalysis};
|
||||
use crate::atlas::heightmap::BodyHeightmap;
|
||||
use crate::atlas::subbiome;
|
||||
use crate::simulation::generator::{AttractorType, GeographicAttractor};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Full Layer-1 result for one body.
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Layer1Output {
|
||||
pub body_id: String,
|
||||
pub river_network: RiverNetwork,
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
//! Golden-seed regression for the Layer 0→1 generation cascade (#952, D-200, D-224).
|
||||
//!
|
||||
//! Pins two artifacts for one fixed input heightmap, in a single diffable JSON
|
||||
//! golden (matching the `golden_suite.rs` convention):
|
||||
//! - **Layer 0** — SHA-256 of the source `heightmap.png` bytes. Flips if the
|
||||
//! Python heightmap generator (or the committed file) changes.
|
||||
//! - **Layer 1** — the serialized `Layer1Output` of the cascade run on a
|
||||
//! downsampled copy. Flips if the Rust drainage / feature / sub-biome code
|
||||
//! changes.
|
||||
//!
|
||||
//! The input is a real committed body heightmap (the #963 bake output), so this
|
||||
//! is an end-to-end determinism guard. The cascade is downsampled to keep the
|
||||
//! golden small while still exercising the full Layer-1 pipeline.
|
||||
//!
|
||||
//! Regenerate after an intended change:
|
||||
//! UPDATE_GOLDEN=1 cargo test --test cascade_golden
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde_json::{json, Value};
|
||||
use settled_reach_server::atlas::cascade::{run_cascade_from_heightmap, CascadeLayer};
|
||||
use settled_reach_server::atlas::heightmap::load_heightmap_png;
|
||||
use settled_reach_server::seed::{SeedChain, SeedDomain};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
/// Source heightmap — a real committed body, relative to the server manifest dir.
|
||||
const SOURCE_HEIGHTMAP: &str = "../wiki/star-systems/GJ-1/bodies/GJ1c/heightmap.png";
|
||||
/// Downsample target: small enough for a compact golden, large enough for real
|
||||
/// drainage/feature structure.
|
||||
const DOWNSAMPLE: (u32, u32) = (128, 64);
|
||||
/// World seed for the run. Layers 0–1 are RNG-free; this is carried for the
|
||||
/// SeedChain contract (D-224).
|
||||
const WORLD_SEED: u64 = 42;
|
||||
const GOLDEN: &str = "tests/golden/cascade_layer1.json";
|
||||
|
||||
#[test]
|
||||
fn cascade_layer0_to_1_matches_golden() {
|
||||
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
let src = manifest.join(SOURCE_HEIGHTMAP);
|
||||
|
||||
// ── Layer 0 — load + hash the source heightmap.png. ─────────────────────
|
||||
let png_bytes = std::fs::read(&src)
|
||||
.unwrap_or_else(|e| panic!("read source heightmap {}: {e}", src.display()));
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&png_bytes);
|
||||
let sha = format!("{:x}", hasher.finalize());
|
||||
|
||||
let heightmap = load_heightmap_png(&src, "GJ1c", 0.3).expect("decode source heightmap");
|
||||
|
||||
// ── Layer 1 — downsample, then run the cascade to topography. ───────────
|
||||
let small = heightmap.downsample(DOWNSAMPLE.0, DOWNSAMPLE.1);
|
||||
let body_seed = SeedChain::root(WORLD_SEED).derive(SeedDomain::Body, 1);
|
||||
let snapshot = run_cascade_from_heightmap(body_seed, small, CascadeLayer::Topography);
|
||||
let layer1 = snapshot.layer1.expect("Layer 1 ran");
|
||||
|
||||
let actual = json!({
|
||||
"source_heightmap": SOURCE_HEIGHTMAP,
|
||||
"source_sha256": sha,
|
||||
"downsample": [DOWNSAMPLE.0, DOWNSAMPLE.1],
|
||||
"layer1": serde_json::to_value(&layer1).expect("serialize Layer1Output"),
|
||||
});
|
||||
let actual_json = serde_json::to_string_pretty(&actual).expect("format JSON") + "\n";
|
||||
|
||||
let golden_path = manifest.join(GOLDEN);
|
||||
|
||||
if std::env::var("UPDATE_GOLDEN").is_ok() {
|
||||
std::fs::create_dir_all(golden_path.parent().unwrap()).expect("mkdir golden");
|
||||
std::fs::write(&golden_path, &actual_json).expect("write golden");
|
||||
eprintln!(
|
||||
"Golden written: {} ({} bytes)",
|
||||
golden_path.display(),
|
||||
actual_json.len()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let golden_json = std::fs::read_to_string(&golden_path).unwrap_or_else(|e| {
|
||||
panic!(
|
||||
"Golden not found: {}. Run: UPDATE_GOLDEN=1 cargo test --test cascade_golden\n{e}",
|
||||
golden_path.display()
|
||||
)
|
||||
});
|
||||
|
||||
// Compare as parsed JSON so whitespace/formatting never causes spurious diffs.
|
||||
let actual_v: Value = serde_json::from_str(&actual_json).expect("reparse actual JSON");
|
||||
let golden_v: Value = serde_json::from_str(&golden_json).expect("parse golden JSON");
|
||||
|
||||
if actual_v != golden_v {
|
||||
let count =
|
||||
|v: &Value, ptr: &str| v.pointer(ptr).and_then(Value::as_array).map_or(0, Vec::len);
|
||||
panic!(
|
||||
"Cascade golden mismatch.\n \
|
||||
source_sha256: golden={} actual={}\n \
|
||||
river_cells: golden={} actual={}\n \
|
||||
attractors: golden={} actual={}\n\
|
||||
To update: UPDATE_GOLDEN=1 cargo test --test cascade_golden\n Golden: {}",
|
||||
golden_v["source_sha256"],
|
||||
actual_v["source_sha256"],
|
||||
count(&golden_v, "/layer1/river_network/river_cells"),
|
||||
count(&actual_v, "/layer1/river_network/river_cells"),
|
||||
count(&golden_v, "/layer1/attractors"),
|
||||
count(&actual_v, "/layer1/attractors"),
|
||||
golden_path.display(),
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user