clone

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

feat/atlas-next-horizon
atlas / internal / authoring / multipage_build_test.go
multipage_build_test.go 6.3K
1package authoring23import (4 "bytes"5 "context"6 "fmt"7 "net/http"8 "net/http/httptest"9 "os"10 "path/filepath"11 "strings"12 "sync/atomic"13 "testing"14)1516func TestMultiPageBuildIsByteIdenticalOnlineOfflineAndExactReplay(t *testing.T) {17 var calls atomic.Int6418 server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, request *http.Request) {19 calls.Add(1)20 rw.Header().Set("Content-Type", "application/geo+json")21 if request.URL.Query().Get("page") == "2" {22 _, _ = rw.Write([]byte(samplePointPage("marker-2", "Sample Lookout", 7.5, 3.5, "", 2)))23 return24 }25 next := "http://" + request.Host + request.URL.Path + "?page=2"26 _, _ = rw.Write([]byte(samplePointPage("marker-1", "Sample Marker", 4, 6, next, 2)))27 }))2829 projectDir := t.TempDir()30 copySampleProjectFiles(t, projectDir)31 projectPath := filepath.Join(projectDir, "sample-region"+ProjectExtension)32 manifest, err := os.ReadFile(projectPath)33 if err != nil {34 t.Fatal(err)35 }36 updated := strings.Replace(string(manifest), "adapter: geojson", "adapter: ogc-api-features", 1)37 updated = strings.Replace(updated, "locator: sample-region.geojson", "locator: "+server.URL, 1)38 updated = strings.Replace(updated, " mapping:\n feature-set: places", " query:\n collection: sample-points\n limit: 1\n mapping:\n feature-set: places", 1)39 updated = strings.Replace(updated, "presentation:\n", "budgets:\n features: 4\n geometry-positions: 11\n\npresentation:\n", 1)40 if err := os.WriteFile(projectPath, []byte(updated), 0o644); err != nil {41 t.Fatal(err)42 }4344 cacheDir := filepath.Join(t.TempDir(), "cache")45 var onlineEvents []Event46 online, err := Build(context.Background(), BuildOptions{ProjectPath: projectPath, CacheDir: cacheDir, LibraryDir: filepath.Join(t.TempDir(), "online"), Event: func(event Event) { onlineEvents = append(onlineEvents, event) }})47 if err != nil {48 t.Fatal(err)49 }50 selection, err := loadEvidenceSelection(online.Path)51 if err != nil {52 t.Fatal(err)53 }54 selected, ok := selectedSource(selection, "sample-places")55 if !ok || len(selected.Pages) != 2 || selected.Termination == nil || selected.Termination.Kind != TerminationNextExhausted {56 t.Fatalf("selected multi-page evidence = %+v", selected)57 }58 if !allCaptureEventsCached(onlineEvents, false) {59 t.Fatalf("first online build cache events = %+v", onlineEvents)60 }61 tampered := selection62 for index := range tampered.Captures {63 tampered.Captures[index].Pages = append([]SelectedPage(nil), tampered.Captures[index].Pages...)64 if tampered.Captures[index].Source == "sample-places" {65 tampered.Captures[index].Pages[1].Locator += "&changed=true"66 }67 }68 if _, err := replayPlanEvidence(online.Plan, tampered, OpenCache(cacheDir)); err == nil || !strings.Contains(err.Error(), "differs") {69 t.Fatalf("tampered ordered page selection = %v", err)70 }71 var repeatedEvents []Event72 repeated, err := Build(context.Background(), BuildOptions{ProjectPath: projectPath, CacheDir: cacheDir, LibraryDir: filepath.Join(t.TempDir(), "repeated"), Event: func(event Event) { repeatedEvents = append(repeatedEvents, event) }})73 if err != nil {74 t.Fatal(err)75 }76 if !allCaptureEventsCached(repeatedEvents, true) {77 t.Fatalf("repeated online build cache events = %+v", repeatedEvents)78 }79 offline, err := Build(context.Background(), BuildOptions{ProjectPath: projectPath, CacheDir: cacheDir, LibraryDir: filepath.Join(t.TempDir(), "offline"), Offline: true})80 if err != nil {81 t.Fatal(err)82 }83 replayed, err := Build(context.Background(), BuildOptions{ProjectPath: projectPath, CacheDir: cacheDir, LibraryDir: filepath.Join(t.TempDir(), "replay"), ReplayPath: online.Path})84 if err != nil {85 t.Fatal(err)86 }87 if calls.Load() != 4 {88 t.Fatalf("source calls = %d, want only the two pages from each online build", calls.Load())89 }90 want, err := os.ReadFile(online.Path)91 if err != nil {92 t.Fatal(err)93 }94 for _, path := range []string{repeated.Path, offline.Path, replayed.Path} {95 got, err := os.ReadFile(path)96 if err != nil {97 t.Fatal(err)98 }99 if !bytes.Equal(got, want) {100 t.Fatalf("build %s is not byte-identical to online build", path)101 }102 }103 tooSmall := strings.Replace(updated, "features: 4", "features: 3", 1)104 if err := os.WriteFile(projectPath, []byte(tooSmall), 0o644); err != nil {105 t.Fatal(err)106 }107 for _, options := range []BuildOptions{108 {ProjectPath: projectPath, CacheDir: cacheDir, LibraryDir: filepath.Join(t.TempDir(), "online-over")},109 {ProjectPath: projectPath, CacheDir: cacheDir, LibraryDir: filepath.Join(t.TempDir(), "offline-over"), Offline: true},110 {ProjectPath: projectPath, CacheDir: cacheDir, LibraryDir: filepath.Join(t.TempDir(), "replay-over"), ReplayPath: online.Path},111 } {112 if _, err := Build(context.Background(), options); err == nil || !strings.Contains(err.Error(), "emits 4 features, budget is 3") {113 t.Fatalf("semantic budget mode error = %v", err)114 }115 }116 server.Close()117}118119func allCaptureEventsCached(events []Event, want bool) bool {120 found := false121 for _, event := range events {122 if event.Stage != "capture" {123 continue124 }125 found = true126 if event.Cached != want {127 return false128 }129 }130 return found131}132133func samplePointPage(id, title string, x, y float64, next string, matched int) string {134 links := ""135 if next != "" {136 links = fmt.Sprintf(`,"links":[{"rel":"next","href":%q}]`, next)137 }138 return fmt.Sprintf(`{"type":"FeatureCollection","numberMatched":%d,"numberReturned":1,"features":[{"type":"Feature","id":%q,"properties":{"name":%q,"kind":"landmark","featured":true,"rank":1,"score":9.5,"badge":"AQID","category":"sample.category.marker","area":"area-1"},"geometry":{"type":"Point","coordinates":[%g,%g]}}]%s}`, matched, id, title, x, y, links)139}140141func copySampleProjectFiles(t *testing.T, destination string) {142 t.Helper()143 for _, name := range []string{144 "sample-region.atlas-project", "sample-region.geojson", "sample-region-routes.geojson",145 "sample-region-areas.geojson", "sample-region-raster.ppm", "sample-region-symbol.svg",146 } {147 data, err := os.ReadFile(filepath.Join("..", "..", "examples", name))148 if err != nil {149 t.Fatal(err)150 }151 if err := os.WriteFile(filepath.Join(destination, name), data, 0o644); err != nil {152 t.Fatal(err)153 }154 }155}156157func selectedSource(selection EvidenceSelection, source string) (SelectedCapture, bool) {158 for _, selected := range selection.Captures {159 if selected.Source == source {160 return selected, true161 }162 }163 return SelectedCapture{}, false164}