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 / spec / gen / gen_test.go
gen_test.go 5.9K
1package main23import (4 "bytes"5 "fmt"6 "os"7 "path/filepath"8 "strings"9 "testing"10)1112// root is the repository, found from the package directory the test runs in.13func root(t *testing.T) string {14 t.Helper()15 dir, err := repoRoot("")16 if err != nil {17 t.Fatal(err)18 }19 return dir20}2122// TestTheCommittedArtifactsAreWhatTheSpecSays is the gate that replaced the23// prose twin's agreement test. The registry, the TypeScript key constants and24// docs/semconv/REGISTRY.md used to be three hand-written statements of one25// vocabulary, held together by a test that read the document with a regular26// expression. They are now one statement and two derivations, and the thing27// worth checking is that the derivations are current: a spec edit that was not28// regenerated is a red gate rather than a quiet drift.29func TestTheCommittedArtifactsAreWhatTheSpecSays(t *testing.T) {30 dir := root(t)31 artifacts, err := build(dir)32 if err != nil {33 t.Fatal(err)34 }35 if len(artifacts) != 3 {36 t.Fatalf("the generator produced %d artifacts, want 3", len(artifacts))37 }38 for _, path := range sorted(artifacts) {39 committed, err := os.ReadFile(filepath.Join(dir, filepath.FromSlash(path)))40 if err != nil {41 t.Errorf("%s: %v", path, err)42 continue43 }44 if !bytes.Equal(committed, artifacts[path]) {45 t.Errorf("%s is out of date with %s — run `go generate ./format/semconv`\n%s",46 path, specFile, firstDifference(committed, artifacts[path]))47 }48 }49}5051// TestGenerationIsByteStable is the property the artifacts' committedness52// stands on. Rendering is a pure function of the spec -- every listing in the53// spec's own order or sorted, no map ranged over -- so two runs must produce54// identical bytes. A generator that emitted a map's iteration order would pass55// the test above on the machine that last ran it and fail on every other.56func TestGenerationIsByteStable(t *testing.T) {57 dir := root(t)58 first, err := build(dir)59 if err != nil {60 t.Fatal(err)61 }62 for run := range 4 {63 again, err := build(dir)64 if err != nil {65 t.Fatal(err)66 }67 for _, path := range sorted(first) {68 if !bytes.Equal(first[path], again[path]) {69 t.Fatalf("run %d produced different bytes for %s", run+2, path)70 }71 }72 }73}7475// Every artifact says it is generated, in its own comment syntax, on its first76// line. The Go form is the one `go` tooling recognises; the others are for a77// person who opened the file to edit it.78func TestEveryArtifactAnnouncesItself(t *testing.T) {79 artifacts, err := build(root(t))80 if err != nil {81 t.Fatal(err)82 }83 for _, path := range sorted(artifacts) {84 first, _, _ := strings.Cut(string(artifacts[path]), "\n")85 if !strings.Contains(first, banner) {86 t.Errorf("%s opens with %q, which does not carry the generated banner", path, first)87 }88 }89}9091// A spec the generator would happily render into something nobody can trust is92// refused before anything is written. These are the faults worth naming: they93// are the ones an ordinary edit makes.94func TestTheSpecIsHeldToItsOwnShape(t *testing.T) {95 valid := func() spec {96 return spec{97 Version: 2,98 Namespace: "atlas.",99 Entities: []entity{{Name: "world", Const: "EntityWorld"}},100 Stabilities: []named{{Name: "stable", Const: "Stable"}},101 Keys: []key{{102 Key: "atlas.geometry.body", Const: "KeyGeometryBody",103 Entity: "world", Stability: "stable", Check: "slug",104 Doc: "a body", ValuesMD: "slug", Meaning: "the body pictured",105 }},106 }107 }108 reference := valid()109 if err := reference.validate(); err != nil {110 t.Fatalf("the reference spec is refused: %v", err)111 }112113 cases := []struct {114 name string115 break_ func(*spec)116 mention string117 }{118 {"a key outside the namespace", func(s *spec) {119 s.Keys[0].Key = "vendor.thing"120 }, "namespace"},121 {"an entity nobody declared", func(s *spec) {122 s.Keys[0].Entity = "zone"123 }, "not a declared entity"},124 {"a stability tier nobody declared", func(s *spec) {125 s.Keys[0].Stability = "settled"126 }, "not a declared tier"},127 {"a checker nobody wrote", func(s *spec) {128 s.Keys[0].Check = "looksRight"129 }, "not a checker"},130 {"an enum with no vocabulary", func(s *spec) {131 s.Keys[0].Check = "enum"132 }, "vocabulary"},133 {"a vocabulary on something that is not an enum", func(s *spec) {134 s.Keys[0].Values = []vocab{{Const: "Whatever", Value: "whatever"}}135 }, "vocabulary"},136 {"numbers with no arity", func(s *spec) {137 s.Keys[0].Check = "numbers"138 s.Keys[0].Values = nil139 }, "arity"},140 {"an arity on something that counts nothing", func(s *spec) {141 s.Keys[0].Arity = 4142 }, "arity"},143 {"a key documented nowhere", func(s *spec) {144 s.Keys[0].Meaning = ""145 }, "no documented values or meaning"},146 {"one key twice", func(s *spec) {147 s.Keys = append(s.Keys, s.Keys[0])148 }, "declared twice"},149 {"a policy key colliding with a registered one", func(s *spec) {150 s.PolicyKeys = []key{{Key: "atlas.note.text", Const: "KeyGeometryBody"}}151 }, "constant"},152 {"a first key with no comment for the rest to join", func(s *spec) {153 s.Keys[0].Doc = ""154 }, "doc comment"},155 }156 for _, test := range cases {157 t.Run(test.name, func(t *testing.T) {158 s := valid()159 test.break_(&s)160 err := s.validate()161 if err == nil || !strings.Contains(err.Error(), test.mention) {162 t.Errorf("validate = %v, wanted mention of %q", err, test.mention)163 }164 })165 }166}167168// firstDifference points at where two renderings part company, so a failure169// reads as a diff rather than as two walls of generated text.170func firstDifference(have, want []byte) string {171 haveLines := strings.Split(string(have), "\n")172 wantLines := strings.Split(string(want), "\n")173 for index := range max(len(haveLines), len(wantLines)) {174 h, w := at(haveLines, index), at(wantLines, index)175 if h != w {176 return fmt.Sprintf(" line %d\n committed: %s\n generated: %s", index+1, h, w)177 }178 }179 return " the files differ only in their trailing bytes"180}181182func at(lines []string, index int) string {183 if index < len(lines) {184 return lines[index]185 }186 return "(end of file)"187}