clone

an offline map explorer where each world, from a game map to a city's open data, ships as one .atlas file

archive/golden-reference
atlas / cmd / cartograph / measure_test.go
measure_test.go 8.3K
1package main23import (4 "os"5 "path/filepath"6 "testing"78 "github.com/FelineStateMachine/atlas/internal/bundle/bundletest"9)1011// fixtureMerged is a merge ledger the shape tools/generate writes, handed to12// bundletest verbatim so measurement reads it the way it reads a real build.13func fixtureMerged() []map[string]any {14 return []map[string]any{{15 "source": "ign-wiki",16 "donorPins": 5,17 "matched": []map[string]any{18 {"d": 11, "w": 1, "px": 12},19 {"d": 12, "w": 2, "px": 40, "e": true},20 },21 "added": 1,22 "adopted": []map[string]any{{"d": 13, "into": "markers"}},23 "held": []map[string]any{{"d": 14, "t": "Old Well", "why": "name 200px away"}},24 "rejected": []map[string]any{{"d": 15, "t": "Off World", "why": "outside the world"}},25 "alignment": "affine over 9 anchors, median 26px",26 }, {27 // A ledger that speaks features, the way builds write them now:28 // donorPins repeats the point count and the shapes ride held.29 "source": "arcgis-hub",30 "donorPins": 0,31 "donorFeatures": map[string]any{"point": 0, "path": 1, "area": 2},32 "held": []map[string]any{33 {"d": 21, "t": "North Basin", "why": "shape features do not merge yet"},34 {"d": 22, "t": "South Basin", "why": "shape features do not merge yet"},35 {"d": 23, "t": "Mill Race", "why": "shape features do not merge yet"},36 },37 }}38}3940func TestMeasureBundle(t *testing.T) {41 path := bundletest.Build(t, t.TempDir(), bundletest.Spec{42 Slug: "hollowmere",43 Title: "Hollowmere",44 CreatedAt: "2026-03-01T00:00:00Z",45 Worlds: []bundletest.WorldSpec{{46 Slug: "overworld",47 Pins: []bundletest.Pin{48 {Title: "Gate", Description: "The way in."},49 {Title: "Well", Silent: true},50 {Title: "Shrine"},51 },52 Merged: fixtureMerged(),53 }},54 })5556 measured, err := measureBundle(path)57 if err != nil {58 t.Fatal(err)59 }60 if measured.VolumeSlug != "hollowmere" || measured.VolumeTitle != "Hollowmere" {61 t.Fatalf("identity read as %s (%s)", measured.VolumeTitle, measured.VolumeSlug)62 }6364 // Annotation: three pins, two with words. The medians sort "The way in."65 // (11 chars) under "About Shrine" (12), so the upper middle is 12.66 if measured.Pins != 3 || measured.Described != 2 {67 t.Errorf("annotation: %d pins, %d described; want 3 and 2", measured.Pins, measured.Described)68 }69 if measured.MedianLength != 12 {70 t.Errorf("median description length %d, want 12", measured.MedianLength)71 }72 if got := measured.DescribedPct(); got != "66%" {73 t.Errorf("described share %s, want 66%%", got)74 }7576 // Cartography: the fixture writes two distinct tiles, one at each level.77 if measured.TileCount != 2 || measured.Depth != 1 || measured.DepthTiles != 1 {78 t.Errorf("cartography: %d tiles, depth z%d holds %d; want 2, z1, 1",79 measured.TileCount, measured.Depth, measured.DepthTiles)80 }81 if measured.RasterBytes == 0 {82 t.Error("raster bytes read as zero")83 }8485 // Structure and icons: one group of one iconed marker category, one layer.86 if measured.Categories != 1 || measured.Groups != 1 || measured.Lenses != 1 {87 t.Errorf("structure: %d categories, %d groups, %d layers; want 1 each",88 measured.Categories, measured.Groups, measured.Lenses)89 }90 if measured.IconsCarried != 1 || measured.IconsWanted != 1 {91 t.Errorf("icons: %d of %d; want 1 of 1", measured.IconsCarried, measured.IconsWanted)92 }9394 // The merge accounts, kept whole rather than counted.95 if len(measured.Merges) != 2 {96 t.Fatalf("read %d merge accounts, want 2", len(measured.Merges))97 }98 account := measured.Merges[0]99 if account.Map != "overworld" || account.Source != "ign-wiki" || account.DonorPins != 5 {100 t.Errorf("account identity: map %s source %s donors %d", account.Map, account.Source, account.DonorPins)101 }102 // An account from before the ledger spoke features: the point count103 // stands in for the whole offering.104 if account.DonorFeatures != (featureCounts{Point: 5}) || account.DonorShapesN() != 0 {105 t.Errorf("fallback features read as %+v (%d shapes), want 5 points standing in",106 account.DonorFeatures, account.DonorShapesN())107 }108 if account.MatchedN() != 2 || account.MedianMatchPx() != 40 {109 t.Errorf("matched %d at median %dpx, want 2 at 40px", account.MatchedN(), account.MedianMatchPx())110 }111 if account.Added != 1 || account.AdoptedN() != 1 || account.RejectedN() != 1 {112 t.Errorf("added %d adopted %d rejected %d, want 1 each",113 account.Added, account.AdoptedN(), account.RejectedN())114 }115 if account.HeldN() != 1 || account.Held[0].Title != "Old Well" || account.Held[0].Reason != "name 200px away" {116 t.Errorf("held ledger read wrong: %+v", account.Held)117 }118 // An account that speaks features: the per-kind counts come through and119 // the held shapes ride the same ledger the held pins do.120 shapes := measured.Merges[1]121 if shapes.DonorFeatures != (featureCounts{Point: 0, Path: 1, Area: 2}) || shapes.DonorShapesN() != 3 {122 t.Errorf("feature counts read as %+v (%d shapes), want 1 path and 2 areas",123 shapes.DonorFeatures, shapes.DonorShapesN())124 }125 if shapes.DonorPins != 0 || shapes.AddedShapes != 0 || shapes.HeldN() != 3 {126 t.Errorf("shape account read as %d donor pins, %d added shapes, %d held; want 0, 0, 3",127 shapes.DonorPins, shapes.AddedShapes, shapes.HeldN())128 }129 if got := measured.SourcesSeen(); len(got) != 2 || got[0] != "arcgis-hub" || got[1] != "ign-wiki" {130 t.Errorf("sources seen %v, want [arcgis-hub ign-wiki]", got)131 }132}133134// install builds a fixture and moves it into the scanned directory under its135// own name, the way versioned builds of one game sit side by side.136func install(t *testing.T, dir string, name string, spec bundletest.Spec) string {137 t.Helper()138 built := bundletest.Build(t, t.TempDir(), spec)139 target := filepath.Join(dir, name)140 if err := os.Rename(built, target); err != nil {141 t.Fatal(err)142 }143 return target144}145146func TestLibraryOrdersBuilds(t *testing.T) {147 dir := t.TempDir()148 install(t, dir, "hollowmere-jan.atlas", bundletest.Spec{149 Slug: "hollowmere", Title: "Hollowmere", CreatedAt: "2026-01-01T00:00:00Z",150 })151 install(t, dir, "hollowmere-feb.atlas", bundletest.Spec{152 Slug: "hollowmere", Title: "Hollowmere", CreatedAt: "2026-02-01T00:00:00Z",153 })154 // Same capture as February, newer policy: the revision must win the top.155 install(t, dir, "hollowmere-feb-r1.atlas", bundletest.Spec{156 Slug: "hollowmere", Title: "Hollowmere", CreatedAt: "2026-02-01T00:00:00Z", Revision: 1,157 })158 install(t, dir, "briar.atlas", bundletest.Spec{159 Slug: "briar", Title: "Briar", CreatedAt: "2026-01-15T00:00:00Z",160 })161162 lib := &library{dir: dir}163 games, skipped, err := lib.volumes()164 if err != nil {165 t.Fatal(err)166 }167 if len(skipped) != 0 {168 t.Fatalf("skipped %v", skipped)169 }170 if len(games) != 2 {171 t.Fatalf("read %d games, want 2", len(games))172 }173 // Sorted by title: Briar before Hollowmere.174 if games[0].Slug != "briar" || games[1].Slug != "hollowmere" {175 t.Fatalf("order %s, %s", games[0].Slug, games[1].Slug)176 }177 builds := games[1].Builds178 if len(builds) != 3 {179 t.Fatalf("hollowmere has %d builds, want 3", len(builds))180 }181 if builds[0].File != "hollowmere-feb-r1.atlas" {182 t.Errorf("serving build is %s, want the revised February build", builds[0].File)183 }184 if builds[1].File != "hollowmere-feb.atlas" || builds[2].File != "hollowmere-jan.atlas" {185 t.Errorf("order below serving: %s, %s", builds[1].File, builds[2].File)186 }187 if games[1].Serving() != builds[0] {188 t.Error("Serving is not the first build")189 }190 if games[1].Build("hollowmere-jan.atlas") != builds[2] {191 t.Error("Build by file name missed")192 }193}194195func TestLibrarySkipsBadBundles(t *testing.T) {196 dir := t.TempDir()197 install(t, dir, "briar.atlas", bundletest.Spec{Slug: "briar"})198 if err := os.WriteFile(filepath.Join(dir, "broken.atlas"), []byte("not a zip"), 0o644); err != nil {199 t.Fatal(err)200 }201 games, skipped, err := (&library{dir: dir}).volumes()202 if err != nil {203 t.Fatal(err)204 }205 if len(games) != 1 || len(skipped) != 1 {206 t.Fatalf("read %d games and %d skipped, want 1 and 1", len(games), len(skipped))207 }208}209210func TestLoadPins(t *testing.T) {211 path := bundletest.Build(t, t.TempDir(), bundletest.Spec{212 Slug: "hollowmere",213 Worlds: []bundletest.WorldSpec{{214 Slug: "overworld",215 Pins: []bundletest.Pin{{Title: "Gate"}, {Title: "Well"}},216 }},217 })218 pins, err := loadPins(path, []string{"overworld"})219 if err != nil {220 t.Fatal(err)221 }222 titles := pins["overworld"]223 if len(titles) != 2 || titles[1] != "Gate" || titles[2] != "Well" {224 t.Fatalf("pins read as %v", titles)225 }226}