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 / capture_set_test.go
capture_set_test.go 8.0K
1package authoring23import (4 "context"5 "fmt"6 "net/http"7 "net/http/httptest"8 "os"9 "path/filepath"10 "strings"11 "sync/atomic"12 "testing"13 "time"14)1516func TestCaptureSetFollowsPagesAndReplaysOfflineAtomically(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 switch request.URL.Query().Get("page") {22 case "2":23 _, _ = rw.Write([]byte(`{"type":"FeatureCollection","numberMatched":2,"numberReturned":1,"features":[{"type":"Feature","id":"second","properties":{},"geometry":null}]}`))24 default:25 _, _ = fmt.Fprintf(rw, `{"type":"FeatureCollection","numberMatched":2,"numberReturned":1,"features":[{"type":"Feature","id":"first","properties":{},"geometry":null}],"links":[{"rel":"next","href":%q}]}`, serverURL(request)+"?page=2")26 }27 }))28 defer server.Close()2930 cache := OpenCache(t.TempDir())31 cache.now = func() time.Time { return time.Date(2026, 8, 8, 12, 0, 0, 0, time.UTC) }32 request := featureSetRequest("sample-roads", "ogc-api-features", server.URL)33 registry := defaultAdapterRegistry()34 set, cached, err := cache.AcquireSet(context.Background(), []Request{request}, registry, CaptureSetOptions{MaxRequests: 8})35 if err != nil {36 t.Fatal(err)37 }38 if cached || len(set.Roots) != 1 || len(set.Roots[0].Pages) != 2 {39 t.Fatalf("online set = cached %v, %+v", cached, set)40 }41 if set.Roots[0].Termination.Kind != TerminationNextExhausted || set.Roots[0].Termination.Returned != 2 || set.Roots[0].Termination.Matched == nil || *set.Roots[0].Termination.Matched != 2 {42 t.Fatalf("termination = %+v", set.Roots[0].Termination)43 }44 if calls.Load() != 2 {45 t.Fatalf("online calls = %d, want 2", calls.Load())46 }4748 offline, cached, err := cache.AcquireSet(context.Background(), []Request{request}, registry, CaptureSetOptions{Offline: true, MaxRequests: 8})49 if err != nil {50 t.Fatal(err)51 }52 if !cached || offline.Digest != set.Digest || calls.Load() != 2 {53 t.Fatalf("offline set = cached %v digest %s calls %d", cached, offline.Digest, calls.Load())54 }55 selected, err := cache.SelectSet(set.ID, set.Digest)56 if err != nil || selected.Digest != set.Digest {57 t.Fatalf("select exact set = %+v, %v", selected, err)58 }59}6061func TestCaptureSetRefusesIncompletePaginationWithoutPublishingASet(t *testing.T) {62 server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, request *http.Request) {63 rw.Header().Set("Content-Type", "application/geo+json")64 if request.URL.Query().Get("page") == "2" {65 _, _ = rw.Write([]byte(`{"type":"FeatureCollection","numberMatched":3,"numberReturned":1,"features":[{"type":"Feature","id":"second","properties":{},"geometry":null}]}`))66 return67 }68 _, _ = fmt.Fprintf(rw, `{"type":"FeatureCollection","numberMatched":3,"numberReturned":1,"features":[{"type":"Feature","id":"first","properties":{},"geometry":null}],"links":[{"rel":"next","href":%q}]}`, serverURL(request)+"?page=2")69 }))70 defer server.Close()7172 cache := OpenCache(t.TempDir())73 request := featureSetRequest("sample-roads", "ogc-api-features", server.URL)74 setID := captureSetID([]Request{request})75 _, _, err := cache.AcquireSet(context.Background(), []Request{request}, defaultAdapterRegistry(), CaptureSetOptions{MaxRequests: 8})76 if err == nil || !strings.Contains(err.Error(), "incomplete") {77 t.Fatalf("incomplete set = %v", err)78 }79 if _, err := cache.LatestSet(setID); !os.IsNotExist(err) {80 t.Fatalf("partial set became selectable: %v", err)81 }82 if _, err := os.Stat(filepath.Join(cache.Root(), "capture-sets")); !os.IsNotExist(err) {83 t.Fatalf("partial set manifest residue = %v", err)84 }85}8687func TestCaptureSetRefusesContinuationCycles(t *testing.T) {88 server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, request *http.Request) {89 rw.Header().Set("Content-Type", "application/geo+json")90 _, _ = fmt.Fprintf(rw, `{"type":"FeatureCollection","numberReturned":0,"features":[],"links":[{"rel":"next","href":%q}]}`, serverURL(request))91 }))92 defer server.Close()9394 request := featureSetRequest("sample-roads", "ogc-api-features", server.URL)95 _, _, err := OpenCache(t.TempDir()).AcquireSet(context.Background(), []Request{request}, defaultAdapterRegistry(), CaptureSetOptions{MaxRequests: 8})96 if err == nil || !strings.Contains(err.Error(), "cycle") {97 t.Fatalf("continuation cycle = %v", err)98 }99}100101func TestCaptureSetDoesNotPublishWhenAnyPlannedRootFails(t *testing.T) {102 root := t.TempDir()103 firstPath := filepath.Join(root, "first.bin")104 if err := os.WriteFile(firstPath, []byte("first"), 0o644); err != nil {105 t.Fatal(err)106 }107 requests := []Request{108 assetSetRequest("sample-assets", "first", firstPath),109 assetSetRequest("sample-assets", "missing", filepath.Join(root, "missing.bin")),110 }111 cache := OpenCache(filepath.Join(root, "cache"))112 setID := captureSetID(requests)113 _, _, err := cache.AcquireSet(context.Background(), requests, defaultAdapterRegistry(), CaptureSetOptions{MaxRequests: 8})114 if err == nil || !strings.Contains(err.Error(), "missing") {115 t.Fatalf("failed multi-root set = %v", err)116 }117 if _, err := cache.LatestSet(setID); !os.IsNotExist(err) {118 t.Fatalf("partial multi-root set became selectable: %v", err)119 }120}121122func TestPlanReportsCachedOnlyAfterTheWholeSetIsPublished(t *testing.T) {123 root := t.TempDir()124 source := filepath.Join(root, "sample-region.geojson")125 if err := os.WriteFile(source, []byte(`{"type":"FeatureCollection","features":[]}`), 0o644); err != nil {126 t.Fatal(err)127 }128 cache := OpenCache(filepath.Join(root, "cache"))129 request := featureSetRequest("sample-features", "geojson", source)130 plan := Plan{Budgets: defaultBuildBudgets, Requests: []PlannedRequest{{Request: request}}}131 if _, _, err := cache.Acquire(context.Background(), request, false); err != nil {132 t.Fatal(err)133 }134 markCachedCaptureSets(&plan, cache)135 if plan.Cached != 0 || plan.Requests[0].Cached {136 t.Fatal("an individually cached page was reported as a complete set")137 }138 if _, _, err := cache.AcquireSet(context.Background(), []Request{request}, defaultAdapterRegistry(), CaptureSetOptions{MaxRequests: 8}); err != nil {139 t.Fatal(err)140 }141 markCachedCaptureSets(&plan, cache)142 if plan.Cached != 1 || !plan.Requests[0].Cached {143 t.Fatal("published capture set was not reported as cached")144 }145}146147func TestCaptureSetDigestIsIndependentOfPlannedRootOrder(t *testing.T) {148 root := t.TempDir()149 paths := []string{filepath.Join(root, "first.bin"), filepath.Join(root, "second.bin")}150 for index, path := range paths {151 if err := os.WriteFile(path, []byte{byte(index + 1)}, 0o644); err != nil {152 t.Fatal(err)153 }154 }155 requests := []Request{156 assetSetRequest("sample-assets", "first", paths[0]),157 assetSetRequest("sample-assets", "second", paths[1]),158 }159 cache := OpenCache(filepath.Join(root, "cache"))160 first, cached, err := cache.AcquireSet(context.Background(), []Request{requests[1], requests[0]}, defaultAdapterRegistry(), CaptureSetOptions{MaxRequests: 8})161 if err != nil || cached {162 t.Fatalf("first ordered set = cached %v, %v", cached, err)163 }164 second, cached, err := cache.AcquireSet(context.Background(), requests, defaultAdapterRegistry(), CaptureSetOptions{MaxRequests: 8})165 if err != nil || !cached {166 t.Fatalf("reordered set = cached %v, %v", cached, err)167 }168 if first.ID != second.ID || first.Digest != second.Digest || first.Roots[0].Acquisition >= first.Roots[1].Acquisition {169 t.Fatalf("capture set ordering differs: first %+v second %+v", first, second)170 }171}172173func featureSetRequest(source, adapter, locator string) Request {174 request := Request{175 Kind: RequestFeatures, Source: source, Adapter: adapter, Locator: locator,176 IdentityLocator: locator, MediaType: "application/geo+json", MaxBytes: 1 << 20,177 }178 finalizeRequest(&request)179 return request180}181182func assetSetRequest(source, asset, locator string) Request {183 request := Request{184 Kind: RequestAsset, Source: source, Adapter: "asset-file", Asset: asset,185 Locator: locator, IdentityLocator: locator, MediaType: "application/octet-stream", MaxBytes: 1 << 20,186 }187 finalizeRequest(&request)188 return request189}190191func serverURL(request *http.Request) string {192 return "http://" + request.Host193}