Files
UnrealPrototyping/Tools/Terrain/internal/studio/patch_test.go
T
2026-09-25 17:02:24 +03:00

232 lines
7.6 KiB
Go

package studio
import (
"os"
"path/filepath"
"strings"
"testing"
)
const legendSrc = `{
"_comment": "what the colours mean",
"image": "Map3.jpg",
"classes": [
{ "name": "ocean", "rgb": [91, 175, 185], "sea": true, "depth_m": 512 },
{ "_comment_plain": "why the floor is a tenth of the rate, at length",
"name": "lowland", "rgb": [153, 204, 102], "uplift_mm_yr": 0.08, "k_mult": 1.0,
"massif": { "floor_mm_yr": 0.012, "fraction": 0.16 },
"coastal_plain_km": 1.0 },
{ "name": "highland", "rgb": [68, 170, 102], "uplift_mm_yr": 0.25, "k_mult": 1.0 }
]
}`
// The whole reason this is text surgery and not MarshalIndent: a legend is mostly commentary, and the
// commentary has to survive a save byte for byte.
func TestPatchingKeepsEverythingItDidNotChange(t *testing.T) {
out, err := patchClassNumber(legendSrc, "lowland", "uplift_mm_yr", 0.12)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(out, `"uplift_mm_yr": 0.12`) {
t.Error("the new value is not there")
}
if strings.Contains(out, `"uplift_mm_yr": 0.08`) {
t.Error("the old value is still there")
}
if !strings.Contains(out, `"_comment_plain": "why the floor is a tenth of the rate, at length"`) {
t.Error("the comment was dropped")
}
if !strings.Contains(out, `"uplift_mm_yr": 0.25`) {
t.Error("the other class's rate was touched")
}
// And nothing else moved: the only difference from the original is those four characters.
if a, b := strings.Replace(out, "0.12", "0.08", 1), legendSrc; a != b {
t.Errorf("the file changed somewhere else:\n--- got\n%s\n--- want\n%s", a, b)
}
}
func TestPatchingAddsAKeyThatIsNotThere(t *testing.T) {
out, err := patchClassNumber(legendSrc, "highland", "coastal_plain_km", 4)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(out, `"coastal_plain_km": 4`) {
t.Fatalf("the key was not added:\n%s", out)
}
if !strings.Contains(out, `"name": "highland", "coastal_plain_km": 4`) {
t.Errorf("it did not go in after the name:\n%s", out)
}
}
func TestPatchingAMassifBlock(t *testing.T) {
order := []string{"floor_mm_yr", "fraction"}
out, err := patchClassObject(legendSrc, "lowland", "massif",
map[string]float64{"floor_mm_yr": 0.02, "fraction": 0.25}, order)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(out, `"massif": { "floor_mm_yr": 0.02, "fraction": 0.25 }`) {
t.Fatalf("the massif block was not rewritten:\n%s", out)
}
// Adding one to a class that has none.
out, err = patchClassObject(legendSrc, "highland", "massif",
map[string]float64{"floor_mm_yr": 0.045, "fraction": 0.3}, order)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(out, `"massif": { "floor_mm_yr": 0.045, "fraction": 0.3 }`) {
t.Fatalf("the massif block was not added:\n%s", out)
}
}
func TestRemovingAMassifBlock(t *testing.T) {
out, err := patchClassObject(legendSrc, "lowland", "massif", nil, nil)
if err != nil {
t.Fatal(err)
}
if strings.Contains(out, "massif") {
t.Fatalf("the massif block is still there:\n%s", out)
}
// The class has to still parse: no doubled or dangling comma where it was.
if strings.Contains(out, ",,") || strings.Contains(out, ", }") && !strings.Contains(legendSrc, ", }") {
t.Errorf("the comma was left in a bad state:\n%s", out)
}
}
func TestPatchingARefusesAClassThatIsNotThere(t *testing.T) {
if _, err := patchClassNumber(legendSrc, "tundra", "uplift_mm_yr", 0.1); err == nil {
t.Fatal("it accepted a class the legend does not have")
}
}
const manifestSrc = `{
"level": "/Game/Maps/L_Planet",
"planet": {
"_comment_scale": "why the cell is eight metres",
"template": "Templates/Map3.jpg",
"circumference_km": 100,
"coast_jitter_px": 48
}
}`
func TestPatchingTheManifestPlanetBlock(t *testing.T) {
out, err := patchTopNumber(manifestSrc, "planet", "coast_jitter_px", 64)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(out, `"coast_jitter_px": 64`) {
t.Fatalf("not patched:\n%s", out)
}
if !strings.Contains(out, `"_comment_scale"`) {
t.Error("the comment was dropped")
}
out, err = patchTopNumber(manifestSrc, "planet", "massif_wavelength_km", 7)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(out, `"massif_wavelength_km": 7`) {
t.Fatalf("a missing key was not added:\n%s", out)
}
out, err = patchTopString(manifestSrc, "planet", "template", "Templates/Map3.png")
if err != nil {
t.Fatal(err)
}
if !strings.Contains(out, `"template": "Templates/Map3.png"`) {
t.Fatalf("the template path was not repointed:\n%s", out)
}
}
// The base map is an input a person made by hand and there is no undo for it outside this process. Saving
// versions rather than overwriting is the whole contract, and the second half of it is that saving twice
// gives _001 and _002 rather than _001 and _001_002.
func TestSavingNeverOverwritesAndNumbersUpwards(t *testing.T) {
dir := t.TempDir()
base := filepath.Join(dir, "Map3.jpg")
if err := os.WriteFile(base, []byte("the base map"), 0o644); err != nil {
t.Fatal(err)
}
first, err := nextVersion(base)
if err != nil {
t.Fatal(err)
}
if filepath.Base(first) != "Map3_001.png" {
t.Errorf("first save went to %q, want Map3_001.png", filepath.Base(first))
}
if err := os.WriteFile(first, []byte("v1"), 0o644); err != nil {
t.Fatal(err)
}
// Asked again from the *versioned* path, which is what the manifest now points at.
second, err := nextVersion(first)
if err != nil {
t.Fatal(err)
}
if filepath.Base(second) != "Map3_002.png" {
t.Errorf("second save went to %q, want Map3_002.png", filepath.Base(second))
}
// And the base map is still exactly what it was.
if b, err := os.ReadFile(base); err != nil || string(b) != "the base map" {
t.Errorf("the base map was touched: %q %v", b, err)
}
}
// The overlay legend is patched by the same three functions - a mark is an object with a "name" like a class
// is - so the thing worth testing separately is the one that is new: removing a key.
const overlaySrc = `{
"image": "Map3.overlay.png",
"marks": [
{ "_comment": "why this shore is pinned, at length",
"name": "drawn_coast", "rgb": [255, 0, 255], "coast_jitter": 0 },
{ "name": "forest", "rgb": [0, 128, 0] },
{ "name": "road", "rgb": [90, 60, 30], "kind": "path", "width_m": 8 }
]
}`
func TestRemovingAKeyLeavesNoTrace(t *testing.T) {
// Add one, then take it away: the file has to come back exactly as it started, or every later diff
// carries the scar of a setting somebody tried once.
with, err := patchClassNumber(overlaySrc, "forest", "coast_jitter", 0.5)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(with, `"coast_jitter": 0.5`) {
t.Fatalf("the key was not added:\n%s", with)
}
back, err := patchClassRemove(with, "forest", "coast_jitter")
if err != nil {
t.Fatal(err)
}
if back != overlaySrc {
t.Errorf("a round trip changed the file:\n--- want ---\n%s\n--- got ---\n%s", overlaySrc, back)
}
// Removing one that is not there is not an error: the studio sends "this mark says nothing about the
// coast" whether or not it ever did.
same, err := patchClassRemove(overlaySrc, "road", "coast_jitter")
if err != nil || same != overlaySrc {
t.Errorf("removing an absent key should be a no-op; err=%v changed=%v", err, same != overlaySrc)
}
// And the one that is there, on a mark carrying commentary.
out, err := patchClassRemove(overlaySrc, "drawn_coast", "coast_jitter")
if err != nil {
t.Fatal(err)
}
if strings.Contains(out, "coast_jitter") {
t.Error("the key is still there")
}
if !strings.Contains(out, `"_comment": "why this shore is pinned, at length"`) {
t.Error("the comment went with it")
}
if !strings.Contains(out, `"name": "drawn_coast", "rgb": [255, 0, 255] }`) {
t.Errorf("the trailing comma and its space were not cleaned up:\n%s", out)
}
}