114 lines
4.8 KiB
Go
114 lines
4.8 KiB
Go
package archguard
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"golang.org/x/tools/go/analysis/checker"
|
|
"golang.org/x/tools/go/packages"
|
|
)
|
|
|
|
// packageFloor is the smallest number of packages a healthy load returns. Without it, a run that loads
|
|
// NOTHING — a bad pattern, a rename, a broken module — is indistinguishable from a clean tree and the
|
|
// guard silently becomes decoration. `go vet` is loud about a package that fails to build, but it is
|
|
// perfectly quiet about analyzing zero of them, so the floor is asserted rather than assumed. The module
|
|
// holds ~20 packages (~40 with test variants); the floor catches ZERO, it does not track the count.
|
|
const packageFloor = 15
|
|
|
|
// TestInvariantsHoldInTheShippingTree runs the analyzers over the REAL module from an ordinary `go test`.
|
|
//
|
|
// The invariants moved out of _test.go files into go/analysis, which made them depend on somebody typing
|
|
// `make vet` — `go test -vet=` refuses a custom analyzer, so the ordinary battery stopped carrying them.
|
|
// This puts them back without a second implementation: the same Analyzers() the vet tool wires up, driven
|
|
// here through the checker API instead of unitchecker.
|
|
func TestInvariantsHoldInTheShippingTree(t *testing.T) {
|
|
// Every analyzer, from the same slice cmd/tmvet uses: dropping one from Analyzers() must fail HERE,
|
|
// which is the property the old walk's file floor used to provide for its own coverage.
|
|
if got := len(Analyzers()); got != 3 {
|
|
t.Fatalf("Analyzers() returns %d analyzers, want 3 (stageseam, requestseam, egressseam) — an "+
|
|
"invariant that is not in this slice is enforced by nothing", got)
|
|
}
|
|
|
|
cfg := &packages.Config{Mode: packages.LoadAllSyntax, Tests: true, Dir: "../.."}
|
|
pkgs, err := packages.Load(cfg, "textmachine/backend/...")
|
|
if err != nil {
|
|
t.Fatalf("load the module: %v", err)
|
|
}
|
|
if len(pkgs) < packageFloor {
|
|
t.Fatalf("loaded only %d packages (floor %d) — the pattern matches nothing or the module is "+
|
|
"broken, which makes this guard decoration rather than a guard", len(pkgs), packageFloor)
|
|
}
|
|
var loadErrs []string
|
|
packages.Visit(pkgs, nil, func(p *packages.Package) {
|
|
for _, e := range p.Errors {
|
|
loadErrs = append(loadErrs, e.Error())
|
|
}
|
|
})
|
|
if len(loadErrs) > 0 {
|
|
// A package that does not type-check is analyzed as nothing at all — silently, unless said here.
|
|
t.Fatalf("packages failed to load, so they were NOT analyzed:\n %s", strings.Join(loadErrs, "\n "))
|
|
}
|
|
|
|
graph, err := checker.Analyze(Analyzers(), pkgs, nil)
|
|
if err != nil {
|
|
t.Fatalf("run the analyzers: %v", err)
|
|
}
|
|
var found []string
|
|
for act := range graph.All() {
|
|
if act.Err != nil {
|
|
found = append(found, act.Analyzer.Name+" errored on "+act.Package.PkgPath+": "+act.Err.Error())
|
|
}
|
|
for _, d := range act.Diagnostics {
|
|
found = append(found, act.Package.Fset.Position(d.Pos).String()+": "+d.Message)
|
|
}
|
|
}
|
|
if len(found) > 0 {
|
|
t.Fatalf("architectural invariants violated in the shipping tree:\n %s", strings.Join(found, "\n "))
|
|
}
|
|
assertGuardVocabularyIsLive(t, pkgs)
|
|
}
|
|
|
|
// assertGuardVocabularyIsLive fails when the guards name something that no longer exists.
|
|
//
|
|
// Both halves of this analyzer set are keyed on STRINGS — the exempt lists name files, and the egress
|
|
// shape names two types. A rename on either side does not break a build and does not fail a test: the
|
|
// exemption simply stops matching (a legitimate file starts being reported, which is loud) or, far
|
|
// worse, the shape half stops matching anything at all and the guard goes quietly decorative. That
|
|
// second direction is exactly the failure mode this whole package was built to end, so it is asserted
|
|
// rather than trusted.
|
|
func assertGuardVocabularyIsLive(t *testing.T, pkgs []*packages.Package) {
|
|
t.Helper()
|
|
const modulePrefix = "textmachine/backend/"
|
|
for _, list := range [][]string{stageSeam.exempt, requestSeam.exempt, egressExempt} {
|
|
for _, f := range list {
|
|
rel, ok := strings.CutPrefix(f, modulePrefix)
|
|
if !ok {
|
|
t.Errorf("exemption %q is not inside this module — it can never match", f)
|
|
continue
|
|
}
|
|
if _, err := os.Stat(filepath.Join("../..", rel)); err != nil {
|
|
t.Errorf("exemption %q names a file that does not exist (%v) — a rename left it behind, "+
|
|
"and an exemption nobody can see is how a seam re-opens", f, err)
|
|
}
|
|
}
|
|
}
|
|
var llmPkgFound bool
|
|
for _, p := range pkgs {
|
|
if p.PkgPath != llmPkg || p.Types == nil {
|
|
continue
|
|
}
|
|
llmPkgFound = true
|
|
for _, name := range []string{llmRequestType, llmResponseType} {
|
|
if p.Types.Scope().Lookup(name) == nil {
|
|
t.Errorf("the egress shape is keyed on %s.%s, which no longer exists — renaming it "+
|
|
"silences the name-independent half of the guard without failing anything else",
|
|
llmPkg, name)
|
|
}
|
|
}
|
|
}
|
|
if !llmPkgFound {
|
|
t.Errorf("package %s was not loaded, so the egress vocabulary could not be checked", llmPkg)
|
|
}
|
|
}
|