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.
145 lines
5.6 KiB
Go
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])
|
|
}
|