Files
mesh-catalog/modules/mesh-build-go/buildstep/seatgen/layout_test.go
T

180 lines
7.8 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): there is no seat set and no
// root Go module (ADR 0635 §1), each seat defined whole is a Go module of its own holding its package alone, the
// parts it includes are written into it, a definition at the seat-list grain generates nothing, 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/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 mods[dir] == "" {
t.Errorf("%s is in no seat's own Go module", 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 moment (format date-time) is the standard library's time in Go, a required one by value and an optional one
// by pointer (a zero time is not an absent one), and a string in TypeScript.
if !re.MustCompile(`Pushed\s+time\.Time\s+`+"`json:\"pushed\"`").MatchString(src) || !re.MustCompile(`At\s+\*time\.Time\s+`+"`json:\"at,omitempty\"`").MatchString(src) ||
!(strings.Contains(src, "\t\"time\"\n") || strings.Contains(src, "import \"time\"")) {
t.Errorf("%s does not write a date-time as time.Time:\n%s", seat, src)
}
if ts := string(files["ts/seats/"+seat+"/src/parts/registry.ts"]); !strings.Contains(ts, `"pushed": string;`) || !strings.Contains(ts, `"at"?: string;`) {
t.Errorf("%s's TypeScript does not write a date-time as a string:\n%s", seat, ts)
}
// A scalar typed ["T","null"] is a pointer in Go and `T | null` in TypeScript, so a declared zero survives with its type.
for _, w := range []string{`Floor\s+\*float64\s+` + "`json:\"floor,omitempty\"`", `Limit\s+\*int64\s+` + "`json:\"limit,omitempty\"`", `Label\s+\*string\s+` + "`json:\"label,omitempty\"`", `On\s+\*bool\s+` + "`json:\"on,omitempty\"`"} {
if !re.MustCompile(w).MatchString(src) {
t.Errorf("%s does not write a nullable scalar as a pointer (%s):\n%s", seat, w, src)
}
}
if ts := string(files["ts/seats/"+seat+"/src/parts/registry.ts"]); !strings.Contains(ts, `"floor"?: number | null;`) || !strings.Contains(ts, `"on"?: boolean | null;`) || !strings.Contains(ts, `"label"?: string | null;`) {
t.Errorf("%s's TypeScript does not write a nullable scalar as T | null:\n%s", seat, ts)
}
// 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)
}
}
for p := range files {
if strings.Contains(p, "onestore") || p == "go/seats/seats.go" || p == "go/go.mod" || p == "ts/package.json" || p == "ts/src/seats.ts" {
t.Errorf("%s is written: a seat list is not generated, and a definition at its grain generates nothing", p)
}
}
// 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; there is no package above them.
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/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)
}
}
}
}
// F7: a published package is built from its own lock (mesh-build-node refuses one with none), and a generated one has
// no dependency to lock: its lock names the package alone and its package.json has no script and no dependency.
for _, seat := range []string{"aregistry", "bregistry"} {
var pj, lock map[string]any
if err := json.Unmarshal(files["ts/seats/"+seat+"/package.json"], &pj); err != nil {
t.Fatal(err)
}
if err := json.Unmarshal(files["ts/seats/"+seat+"/package-lock.json"], &lock); err != nil {
t.Fatalf("%s has no readable package-lock.json: %v", seat, err)
}
for _, k := range []string{"scripts", "dependencies", "devDependencies"} {
if _, ok := pj[k]; ok {
t.Errorf("%s's package.json has %s: a generated package needs no compiler or dependency", seat, k)
}
}
if pk, _ := lock["packages"].(map[string]any); len(pk) != 1 || lock["name"] != pj["name"] {
t.Errorf("%s's lock %v does not name its package alone", seat, lock)
}
}
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)
}
}
}