1package semconv_test23import (4 "math"5 "strings"6 "testing"78 "github.com/FelineStateMachine/atlas/format/semconv"9)1011func TestValidateAcceptsTheVocabulary(t *testing.T) {12 cases := []struct {13 name string14 entity semconv.Entity15 attrs map[string]string16 }{17 {"a point collection", semconv.EntityCollection, map[string]string{semconv.KeyRenderAs: "pin"}},18 {"a text collection with a standard icon", semconv.EntityCollection, map[string]string{19 semconv.KeyRenderAs: "text", semconv.KeyIconStd: "maki/mountain"}},20 {"a glyph collection with a merge identity", semconv.EntityCollection, map[string]string{21 semconv.KeyIconKind: "glyph", semconv.KeyCollectionKey: "ripperdoc"}},22 {"a declared kind", semconv.EntityCollection, map[string]string{semconv.KeyGeometryKind: "point"}},23 {"a path with a stroke", semconv.EntityCollection, map[string]string{24 semconv.KeyGeometryKind: "path", semconv.KeyStrokeWidthPx: "12"}},25 {"an area with a label policy", semconv.EntityCollection, map[string]string{26 semconv.KeyGeometryKind: "area", semconv.KeyLabelPolicy: "quiet"}},27 {"a sphere fully declared", semconv.EntityWorld, map[string]string{28 semconv.KeyGeometrySurface: "sphere",29 semconv.KeyGeometryProjection: "equirect",30 semconv.KeyGeometryEquirectPx: "0,0,8192,4096",31 semconv.KeyGeometryEquirectDeg: "-180,90,180,-90",32 semconv.KeyGeometryBody: "mars",33 semconv.KeyGeometryRadiusKM: "3389.5",34 }},35 {"a mercator cut", semconv.EntityWorld, map[string]string{36 semconv.KeyGeometrySurface: "plane",37 semconv.KeyGeometryMercatorPx: "0,0,8192,8192",38 semconv.KeyGeometryMercatorDeg: "-105.19,39.98,-104.96,39.80",39 }},40 {"a marker outset", semconv.EntityWorld, map[string]string{semconv.KeyIconOutset: "dark"}},41 {"published coordinates", semconv.EntityFeature, map[string]string{42 semconv.KeyGeoLat: "-42.4301", semconv.KeyGeoLon: "70.5025"}},43 {"a watershed", semconv.EntityFeature, map[string]string{semconv.KeyHydroHUC12: "170703010101"}},44 {"a foreign namespace", semconv.EntityFeature, map[string]string{"vendor.thing": "whatever"}},45 // Empty is always fine: conventions are declared, never demanded.46 {"nothing at all", semconv.EntityCollection, nil},47 }48 for _, test := range cases {49 t.Run(test.name, func(t *testing.T) {50 if err := semconv.Validate(test.entity, test.attrs); err != nil {51 t.Errorf("%s %v refused: %v", test.entity, test.attrs, err)52 }53 })54 }55}5657func TestValidateRefusesWhatTheRegistryDoesNotKnow(t *testing.T) {58 cases := []struct {59 name string60 entity semconv.Entity61 attrs map[string]string62 mention string63 }{64 {"an invented key", semconv.EntityCollection, map[string]string{"atlas.render.like": "pin"}, "not registered"},65 {"a value outside the vocabulary", semconv.EntityCollection, map[string]string{semconv.KeyRenderAs: "zone"}, "not one of"},66 {"a collection key on a world", semconv.EntityWorld, map[string]string{semconv.KeyRenderAs: "pin"}, "attaches to"},67 {"a bare icon name", semconv.EntityCollection, map[string]string{semconv.KeyIconStd: "mountain"}, "set/name"},68 {"an icon name in caps", semconv.EntityCollection, map[string]string{semconv.KeyIconStd: "Maki/Mountain"}, "slug"},69 {"a geometry kind that is none", semconv.EntityCollection, map[string]string{semconv.KeyGeometryKind: "polygon"}, "not one of"},70 {"a label policy that is none", semconv.EntityCollection, map[string]string{semconv.KeyLabelPolicy: "loud"}, "not one of"},71 {"a collection key on a feature", semconv.EntityFeature, map[string]string{semconv.KeyGeometryKind: "area"}, "attaches to"},72 {"a stroke on a feature", semconv.EntityFeature, map[string]string{semconv.KeyStrokeWidthPx: "12"}, "attaches to"},73 {"a stroke of zero", semconv.EntityCollection, map[string]string{semconv.KeyStrokeWidthPx: "0"}, "positive"},74 {"a three-number window", semconv.EntityWorld, map[string]string{semconv.KeyGeometryEquirectPx: "0,0,8192"}, "4"},75 {"a radius in words", semconv.EntityWorld, map[string]string{semconv.KeyGeometryRadiusKM: "big"}, "number"},76 {"a latitude in words", semconv.EntityFeature, map[string]string{semconv.KeyGeoLat: "north"}, "number"},77 {"a short watershed", semconv.EntityFeature, map[string]string{semconv.KeyHydroHUC12: "17070301"}, "twelve digits"},78 {"a watershed on a collection", semconv.EntityCollection, map[string]string{semconv.KeyHydroHUC12: "170703010101"}, "attaches to"},79 // The policy name never rides a payload.80 {"a policy name", semconv.EntityFeature, map[string]string{semconv.KeyNoteText: "words"}, "not registered"},81 }82 for _, test := range cases {83 t.Run(test.name, func(t *testing.T) {84 err := semconv.Validate(test.entity, test.attrs)85 if err == nil || !strings.Contains(err.Error(), test.mention) {86 t.Errorf("%s %v: %v, wanted mention of %q", test.entity, test.attrs, err, test.mention)87 }88 })89 }90}9192// A reader that meets a key from a newer vocabulary skips it; it never has a93// reason to refuse the bundle carrying it. This is the lenient half of the94// asymmetry, stated as a test so it cannot quietly become strict.95func TestReadersAreLenientAboutUnknownKeys(t *testing.T) {96 if _, known := semconv.EntityOf("atlas.something.new"); known {97 t.Error("an unregistered key reports as known")98 }99 if _, known := semconv.StabilityOf("atlas.something.new"); known {100 t.Error("an unregistered key reports a stability")101 }102 for _, key := range semconv.Keys() {103 if _, known := semconv.EntityOf(key); !known {104 t.Errorf("%s is listed but unknown", key)105 }106 if stability, known := semconv.StabilityOf(key); !known ||107 (stability != semconv.Stable && stability != semconv.Experimental) {108 t.Errorf("%s carries stability %q", key, stability)109 }110 }111}112113func TestKeysAreSortedAndComplete(t *testing.T) {114 keys := semconv.Keys()115 if len(keys) != 19 {116 t.Errorf("the registry holds %d keys, want 19", len(keys))117 }118 for index := 1; index < len(keys); index++ {119 if keys[index-1] >= keys[index] {120 t.Fatalf("Keys is unsorted at %d: %q then %q", index, keys[index-1], keys[index])121 }122 }123 for _, key := range keys {124 if !strings.HasPrefix(key, semconv.Namespace) {125 t.Errorf("%s is registered outside the namespace", key)126 }127 }128}129130func TestRenderAsPrefersAttributesOverLegacy(t *testing.T) {131 cases := []struct {132 attrs map[string]string133 legacy string134 want string135 }{136 {map[string]string{semconv.KeyRenderAs: "text"}, "markers", "text"},137 {map[string]string{semconv.KeyRenderAs: "pin"}, "text", "pin"},138 {nil, "text", "text"},139 {nil, "markers", "pin"},140 {nil, "", "pin"},141 }142 for _, test := range cases {143 if got := semconv.RenderAs(test.attrs, test.legacy); got != test.want {144 t.Errorf("RenderAs(%v, %q) = %q, want %q", test.attrs, test.legacy, got, test.want)145 }146 }147}148149func TestLabelPolicyReadsTheCuration(t *testing.T) {150 cases := []struct {151 kind string152 attrs map[string]string153 want string154 }{155 {semconv.GeometryArea, nil, semconv.LabelAlways},156 {semconv.GeometryArea, map[string]string{semconv.KeyLabelPolicy: "quiet"}, semconv.LabelQuiet},157 {semconv.GeometryPath, nil, semconv.LabelQuiet},158 {semconv.GeometryPoint, nil, semconv.LabelQuiet},159 // A point collection drawn as text is a producer pinning names on,160 // so its names speak unasked; one drawn as pins waits to be asked.161 {semconv.GeometryPoint, map[string]string{semconv.KeyRenderAs: semconv.RenderAsText}, semconv.LabelAlways},162 {semconv.GeometryPoint, map[string]string{semconv.KeyRenderAs: semconv.RenderAsPin}, semconv.LabelQuiet},163 // A declared policy is the answer whatever the kind, including164 // against the text default.165 {semconv.GeometryPoint, map[string]string{166 semconv.KeyRenderAs: semconv.RenderAsText,167 semconv.KeyLabelPolicy: semconv.LabelQuiet,168 }, semconv.LabelQuiet},169 {semconv.GeometryPath, map[string]string{semconv.KeyLabelPolicy: semconv.LabelAlways}, semconv.LabelAlways},170 }171 for _, test := range cases {172 if got := semconv.LabelPolicy(test.kind, test.attrs); got != test.want {173 t.Errorf("LabelPolicy(%q, %v) = %q, want %q", test.kind, test.attrs, got, test.want)174 }175 }176}177178// The Mars mapping drives both ways: the declared window is the top half of179// the 8192-pixel world, and every corner and famous place must survive there180// and back within floating-point noise.181func TestEquirectRoundTrip(t *testing.T) {182 mapping, err := semconv.ParseEquirect("0,0,8192,4096", "-180,90,180,-90")183 if err != nil {184 t.Fatal(err)185 }186 places := []struct {187 name string188 lat, lon float64189 }{190 {"northwest corner", 90, -180},191 {"northeast corner", 90, 180},192 {"southwest corner", -90, -180},193 {"southeast corner", -90, 180},194 {"the origin", 0, 0},195 {"Olympus Mons", 18.6528, -133.8025},196 {"Hellas Planitia", -42.4301, 70.5025},197 {"Valles Marineris", -14.0059, -58.5877},198 }199 for _, place := range places {200 x, y := mapping.Apply(place.lat, place.lon)201 lat, lon := mapping.Invert(x, y)202 if math.Abs(lat-place.lat) > 1e-9 || math.Abs(lon-place.lon) > 1e-9 {203 t.Errorf("%s round-trips to %v,%v via %v,%v", place.name, lat, lon, x, y)204 }205 }206 // The anchor the whole design stands on: Olympus Mons lands where the207 // linear mapping says it should, with no hidden fudge in between.208 x, y := mapping.Apply(18.6528, -133.8025)209 wantX := (-133.8025 + 180) / 360 * 8192210 wantY := (90 - 18.6528) / 180 * 4096211 if math.Abs(x-wantX) > 1e-9 || math.Abs(y-wantY) > 1e-9 {212 t.Errorf("Olympus Mons at %v,%v, want %v,%v", x, y, wantX, wantY)213 }214}215216func TestParseEquirectRefusesEmptyWindows(t *testing.T) {217 cases := []struct {218 name string219 px, deg string220 }{221 {"a window with no width", "0,0,0,4096", "-180,90,180,-90"},222 {"a window with no height", "0,0,8192,0", "-180,90,180,-90"},223 {"a ground with no span", "0,0,8192,4096", "-180,90,-180,-90"},224 {"a ground with no height", "0,0,8192,4096", "-180,90,180,90"},225 {"three numbers", "0,0,8192", "-180,90,180,-90"},226 }227 for _, test := range cases {228 t.Run(test.name, func(t *testing.T) {229 if _, err := semconv.ParseEquirect(test.px, test.deg); err == nil {230 t.Errorf("ParseEquirect(%q, %q) accepted", test.px, test.deg)231 }232 })233 }234}235236func TestEquirectOfReadsAWorldsAttributes(t *testing.T) {237 if _, declared, err := semconv.EquirectOf(map[string]string{semconv.KeyGeometrySurface: "plane"}); declared || err != nil {238 t.Errorf("a plane declares a mapping: declared=%v err=%v", declared, err)239 }240 mapping, declared, err := semconv.EquirectOf(map[string]string{241 semconv.KeyGeometryEquirectPx: "0,0,8192,4096",242 semconv.KeyGeometryEquirectDeg: "-180,90,180,-90",243 })244 if !declared || err != nil {245 t.Fatalf("a declared mapping did not read: declared=%v err=%v", declared, err)246 }247 if mapping.W != 8192 || mapping.North != 90 {248 t.Errorf("mapping read as %+v", mapping)249 }250 if _, declared, err := semconv.EquirectOf(map[string]string{251 semconv.KeyGeometryEquirectPx: "0,0,8192,4096",252 }); !declared || err == nil {253 t.Errorf("half a mapping passed: declared=%v err=%v", declared, err)254 }255}256257// The registry used to have a prose twin -- docs/semconv/REGISTRY.md, written258// by hand -- and a test here that read the document with a regular expression259// and held the two to the same list of keys, entities and stability tiers.260// Both are now cut from spec/registry.yaml, along with the TypeScript lanes'261// key constants, so there is nothing left for two twins to disagree about.262// What replaced the agreement test is spec/gen's TestTheCommittedArtifactsAre263// WhatTheSpecSays: the committed artifacts must equal what a generation run264// produces. The check moved from "do these two say the same thing" to "is the265// derivation current", which is the only question a generated file can be266// wrong about.