1// Package bundletest builds small real bundles for tests. A fixture is2// written through the same Writer the bundler uses, so the writer and reader3// verify each other and nothing opaque is checked into the tree.4package bundletest56import (7 "bytes"8 "encoding/json"9 "os"10 "path/filepath"11 "strconv"12 "testing"1314 "github.com/FelineStateMachine/atlas/internal/bundle"15)1617// Spec describes one fixture volume. Zero values are filled with something18// plausible, so a test that only cares about, say, the slug states the slug19// and nothing else.20type Spec struct {21 Slug string22 Title string23 CreatedAt string24 Stamp string25 Revision int26 Worlds []WorldSpec27}2829// WorldSpec describes one world of a fixture volume: a single layer two zoom30// levels deep, one point collection, and the given pins. Merged, when set, rides31// into the payload's merged block verbatim, so a test can hand a world the32// provenance ledger a real composition would have written.33type WorldSpec struct {34 Slug string35 Title string36 Pins []Pin37 Merged any38}3940// Pin is one fixture location. Description, when set, replaces the41// placeholder note; Silent leaves the pin with no note at all, which is how42// a test states that a pin is undescribed.43type Pin struct {44 Title string45 Lat float6446 Lng float6447 Description string48 Silent bool49}5051// Build writes <spec.Slug>.atlas under dir and returns its path.52func Build(t testing.TB, dir string, spec Spec) string {53 t.Helper()54 if spec.Title == "" {55 spec.Title = spec.Slug56 }57 if spec.CreatedAt == "" {58 spec.CreatedAt = "2026-01-01T00:00:00Z"59 }60 if spec.Stamp == "" {61 spec.Stamp = bundle.HashBytes([]byte(spec.Slug + spec.CreatedAt))62 }63 if len(spec.Worlds) == 0 {64 spec.Worlds = []WorldSpec{{Slug: "overworld", Pins: []Pin{{Title: "Origin"}}}}65 }6667 manifest := bundle.Manifest{68 Format: bundle.Format,69 FormatVersion: bundle.FormatVersion,70 Volume: bundle.Volume{Slug: spec.Slug, Title: spec.Title},71 Version: bundle.Version{Stamp: spec.Stamp, CreatedAt: spec.CreatedAt, Revision: spec.Revision},72 TileGrid: bundle.TileGrid{SourceZoom: 13, FirstTile: 4064, TileSize: 256, Size: 8192},73 }74 for _, m := range spec.Worlds {75 if m.Title == "" {76 m.Title = m.Slug77 }78 manifest.Worlds = append(manifest.Worlds, bundle.WorldEntry{79 Slug: m.Slug,80 Title: m.Title,81 Points: len(m.Pins),82 UpdatedAt: spec.CreatedAt,83 })84 }8586 path := filepath.Join(dir, spec.Slug+".atlas")87 file, err := os.Create(path)88 if err != nil {89 t.Fatal(err)90 }91 defer file.Close()92 writer, err := bundle.NewWriter(file, manifest)93 if err != nil {94 t.Fatal(err)95 }9697 for _, m := range spec.Worlds {98 detail := map[string]any{99 "lenses": []map[string]any{{100 "name": "Default",101 "tiles": m.Slug,102 "minZoom": 0,103 "maxZoom": 1,104 "fullZoom": 1,105 "sourceZoom": 1,106 "formats": []string{"jpg", "jpg"},107 }},108 "collections": []map[string]any{{109 "id": 1, "title": "Marker", "group": "Markers", "kind": "point",110 "iconAsset": "marker.svg", "visible": true,111 }},112 }113 if m.Merged != nil {114 detail["merged"] = m.Merged115 }116 payload, err := json.Marshal(detail)117 if err != nil {118 t.Fatal(err)119 }120 add(t, writer.AddDeflated("worlds/"+m.Slug+".json", payload))121122 locations := make([]bundle.Location, len(m.Pins))123 text := make(map[string]any)124 for index, pin := range m.Pins {125 locations[index] = bundle.Location{126 ID: int64(index + 1),127 Title: pin.Title,128 Lat: pin.Lat,129 Lng: pin.Lng,130 }131 if pin.Silent {132 continue133 }134 description := pin.Description135 if description == "" {136 description = "About " + pin.Title137 }138 text[strconv.Itoa(index+1)] = map[string]string{"d": description}139 }140 packed := bundle.PackLocations(locations)141 add(t, writer.AddStored("worlds/"+m.Slug+".bin", bytes.NewReader(packed)))142 notes, err := json.Marshal(text)143 if err != nil {144 t.Fatal(err)145 }146 add(t, writer.AddDeflated("worlds/"+m.Slug+".text", notes))147148 for _, tile := range []string{"0/0/0.jpg", "1/0/0.jpg"} {149 raster := []byte("jpeg bytes of " + m.Slug + "/" + tile)150 add(t, writer.AddStored("tiles/"+m.Slug+"/"+tile, bytes.NewReader(raster)))151 }152 }153 add(t, writer.AddDeflated("icons/marker.svg", []byte("<svg/>")))154155 if err := writer.Close(); err != nil {156 t.Fatal(err)157 }158 if err := file.Close(); err != nil {159 t.Fatal(err)160 }161 return path162}163164func add(t testing.TB, err error) {165 t.Helper()166 if err != nil {167 t.Fatal(err)168 }169}