clone

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

main
atlas / format / bundle / registry_test.go
registry_test.go 13.0K
1package bundle_test23import (4 "encoding/json"5 "math/rand"6 "os"7 "path/filepath"8 "reflect"9 "testing"1011 "github.com/FelineStateMachine/atlas/format/bundle"12)1314func build(locator, slug, createdAt string, revision int, stamp string) bundle.Descriptor {15 return bundle.Descriptor{16 Locator: locator, Slug: slug, Title: slug,17 CreatedAt: createdAt, Revision: revision, Stamp: stamp, Worlds: 1,18 }19}2021// The build ordering, stated once as a table: creation time, then policy22// revision, then stamp, then locator. Every pair is asymmetric and no pair is23// undecided, which is what makes two folds of one library always agree.24func TestNewerIsTheBuildOrdering(t *testing.T) {25 cases := []struct {26 name string27 a, b bundle.Descriptor28 }{29 {30 "a newer capture wins",31 build("/a", "game", "2026-06-01T00:00:00Z", 0, "00"),32 build("/b", "game", "2026-01-01T00:00:00Z", 9, "ff"),33 },34 {35 "among one capture, the newer revision wins",36 build("/a", "game", "2026-06-01T00:00:00Z", 9, "00"),37 build("/b", "game", "2026-06-01T00:00:00Z", 8, "ff"),38 },39 {40 "among one revision, the higher stamp wins",41 build("/a", "game", "2026-06-01T00:00:00Z", 9, "ff"),42 build("/b", "game", "2026-06-01T00:00:00Z", 9, "00"),43 },44 {45 "among identical versions, the locator decides",46 build("/b", "game", "2026-06-01T00:00:00Z", 9, "ff"),47 build("/a", "game", "2026-06-01T00:00:00Z", 9, "ff"),48 },49 }50 for _, test := range cases {51 t.Run(test.name, func(t *testing.T) {52 if !bundle.Newer(test.a, test.b) {53 t.Errorf("%+v did not beat %+v", test.a, test.b)54 }55 if bundle.Newer(test.b, test.a) {56 t.Errorf("%+v also beat %+v", test.b, test.a)57 }58 })59 }60 self := build("/a", "game", "2026-06-01T00:00:00Z", 1, "aa")61 if bundle.Newer(self, self) {62 t.Error("a build shadows itself")63 }64}6566// The fold is a function: the same descriptors in any arrangement give the67// same winners, and folding never touches its input.68func TestFoldIsDeterministic(t *testing.T) {69 candidates := []bundle.Descriptor{70 build("/mars-1", "mars", "2026-08-01T14:20:42Z", 9, "aa"),71 build("/mars-2", "mars", "2026-08-01T14:48:10Z", 9, "bb"),72 build("/mars-3", "mars", "2026-08-01T14:48:10Z", 3, "ff"),73 build("/city-1", "bend-or", "2026-08-01T20:13:08Z", 9, "cc"),74 build("/city-2", "bend-or", "2026-07-01T20:13:08Z", 9, "dd"),75 build("/lone", "tunic", "2026-07-30T00:00:00Z", 9, "ee"),76 }77 want := bundle.Fold(candidates)78 if len(want) != 3 {79 t.Fatalf("the fold picked %d winners, want 3", len(want))80 }81 if want["mars"].Locator != "/mars-2" {82 t.Errorf("mars served by %s", want["mars"].Locator)83 }84 if want["bend-or"].Locator != "/city-1" {85 t.Errorf("bend-or served by %s", want["bend-or"].Locator)86 }8788 shuffled := append([]bundle.Descriptor(nil), candidates...)89 random := rand.New(rand.NewSource(1))90 for round := 0; round < 50; round++ {91 random.Shuffle(len(shuffled), func(i, j int) { shuffled[i], shuffled[j] = shuffled[j], shuffled[i] })92 if got := bundle.Fold(shuffled); !reflect.DeepEqual(got, want) {93 t.Fatalf("round %d folded to %v, want %v", round, got, want)94 }95 }96 if !reflect.DeepEqual(candidates, append([]bundle.Descriptor(nil), candidates...)) {97 t.Error("the fold mutated its input")98 }99 if len(bundle.Fold(nil)) != 0 {100 t.Error("an empty library folded to something")101 }102}103104func TestShadowedListsWhatTheFoldPassedOver(t *testing.T) {105 candidates := []bundle.Descriptor{106 build("/mars-1", "mars", "2026-08-01T14:20:42Z", 9, "aa"),107 build("/mars-2", "mars", "2026-08-01T14:48:10Z", 9, "bb"),108 build("/mars-3", "mars", "2026-07-01T00:00:00Z", 9, "cc"),109 build("/lone", "tunic", "2026-07-30T00:00:00Z", 9, "ee"),110 }111 shadowed := bundle.Shadowed(candidates)112 var locators []string113 for _, descriptor := range shadowed {114 locators = append(locators, descriptor.Locator)115 }116 if !reflect.DeepEqual(locators, []string{"/mars-1", "/mars-3"}) {117 t.Errorf("shadowed = %v", locators)118 }119}120121func TestChangedNamesTheVolumesThatMoved(t *testing.T) {122 before := bundle.Fold([]bundle.Descriptor{123 build("/mars-1", "mars", "2026-08-01T14:20:42Z", 9, "aa"),124 build("/gone", "tunic", "2026-07-30T00:00:00Z", 9, "ee"),125 build("/same", "gta5", "2026-07-31T00:00:00Z", 9, "ff"),126 })127 after := bundle.Fold([]bundle.Descriptor{128 build("/mars-2", "mars", "2026-08-01T14:48:10Z", 9, "bb"),129 build("/same", "gta5", "2026-07-31T00:00:00Z", 9, "ff"),130 build("/new", "skyrim", "2026-07-30T00:00:00Z", 9, "dd"),131 })132 want := []string{"mars", "skyrim", "tunic"}133 if got := bundle.Changed(before, after); !reflect.DeepEqual(got, want) {134 t.Errorf("Changed = %v, want %v", got, want)135 }136 if got := bundle.Changed(after, after); len(got) != 0 {137 t.Errorf("a rescan of the same library reported %v", got)138 }139}140141// The index is derived: volumes by title, builds newest first, and the title142// a volume answers to is its newest build's.143func TestBuildIndexIsDerivedAndOrdered(t *testing.T) {144 renamed := build("/dir/mars-new.atlas", "mars", "2026-08-01T14:48:10Z", 9, "bb")145 renamed.Title = "Mars"146 renamed.Size = 200147 older := build("/dir/mars-old.atlas", "mars", "2026-08-01T14:20:42Z", 9, "aa")148 older.Title = "The Red Planet"149 older.Size = 100150151 index := bundle.BuildIndex([]bundle.Descriptor{152 older,153 renamed,154 func() bundle.Descriptor {155 d := build("/dir/atlas-city.atlas", "bend-or", "2026-08-01T20:13:08Z", 9, "cc")156 d.Title = "Alphabetically first"157 return d158 }(),159 })160 if len(index.Volumes) != 2 {161 t.Fatalf("the index lists %d volumes, want 2", len(index.Volumes))162 }163 if index.Volumes[0].Title != "Alphabetically first" {164 t.Errorf("volumes are not sorted by title: %s first", index.Volumes[0].Title)165 }166 mars := index.Volumes[1]167 if mars.Title != "Mars" {168 t.Errorf("the volume answers to %q, want the newest build's title", mars.Title)169 }170 if len(mars.Versions) != 2 || mars.Versions[0].File != "mars-new.atlas" {171 t.Errorf("builds are not newest first: %+v", mars.Versions)172 }173 if mars.Versions[0].Size != 200 || mars.Versions[0].Worlds != 1 {174 t.Errorf("build carries %+v", mars.Versions[0])175 }176177 // The wire keys are history the listing never renamed; a reader of an old178 // index must still find them.179 data, err := bundle.MarshalIndex(index)180 if err != nil {181 t.Fatal(err)182 }183 if data[len(data)-1] != '\n' {184 t.Error("the index is not newline-terminated")185 }186 var wire struct {187 Games []struct {188 Slug string `json:"slug"`189 Versions []struct {190 File string `json:"file"`191 Maps int `json:"maps"`192 } `json:"versions"`193 } `json:"games"`194 }195 if err := json.Unmarshal(data, &wire); err != nil {196 t.Fatal(err)197 }198 if len(wire.Games) != 2 || wire.Games[1].Slug != "mars" || wire.Games[1].Versions[0].Maps != 1 {199 t.Errorf("the index wire reads as %s", data)200 }201}202203func TestScanFoldsARealDirectory(t *testing.T) {204 dir := t.TempDir()205 fixture{slug: "mars", title: "Mars", createdAt: "2026-08-01T14:20:42Z", stamp: "aa"}.build(t, dir)206 // A second build of the same volume, installed under its versioned name so207 // the two sit side by side.208 newer := filepath.Join(t.TempDir(), "mars.atlas")209 os.Rename(fixture{210 slug: "mars", title: "Mars", createdAt: "2026-08-01T14:48:10Z", stamp: "bb",211 }.build(t, filepath.Dir(newer)), newer)212 if _, err := bundle.Install(dir, newer); err != nil {213 t.Fatal(err)214 }215 // And something that is not a bundle at all.216 if err := os.WriteFile(filepath.Join(dir, "rubbish.atlas"), []byte("not a zip"), 0o644); err != nil {217 t.Fatal(err)218 }219220 descriptors, skipped, err := bundle.Scan(dir)221 if err != nil {222 t.Fatal(err)223 }224 if len(skipped) != 1 || filepath.Base(skipped[0].Locator) != "rubbish.atlas" {225 t.Errorf("the scan skipped %v", skipped)226 }227 if len(descriptors) != 2 {228 t.Fatalf("the scan found %d bundles, want 2", len(descriptors))229 }230 winners := bundle.Fold(descriptors)231 if winners["mars"].Stamp != "bb" {232 t.Errorf("mars serves stamp %q, want the newer build", winners["mars"].Stamp)233 }234235 if err := bundle.WriteIndex(dir, descriptors); err != nil {236 t.Fatal(err)237 }238 written, err := os.ReadFile(filepath.Join(dir, "index.json"))239 if err != nil {240 t.Fatal(err)241 }242 derived, err := bundle.MarshalIndex(bundle.BuildIndex(descriptors))243 if err != nil {244 t.Fatal(err)245 }246 if string(written) != string(derived) {247 t.Error("the written index is not the derived one")248 }249}250251func TestScanOfNothingIsAnEmptyRegistry(t *testing.T) {252 descriptors, skipped, err := bundle.Scan(filepath.Join(t.TempDir(), "never-created"))253 if err != nil || len(descriptors) != 0 || len(skipped) != 0 {254 t.Errorf("a missing directory scanned to %v, %v, %v", descriptors, skipped, err)255 }256}257258// Install is the one operation that adds to a library, and it never overwrites:259// a build lands under its own name or, being already there, lands nowhere.260func TestInstallIsIdempotentAndNeverOverwrites(t *testing.T) {261 library := t.TempDir()262 staging := t.TempDir()263 source := fixture{slug: "mars", title: "Mars", createdAt: "2026-08-01T14:48:10Z", stamp: "bb"}.build(t, staging)264265 first, err := bundle.Install(library, source)266 if err != nil {267 t.Fatal(err)268 }269 if want := "mars-20260801-bb.atlas"; filepath.Base(first.Locator) != want {270 t.Errorf("installed as %s, want %s", filepath.Base(first.Locator), want)271 }272 second, err := bundle.Install(library, source)273 if err != nil {274 t.Fatal(err)275 }276 if second.Locator != first.Locator {277 t.Errorf("a second install landed at %s", second.Locator)278 }279 // Installing the file already in place is a no-op, not a self-copy.280 third, err := bundle.Install(library, first.Locator)281 if err != nil {282 t.Fatal(err)283 }284 if third.Locator != first.Locator {285 t.Errorf("installing in place landed at %s", third.Locator)286 }287 entries, err := os.ReadDir(library)288 if err != nil {289 t.Fatal(err)290 }291 if len(entries) != 1 {292 t.Errorf("the library holds %d files after three installs", len(entries))293 }294}295296func TestInstallRefusesABrokenBundle(t *testing.T) {297 library := t.TempDir()298 staging := t.TempDir()299 // A bundle whose manifest promises more points than the payload packs.300 broken := fixture{301 slug: "mars", title: "Mars",302 worlds: []fixtureWorld{{slug: "overworld", points: 7, countsStated: true}},303 }.build(t, staging)304305 if _, err := bundle.Install(library, broken); err == nil {306 t.Error("a broken bundle was let in")307 }308 entries, err := os.ReadDir(library)309 if err != nil {310 t.Fatal(err)311 }312 if len(entries) != 0 {313 t.Errorf("a refused install left %d files behind", len(entries))314 }315}316317// TestInstallBytesKeepsInstallsPromises: the in-memory door -- an embedded318// asset, a download -- keeps every promise the path door makes. A valid bundle319// lands under its versioned name; the same bytes again are a no-op that adds320// no second file; a distinct build of the same volume lands beside the first321// rather than over it; and broken bytes are refused before anything is322// written.323func TestInstallBytesKeepsInstallsPromises(t *testing.T) {324 library := t.TempDir()325 staging := t.TempDir()326 source := fixture{slug: "earth", title: "Earth", createdAt: "2026-08-03T14:30:39Z", stamp: "aa"}.build(t, staging)327 data, err := os.ReadFile(source)328 if err != nil {329 t.Fatal(err)330 }331332 first, err := bundle.InstallBytes(library, data)333 if err != nil {334 t.Fatal(err)335 }336 if want := "earth-20260803-aa.atlas"; filepath.Base(first.Locator) != want {337 t.Errorf("installed as %s, want %s", filepath.Base(first.Locator), want)338 }339 firstInfo, err := os.Stat(first.Locator)340 if err != nil {341 t.Fatal(err)342 }343344 again, err := bundle.InstallBytes(library, data)345 if err != nil {346 t.Fatal(err)347 }348 if again.Locator != first.Locator {349 t.Errorf("a second install landed at %s", again.Locator)350 }351 afterInfo, err := os.Stat(first.Locator)352 if err != nil {353 t.Fatal(err)354 }355 if !afterInfo.ModTime().Equal(firstInfo.ModTime()) || afterInfo.Size() != firstInfo.Size() {356 t.Error("a no-op install touched the installed file")357 }358359 other := fixture{slug: "earth", title: "Earth", createdAt: "2026-08-02T00:00:00Z", stamp: "bb"}.build(t, staging)360 otherData, err := os.ReadFile(other)361 if err != nil {362 t.Fatal(err)363 }364 beside, err := bundle.InstallBytes(library, otherData)365 if err != nil {366 t.Fatal(err)367 }368 if beside.Locator == first.Locator {369 t.Error("a distinct build overwrote the installed one")370 }371 entries, err := os.ReadDir(library)372 if err != nil {373 t.Fatal(err)374 }375 if len(entries) != 2 {376 t.Errorf("the library holds %d files; two builds sit side by side", len(entries))377 }378 if held, err := os.ReadFile(first.Locator); err != nil || len(held) != int(firstInfo.Size()) {379 t.Errorf("the first build did not survive the second: %v", err)380 }381}382383func TestInstallBytesRefusesBrokenBytes(t *testing.T) {384 library := t.TempDir()385 if _, err := bundle.InstallBytes(library, []byte("not a bundle")); err == nil {386 t.Error("broken bytes were let in")387 }388 staging := t.TempDir()389 broken := fixture{390 slug: "earth", title: "Earth",391 worlds: []fixtureWorld{{slug: "overworld", points: 7, countsStated: true}},392 }.build(t, staging)393 data, err := os.ReadFile(broken)394 if err != nil {395 t.Fatal(err)396 }397 if _, err := bundle.InstallBytes(library, data); err == nil {398 t.Error("an invalid bundle was let in")399 }400 entries, err := os.ReadDir(library)401 if err != nil {402 t.Fatal(err)403 }404 if len(entries) != 0 {405 t.Errorf("a refused install left %d files behind", len(entries))406 }407}