Every method of builds, build requests, rollouts, merges and assignments reads and writes the chain alone: the else branches that read the tables, the movers and MoveChain with their tests, chain_moved reads, ChainReads and the flags that switched a kind, the store's assignments mirror (KeepAssignmentPlans needs none of it: it uses only the AssignmentMirrored type, mirrors.go, and AssignmentValues, provisioning.go) and its tick, the move code of the controller's start, and ForTest's chain_moved row. The listed store tests of the move go with their functions; the tests of the readers, requests, assignments and plans that lived beside them stay in chain_store_test.go. Tests that replayed old data migrations over the build, assignment and pin tables are removed with the tables. The static test that no plan is written but through SavePlansMoving now reads the plan's chain path.
199 lines
6.0 KiB
Go
199 lines
6.0 KiB
Go
package inventory
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
"golang.org/x/crypto/bcrypt"
|
|
|
|
"github.com/novox/mesh-controller/internal/chain"
|
|
"github.com/novox/mesh-controller/internal/store"
|
|
)
|
|
|
|
// ForTest is a fresh inventory in a database of its own, dropped when the test ends.
|
|
//
|
|
// Exported because the check that this mesh reads what a node sends lives beside the link, and a
|
|
// second copy of this would be a second thing to keep true. It takes a *testing.T, so it cannot
|
|
// be called from anything that is not a test.
|
|
func ForTest(t *testing.T) *Inventory {
|
|
t.Helper()
|
|
admin := os.Getenv("MESH_TEST_POSTGRES")
|
|
if admin == "" {
|
|
t.Skip("no MESH_TEST_POSTGRES; run `make check` to raise one")
|
|
}
|
|
name := fmt.Sprintf("inv_%d_%s", time.Now().UnixNano()%1_000_000,
|
|
strings.ToLower(strings.NewReplacer("/", "", "-", "").Replace(t.Name())))
|
|
if len(name) > 60 {
|
|
name = name[:60]
|
|
}
|
|
|
|
// Created already migrated, a copy of a template migrated once (novox/hq issue 591); the Migrate
|
|
// below then finds nothing pending, which it does only if the copy's record matches every migration.
|
|
migrations, err := Migrations()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
store.CreateMigratedForTest(t, admin, name, migrations)
|
|
busPasswordCostForTest.Store(int32(bcrypt.MinCost))
|
|
|
|
cut := strings.LastIndex(admin, "/")
|
|
t.Setenv(store.Variable(Name), admin[:cut]+"/"+name+"?sslmode=disable")
|
|
|
|
inv, err := Open(t.Context())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() {
|
|
inv.Close()
|
|
c, err := pgx.Connect(context.Background(), admin)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer c.Close(context.Background())
|
|
_, _ = c.Exec(context.Background(), "drop database if exists "+name+" with (force)")
|
|
})
|
|
if err := inv.Ready(t.Context(), 20*time.Second); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if _, err := inv.store.Migrate(t.Context(), migrations); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// The chain is on the bus in a mesh; in a test it is in memory (ADR 0408).
|
|
c := chain.InMemory(nil)
|
|
inv.UseChain(c)
|
|
testChains.Store(os.Getenv(store.Variable(Name)), c)
|
|
t.Cleanup(func() { testChains.Delete(os.Getenv(store.Variable(Name))) })
|
|
return inv
|
|
}
|
|
|
|
// RemoveNodeForTest removes a machine's record as a person removing it from the store would: no verb of the
|
|
// mesh removes one yet, and what hangs off it must go with it.
|
|
func (i *Inventory) RemoveNodeForTest(ctx context.Context, name string) (bool, error) {
|
|
tag, err := i.store.Pool().Exec(ctx, `delete from node where name = $1`, name)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return tag.RowsAffected() == 1, nil
|
|
}
|
|
|
|
// ForTestBefore is ForTest with the store migrated only below migration `number`, and a function that runs the
|
|
// rest: for a test of a migration that moves data, which seeds the store as the mesh holds it before the
|
|
// migration, migrates, and reads what the migration made of it (novox/hq issue 651).
|
|
func ForTestBefore(t *testing.T, number int) (*Inventory, func()) {
|
|
t.Helper()
|
|
admin := os.Getenv("MESH_TEST_POSTGRES")
|
|
if admin == "" {
|
|
t.Skip("no MESH_TEST_POSTGRES; run `make check` to raise one")
|
|
}
|
|
name := fmt.Sprintf("invb_%d_%s", time.Now().UnixNano()%1_000_000,
|
|
strings.ToLower(strings.NewReplacer("/", "", "-", "").Replace(t.Name())))
|
|
if len(name) > 60 {
|
|
name = name[:60]
|
|
}
|
|
all, err := Migrations()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var before []store.Migration
|
|
for _, m := range all {
|
|
if m.Number < number {
|
|
before = append(before, m)
|
|
}
|
|
}
|
|
store.CreateMigratedForTest(t, admin, name, before)
|
|
busPasswordCostForTest.Store(int32(bcrypt.MinCost))
|
|
cut := strings.LastIndex(admin, "/")
|
|
t.Setenv(store.Variable(Name), admin[:cut]+"/"+name+"?sslmode=disable")
|
|
inv, err := Open(t.Context())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() {
|
|
inv.Close()
|
|
c, err := pgx.Connect(context.Background(), admin)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer c.Close(context.Background())
|
|
_, _ = c.Exec(context.Background(), "drop database if exists "+name+" with (force)")
|
|
})
|
|
if err := inv.Ready(t.Context(), 20*time.Second); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// The chain is in memory, for what a test of the store before a migration reads of it.
|
|
c := chain.InMemory(nil)
|
|
inv.UseChain(c)
|
|
testChains.Store(os.Getenv(store.Variable(Name)), c)
|
|
t.Cleanup(func() { testChains.Delete(os.Getenv(store.Variable(Name))) })
|
|
return inv, func() {
|
|
t.Helper()
|
|
if _, err := inv.store.Migrate(t.Context(), all); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ExecForTest runs one statement against a test's store, for a fixture no verb of the mesh writes: a row as an
|
|
// older migration left it.
|
|
func (i *Inventory) ExecForTest(t *testing.T, sql string, args ...any) {
|
|
t.Helper()
|
|
if _, err := i.store.Pool().Exec(t.Context(), sql, args...); err != nil {
|
|
t.Fatalf("%s: %v", sql, err)
|
|
}
|
|
}
|
|
|
|
// QueryForTest reads rows of a test's store, each row's columns as JSON text, for a test that checks what a
|
|
// migration left.
|
|
func (i *Inventory) QueryForTest(t *testing.T, sql string, args ...any) [][]string {
|
|
t.Helper()
|
|
rows, err := i.store.Pool().Query(t.Context(), sql, args...)
|
|
if err != nil {
|
|
t.Fatalf("%s: %v", sql, err)
|
|
}
|
|
defer rows.Close()
|
|
var out [][]string
|
|
for rows.Next() {
|
|
values, err := rows.Values()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
row := make([]string, len(values))
|
|
for k, v := range values {
|
|
row[k] = fmt.Sprint(v)
|
|
if b, err := json.Marshal(v); err == nil {
|
|
row[k] = string(b)
|
|
}
|
|
}
|
|
out = append(out, row)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// PlanValuesForTest runs the pass that keeps the provisioner's plans in the values of the assignments (KeepAssignmentPlans)
|
|
// and answers the values that stand, by key: what a reader of the bucket reads.
|
|
func (i *Inventory) PlanValuesForTest(t *testing.T) map[string][]byte {
|
|
t.Helper()
|
|
if _, _, err := i.KeepAssignmentPlans(t.Context()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
kept, err := i.KeptAssignments(t.Context())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out := make(map[string][]byte, len(kept))
|
|
for key, v := range kept {
|
|
out[key] = v.Value
|
|
}
|
|
return out
|
|
}
|