Files
mesh-controller/internal/inventory/chain_source_test.go
T
jschoubben 472744bbaa Migration 0105 drops the tables of the chain behind a guard; a source scan proves no code names them (issue 1096)
The migration raises first unless chain_moved names builds, rollouts and assignments for every kind that holds rows, and unless pending_assignment holds no open or unanswered row; then it drops build_request, build, batched_merge, release_plan, pending_assignment, assignment, raise_assignment_generation(), node.sent_builds and chain_moved. build_gate stays.
2026-10-18 05:32:14 +02:00

145 lines
5.6 KiB
Go

package inventory
import (
"go/ast"
"go/parser"
"go/token"
"io/fs"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"testing"
)
// The tables of the chain, and the column, that the controller's store no longer holds (novox/hq ADR 0408 §8, issue 1096).
var chainTables = []string{"build", "build_request", "release_plan", "batched_merge", "assignment", "pending_assignment"}
// sqlStatement is what makes a literal a statement of the store: it opens with a verb of SQL.
var sqlStatement = regexp.MustCompile(`(?is)^\s*(select|insert|update|delete|alter|drop|create|with|truncate|from|join|where|and|set|values)\b|\b(select\s.+\sfrom|insert\s+into|delete\s+from)\b`)
// **No code names a table of the chain** (acceptance 12, issue 1096), found by reading the sources: a string literal of any
// non-test file of internal/ or cmd/ that names one after from, into, update, join, delete from, references or table —
// build_gate and port_assignment are other words — or the column sent_builds. This is the static half of the proof; the
// integration test reads the catalog.
func TestNoCodeNamesATableOfTheChain(t *testing.T) {
names := regexp.MustCompile(`(?i)\b(from|into|update|join|delete\s+from|references|table)\s+(` + strings.Join(chainTables, "|") + `)\b|\bsent_builds\b`)
var found []string
for _, root := range []string{"../../internal", "../../cmd"} {
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() && d.Name() == "vendor" {
return filepath.SkipDir
}
if d.IsDir() || !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
return nil
}
file, err := parser.ParseFile(token.NewFileSet(), path, nil, 0)
if err != nil {
return err
}
ast.Inspect(file, func(n ast.Node) bool {
lit, ok := n.(*ast.BasicLit)
if !ok || lit.Kind != token.STRING {
return true
}
text, err := strconv.Unquote(lit.Value)
if err != nil {
return true
}
if !sqlStatement.MatchString(text) {
return true // prose that happens to say "from assignment" is no statement
}
if m := names.FindString(text); m != "" {
found = append(found, filepath.ToSlash(path)+": "+strings.Join(strings.Fields(m), " "))
}
return true
})
return nil
})
if err != nil {
t.Fatal(err)
}
}
if len(found) > 0 {
t.Fatalf("code still names a table of the chain, which the store no longer holds:\n %s", strings.Join(found, "\n "))
}
}
// The scan sees what it says it sees: planted, a string naming a table is found, and a comment or a word beside is not.
func TestTheSourceScanFindsATablePlanted(t *testing.T) {
names := regexp.MustCompile(`(?i)\b(from|into|update|join|delete\s+from|references|table)\s+(` + strings.Join(chainTables, "|") + `)\b|\bsent_builds\b`)
for text, want := range map[string]bool{
"select id from build where id = $1": true,
"insert into assignment (node, module)": true,
"update node set sent_builds = $1": true,
"select 1 from build_gate": false,
"delete from port_assignment": false,
"select a from build_request a join build b": true,
"an assignment from build to build": true,
"the assignment generation": false,
"select count(*) from pending_assignment": true,
} {
if got := names.MatchString(text); got != want {
t.Errorf("%q: %v, want %v", text, got, want)
}
}
}
// **The last migration drops the tables of the chain, behind its guard** (issue 1096): it names each table, the column and
// the function, drops no build_gate, and raises before it drops anything.
func TestTheLastMigrationDropsTheTablesOfTheChain(t *testing.T) {
migrations, err := Migrations()
if err != nil {
t.Fatal(err)
}
last := migrations[len(migrations)-1]
if last.Number < 105 {
t.Fatalf("the last migration is %d", last.Number)
}
raw, err := os.ReadFile(filepath.Join("migrations", migrationFile(t, last.Number)))
if err != nil {
t.Fatal(err)
}
var code []string
for _, line := range strings.Split(string(raw), "\n") {
if !strings.HasPrefix(strings.TrimSpace(line), "--") {
code = append(code, line)
}
}
sql := strings.ToLower(strings.Join(code, "\n"))
for _, table := range append(append([]string{}, chainTables...), "chain_moved") {
if !strings.Contains(sql, "drop table "+table+";") {
t.Errorf("migration %d does not drop %s", last.Number, table)
}
}
for _, want := range []string{"drop function raise_assignment_generation()", "alter table node drop column sent_builds"} {
if !strings.Contains(sql, want) {
t.Errorf("migration %d lacks %q", last.Number, want)
}
}
if strings.Contains(sql, "build_gate") || strings.Contains(sql, "assignment_generation;") {
t.Errorf("migration %d touches a table that stays", last.Number)
}
if raise, drop := strings.Index(sql, "raise exception"), strings.Index(sql, "drop table"); raise < 0 || drop < 0 || raise > drop {
t.Errorf("migration %d does not raise before it drops (%d, %d)", last.Number, raise, drop)
}
for _, kind := range []string{"'builds'", "'rollouts'", "'assignments'", "pending_assignment", "chain_moved"} {
if !strings.Contains(sql[:strings.Index(sql, "drop table")], kind) {
t.Errorf("the guard of migration %d does not look at %s", last.Number, kind)
}
}
}
func migrationFile(t *testing.T, number int) string {
t.Helper()
files, err := filepath.Glob(filepath.Join("migrations", strconv.Itoa(number/1000%10)+strconv.Itoa(number/100%10)+strconv.Itoa(number/10%10)+strconv.Itoa(number%10)+"-*.sql"))
if err != nil || len(files) != 1 {
t.Fatalf("the file of migration %d: %v %v", number, files, err)
}
return filepath.Base(files[0])
}