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 / stamp_test.go
stamp_test.go 6.0K
1package bundle_test23import (4 "reflect"5 "testing"67 "github.com/FelineStateMachine/atlas/format/bundle"8)910// The stamp is the determinism invariant made concrete, so its arithmetic is11// pinned to literal digests rather than to a description of itself. A change12// to the algorithm renames every bundle in every library, which is a decision13// somebody has to make on purpose.14func TestStampVectors(t *testing.T) {15 cases := []struct {16 name string17 parts [][2]string18 want string19 }{20 {21 name: "nothing added",22 want: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",23 },24 {25 name: "one part",26 parts: [][2]string{{"atlas.json", "aa"}},27 want: "195c654d74045824d198e3e92876b6cf83a9cac67f12d860ddcc41e73c469d2b",28 },29 {30 name: "a manifest, a payload, and a pyramid",31 parts: [][2]string{32 {"atlas.json", bundle.HashBytes([]byte("manifest"))},33 {"worlds/overworld.json", bundle.HashBytes([]byte("payload"))},34 {"tiles/overworld", "deadbeef"},35 },36 want: "bfda76a0377d98a607c0e4c70f365457cb1f00a921b7026c246b23c58f11151e",37 },38 }39 for _, test := range cases {40 t.Run(test.name, func(t *testing.T) {41 var stamp bundle.Stamp42 for _, part := range test.parts {43 stamp.Add(part[0], part[1])44 }45 if got := stamp.Sum(); got != test.want {46 t.Errorf("Sum = %s, want %s", got, test.want)47 }48 })49 }50}5152func TestHashBytesIsSHA256Hex(t *testing.T) {53 cases := map[string]string{54 "": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",55 "abc": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",56 }57 for input, want := range cases {58 if got := bundle.HashBytes([]byte(input)); got != want {59 t.Errorf("HashBytes(%q) = %s, want %s", input, got, want)60 }61 }62}6364// Order independence is what lets a producer add parts as it happens to build65// them -- worlds in whatever order, pyramids from a map -- and still agree66// with a producer that built them in another order.67func TestStampIsOrderIndependent(t *testing.T) {68 parts := [][2]string{69 {"atlas.json", "1"},70 {"worlds/a.json", "2"},71 {"worlds/b.json", "3"},72 {"tiles/a", "4"},73 {"icons/marker.svg", "5"},74 }75 var forward bundle.Stamp76 for _, part := range parts {77 forward.Add(part[0], part[1])78 }79 var backward bundle.Stamp80 for index := len(parts) - 1; index >= 0; index-- {81 backward.Add(parts[index][0], parts[index][1])82 }83 if forward.Sum() != backward.Sum() {84 t.Errorf("adding the same parts in reverse gave %s, not %s", backward.Sum(), forward.Sum())85 }86 if !reflect.DeepEqual(forward.Parts(), backward.Parts()) {87 t.Errorf("Parts disagree: %v and %v", forward.Parts(), backward.Parts())88 }89 // Summing does not consume, so a producer may sum, add more, and sum again.90 before := forward.Sum()91 if forward.Sum() != before {92 t.Error("summing twice gave two answers")93 }94}9596// The name of a part is as load-bearing as its hash: two parts holding the97// same bytes under different names are a different bundle, and a renamed part98// is a rebuild.99func TestStampSeparatesNameFromHash(t *testing.T) {100 var named, renamed bundle.Stamp101 named.Add("worlds/a.json", "ff")102 renamed.Add("worlds/b.json", "ff")103 if named.Sum() == renamed.Sum() {104 t.Error("renaming a part did not change the stamp")105 }106107}108109// The separator is a single space, which is only unambiguous because a part110// name holds none. The collision is pinned here rather than fixed: fixing it111// would restamp every bundle in every library, and the constraint costs112// nothing to keep. A producer naming a part with a space is the bug.113func TestStampPartNamesMustNotHoldSpaces(t *testing.T) {114 var left, right bundle.Stamp115 left.Add("a b", "c")116 right.Add("a", "b c")117 if left.Sum() != right.Sum() {118 t.Error("the space separator has stopped being ambiguous; docs/format.md says it is")119 }120121 // Under the constraint, names and hashes stay distinguishable.122 var named, hashed bundle.Stamp123 named.Add("worlds/a.json", "ff00")124 hashed.Add("worlds/a.jsonff00", "")125 if named.Sum() == hashed.Sum() {126 t.Error("a space-free name collided with its own hash")127 }128}129130func TestShortStamp(t *testing.T) {131 cases := map[string]string{132 "": "",133 "abc": "abc",134 "abcdefghijkl": "abcdefghijkl",135 "abcdefghijklmnopqrst": "abcdefghijkl",136 }137 for input, want := range cases {138 if got := bundle.ShortStamp(input); got != want {139 t.Errorf("ShortStamp(%q) = %q, want %q", input, got, want)140 }141 }142}143144func TestCaptureDay(t *testing.T) {145 cases := map[string]string{146 "2026-08-01T09:30:00Z": "20260801",147 "2026-08-01T20:13:08.883648Z": "20260801",148 "2026-08-01": "20260801",149 "20260801": "20260801",150 "2026-08": "202608",151 "": "",152 "never": "",153 }154 for input, want := range cases {155 if got := bundle.CaptureDay(input); got != want {156 t.Errorf("CaptureDay(%q) = %q, want %q", input, got, want)157 }158 }159}160161// The file name is derived from nothing but the manifest, so the same build162// carries the same name wherever and whenever it is written.163func TestVersionedFileName(t *testing.T) {164 stamp := "abababababababababababababababababababababababababababababababab"165 cases := []struct {166 name string167 slug string168 createdAt string169 stamp string170 want string171 }{172 {"an ordinary build", "fixture", "2026-08-01T09:30:00Z", stamp, "fixture-20260801-abababababab.atlas"},173 {"a fractional capture time", "mars", "2026-08-01T14:48:10.486177Z", stamp, "mars-20260801-abababababab.atlas"},174 {"a short stamp", "fixture", "2026-08-01T09:30:00Z", "abc", "fixture-20260801-abc.atlas"},175 {"no capture time at all", "fixture", "", stamp, "fixture-abababababab.atlas"},176 }177 for _, test := range cases {178 t.Run(test.name, func(t *testing.T) {179 manifest := bundle.Manifest{180 Volume: bundle.Volume{Slug: test.slug},181 Version: bundle.Version{Stamp: test.stamp, CreatedAt: test.createdAt},182 }183 if got := bundle.VersionedFileName(manifest); got != test.want {184 t.Errorf("name = %q, want %q", got, test.want)185 }186 })187 }188}