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]) }