A caller names a value the seat definition gives, such as the removal reason retention, and never writes it by hand. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
145 lines
5.5 KiB
Go
145 lines
5.5 KiB
Go
package seatgen
|
|
|
|
import (
|
|
"encoding/json"
|
|
"go/parser"
|
|
"go/token"
|
|
re "regexp"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/seatdef"
|
|
)
|
|
|
|
// TestEachSeatDefinedWholeIsAGoModuleOfItsOwn is ADR 0625's generator check (§1, §2, §3): the seat set is one package in
|
|
// the root Go module, each seat defined whole is a Go module of its own holding its package alone, the parts it
|
|
// includes are written into it, and no generated Go file imports anything outside the standard library.
|
|
func TestEachSeatDefinedWholeIsAGoModuleOfItsOwn(t *testing.T) {
|
|
m, err := Load("testdata/contracts", "example.org/contracts/go", "@example/contracts")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
files, err := Generate(m)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
mods := map[string]string{}
|
|
for p, b := range files {
|
|
if strings.HasPrefix(p, "go/") && strings.HasSuffix(p, "go.mod") {
|
|
mods[strings.TrimSuffix(p, "go.mod")] = strings.SplitN(string(b), "\n", 2)[0]
|
|
}
|
|
}
|
|
want := map[string]string{
|
|
"go/": "module example.org/contracts/go",
|
|
"go/seats/aregistry/": "module example.org/contracts/go/seats/aregistry",
|
|
"go/seats/bregistry/": "module example.org/contracts/go/seats/bregistry",
|
|
}
|
|
for dir, line := range want {
|
|
if mods[dir] != line {
|
|
t.Errorf("%sgo.mod says %q, want %q", dir, mods[dir], line)
|
|
}
|
|
}
|
|
// Every seat the definitions mark whole has its own module, whatever the seat set says of it (ADR 0627: the seat
|
|
// set marks ten whole, and none is without its package).
|
|
for name, d := range m.Seats {
|
|
if d.Grain == seatdef.Whole && mods["go/seats/"+pkgName(name)+"/"] == "" {
|
|
t.Errorf("the seat %s is defined whole and has no Go module of its own", name)
|
|
}
|
|
}
|
|
if len(mods) != len(want) {
|
|
t.Errorf("Go modules %v, want %v", mods, want)
|
|
}
|
|
for p, b := range files {
|
|
if !strings.HasPrefix(p, "go/") || !strings.HasSuffix(p, ".go") {
|
|
continue
|
|
}
|
|
dir := p[:strings.LastIndex(p, "/")+1]
|
|
if dir != "go/seats/" && mods[dir] == "" {
|
|
t.Errorf("%s is in no seat's own Go module, nor the seat set's package", p)
|
|
}
|
|
f, err := parser.ParseFile(token.NewFileSet(), p, b, parser.ImportsOnly)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, im := range f.Imports {
|
|
if path := strings.Trim(im.Path.Value, `"`); strings.Contains(strings.SplitN(path, "/", 2)[0], ".") {
|
|
t.Errorf("%s imports %s: a generated package imports nothing outside the standard library", p, path)
|
|
}
|
|
}
|
|
}
|
|
for _, seat := range []string{"aregistry", "bregistry"} {
|
|
src := string(files["go/seats/"+seat+"/contract.go"])
|
|
if !strings.Contains(src, "type Ref struct") || !strings.Contains(src, "type PublishedEvent") {
|
|
t.Errorf("%s does not hold the part's types whole", seat)
|
|
}
|
|
// A string's enum is a constant per value, so a caller names the definition's value (A3t: the removal reason).
|
|
if !re.MustCompile(`RefReasonRetention\s+= "retention"`).MatchString(src) || !re.MustCompile(`RefReasonTagGone\s+= "tag-gone"`).MatchString(src) {
|
|
t.Errorf("%s does not name the part's enum values:\n%s", seat, src)
|
|
}
|
|
}
|
|
if !strings.Contains(string(files["go/seats/seats.go"]), "OneStore") {
|
|
t.Error("the seat set does not name the seat at the seat list's grain")
|
|
}
|
|
|
|
// The npm side (ADR 0625 §1): the indexer counts an npm package a lock resolves, so each seat defined whole is an
|
|
// npm package of its own (`<package>-<seat>` at ts/seats/<seat without hyphens>), holding its parts' types whole and
|
|
// importing nothing outside itself; the seat set's package (ts/) holds the seat list alone.
|
|
pkgs := map[string]string{}
|
|
for p, b := range files {
|
|
if strings.HasPrefix(p, "ts/") && strings.HasSuffix(p, "package.json") {
|
|
var pj struct {
|
|
Name string `json:"name"`
|
|
}
|
|
if err := json.Unmarshal(b, &pj); err != nil {
|
|
t.Fatalf("%s: %v", p, err)
|
|
}
|
|
pkgs[strings.TrimSuffix(p, "package.json")] = pj.Name
|
|
}
|
|
}
|
|
wantPkgs := map[string]string{
|
|
"ts/": "@example/contracts",
|
|
"ts/seats/aregistry/": "@example/contracts-a-registry",
|
|
"ts/seats/bregistry/": "@example/contracts-b-registry",
|
|
}
|
|
if len(pkgs) != len(wantPkgs) {
|
|
t.Errorf("npm packages %v, want %v", pkgs, wantPkgs)
|
|
}
|
|
for dir, name := range wantPkgs {
|
|
if pkgs[dir] != name {
|
|
t.Errorf("%spackage.json names %q, want %q", dir, pkgs[dir], name)
|
|
}
|
|
}
|
|
for p, b := range files {
|
|
if !strings.HasPrefix(p, "ts/") || !strings.HasSuffix(p, ".ts") {
|
|
continue
|
|
}
|
|
dir := ""
|
|
for d := range pkgs {
|
|
if strings.HasPrefix(p, d) && len(d) > len(dir) {
|
|
dir = d
|
|
}
|
|
}
|
|
if dir == "" || !strings.HasPrefix(p, dir+"src/") {
|
|
t.Errorf("%s is in no npm package's src", p)
|
|
}
|
|
for _, line := range strings.Split(string(b), "\n") {
|
|
i := strings.Index(line, ` from "`)
|
|
if l := strings.TrimSpace(line); i >= 0 && (strings.HasPrefix(l, "import") || strings.HasPrefix(l, "export")) {
|
|
from := strings.Trim(line[i+len(` from `):], `";`)
|
|
if !strings.HasPrefix(from, "./") {
|
|
t.Errorf("%s imports %s: a generated npm package imports only its own files", p, from)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for _, seat := range []string{"aregistry", "bregistry"} {
|
|
if !strings.Contains(string(files["ts/seats/"+seat+"/src/index.ts"]), "export type PublishedEvent = registry.PublishedEvent") ||
|
|
!strings.Contains(string(files["ts/seats/"+seat+"/src/parts/registry.ts"]), "export type PublishedEvent") {
|
|
t.Errorf("the npm package of %s does not hold the part's types whole", seat)
|
|
}
|
|
}
|
|
if ix := string(files["ts/src/index.ts"]); strings.Contains(ix, "Registry") || !strings.Contains(ix, "seats") {
|
|
t.Errorf("the seat set's npm package exports a seat's contract or not the seat list:\n%s", ix)
|
|
}
|
|
}
|