mesh/delivery-group group feat/774-l3-reland rejected: composed together: the checker's verdict at the members' heads (b48c90fea31f): integration: mesh-self-ch…
mesh/delivery closed: a newer head of the same pull request
mesh/check failed: integration: mesh-self-check/cmd/mesh-self-check.TestTheSelfCheckWholeRunsOnTheBus: the run answered {Run:doctor-1793351737-1 At:20…
One commit with exactly the tree of the reviewed head35b4509c0a, on main12c2574c9d, so two compiled programs added and removed on the branch leave no trace in history (issue 1577).
198 lines
7.3 KiB
Go
198 lines
7.3 KiB
Go
package main
|
|
|
|
// The data the modules declare, measured, and a retired item deleted only after a last restore point
|
|
// (novox/hq ADR 0233) — over a fake restic and a fake machine.
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
"git.novox.be/novox/mesh-sdk/go/health"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
const composedData = "# The data the modules on this machine declare, composed by the mesh. Do not edit.\n" +
|
|
"# postgres\nitem store irreplaceable /var/lib/mesh-store /var/lib/mesh-store/dumps\nitem dumps rebuildable /var/lib/mesh-store/dumps -\n" +
|
|
"# searxng\nitem valkey cache /var/lib/searxng/valkey-data -\n"
|
|
|
|
func withData(t *testing.T, declared, data string) Where {
|
|
t.Helper()
|
|
w := placed(t, declared)
|
|
w.Data = filepath.Join(w.State, "data.conf")
|
|
must(t, os.WriteFile(w.Data, []byte(data), 0o600))
|
|
return w
|
|
}
|
|
|
|
func TestTheComposedDataFileIsReadIntoEachModulesItems(t *testing.T) {
|
|
b := &Backups{Where: withData(t, composed, composedData), Now: time.Now, Say: quiet}
|
|
got, err := b.Items()
|
|
must(t, err)
|
|
if len(got["postgres"]) != 2 || got["postgres"][0].CoveredBy != "/var/lib/mesh-store/dumps" || got["postgres"][1].CoveredBy != "" ||
|
|
got["searxng"][0].Class != "cache" {
|
|
t.Fatalf("%+v", got)
|
|
}
|
|
// An older controller composes no data file: nothing is declared, nothing fails.
|
|
b = &Backups{Where: placed(t, composed), Now: time.Now, Say: quiet}
|
|
if items, err := b.Items(); err != nil || len(items) != 0 {
|
|
t.Fatalf("%v, %v", items, err)
|
|
}
|
|
}
|
|
|
|
// `backed-up` says every item with disk-load's newest reading and its peak, and the newest good night of the module that
|
|
// keeps the path covering it; an item with no reading yet is said with none.
|
|
func TestEveryItemIsSaidWithItsReadingAndItsLastGoodBackup(t *testing.T) {
|
|
where := withData(t, composed, composedData)
|
|
size, peak := int64(1<<30), int64(2<<30)
|
|
at := time.Date(2026, 10, 6, 4, 0, 0, 0, time.UTC)
|
|
write := time.Unix(1759744800, 0).UTC()
|
|
night := time.Date(2026, 10, 6, 3, 0, 0, 0, time.UTC)
|
|
b := &Backups{Where: where, Now: func() time.Time { return night.Add(time.Hour) }, Say: quiet,
|
|
Run: func(context.Context, string, ...string) (string, error) { return "[]", nil },
|
|
Readings: func() ([]health.Reading, error) {
|
|
return []health.Reading{{V: 1, Node: "ace", PlacedDataItem: catalogue.PlacedDataItem{Module: "postgres", ID: "store"},
|
|
Measured: health.Measured{SizeBytes: &size, LastWrite: &write, MeasuredAt: &at, Precision: "exact"}, Peak: &peak, PeakAt: "2026-10-05"}}, nil
|
|
}}
|
|
b.recordGood("postgres", night)
|
|
got, err := b.BackedUp(context.Background(), "")
|
|
must(t, err)
|
|
var store, dumps *ItemReport
|
|
for _, m := range got {
|
|
for i := range m.Data {
|
|
switch m.Data[i].ID {
|
|
case "store":
|
|
store = &m.Data[i]
|
|
case "dumps":
|
|
dumps = &m.Data[i]
|
|
}
|
|
}
|
|
}
|
|
if store == nil || store.SizeBytes == nil || *store.SizeBytes != 1<<30 || store.Peak == nil || *store.Peak != 2<<30 ||
|
|
store.LastWrite == nil || store.LastWrite.Unix() != 1759744800 || store.LastBackup == nil || !store.LastBackup.Equal(night) {
|
|
t.Fatalf("the store is said as %+v", store)
|
|
}
|
|
if dumps == nil || dumps.LastBackup != nil || dumps.SizeBytes != nil {
|
|
t.Fatalf("an item with no reading or no backup is said with one: %+v", dumps)
|
|
}
|
|
// The same last good night is kept on the bus beside good.json, one row per item a backup path covers.
|
|
b.Node = "ace"
|
|
wrote := map[string]health.GoodRow{}
|
|
b.PutGood = func(k string, r health.GoodRow) error { wrote[k] = r; return nil }
|
|
b.recordGood("postgres", night) // a good night writes the rows, once; the same night again writes nothing
|
|
n := len(wrote)
|
|
b.recordGood("postgres", night)
|
|
row, ok := wrote[health.GoodKey("ace", "postgres", "store")]
|
|
if len(wrote) != 1 || n != 1 || !ok || !row.LastGood.Equal(night) || row.CoveredBy != "/var/lib/mesh-store/dumps" || row.V != health.GoodVersion {
|
|
t.Fatalf("the good rows are %+v", wrote)
|
|
}
|
|
}
|
|
|
|
// A retired item is deleted only after a last restore point of it, tagged as retired; a restore point
|
|
// that fails deletes nothing; and nothing declared now — itself, inside it, or holding it — is deleted.
|
|
func TestARetiredItemIsDeletedOnlyAfterALastRestorePoint(t *testing.T) {
|
|
where := withData(t, composed, composedData)
|
|
var mu sync.Mutex
|
|
var calls []string
|
|
failBackup := false
|
|
run := func(_ context.Context, name string, args ...string) (string, error) {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
line := name + " " + strings.Join(args, " ")
|
|
if name == "restic" {
|
|
line = "restic " + strings.Join(args[5:], " ")
|
|
}
|
|
calls = append(calls, line)
|
|
switch {
|
|
case name == "test":
|
|
return "", nil
|
|
case strings.HasPrefix(line, "restic backup"):
|
|
if failBackup {
|
|
return "", errors.New("the repository is locked")
|
|
}
|
|
return "{\"message_type\":\"summary\",\"snapshot_id\":\"0123456789abcdef\"}\n", nil
|
|
}
|
|
return "", nil
|
|
}
|
|
b := &Backups{Where: where, Run: run, Now: time.Now, Say: quiet}
|
|
ctx := context.Background()
|
|
for _, refused := range []struct{ path, want string }{
|
|
{"/var/lib/mesh-store", "declared now"},
|
|
{"/var/lib/mesh-store/dumps/old", "declared now"},
|
|
{"/var/lib", "declared now"},
|
|
{"/var/lib/mailu/data-mail", "backed up now"},
|
|
{"/srv", "too near the root"},
|
|
{"relative/x", "not a path"},
|
|
} {
|
|
if _, err := b.DeleteRetired(ctx, "m", "i", refused.path, "i", "op", "tidy"); err == nil || !strings.Contains(err.Error(), refused.want) {
|
|
t.Errorf("%s: %v, want %q", refused.path, err, refused.want)
|
|
}
|
|
}
|
|
if _, err := b.DeleteRetired(ctx, "house", "config", "/var/lib/house/config", "nope", "op", "tidy"); err == nil {
|
|
t.Error("a deletion without the item's name again was started")
|
|
}
|
|
if _, err := b.DeleteRetired(ctx, "house", "config", "/var/lib/house/config", "config", "op", ""); err == nil {
|
|
t.Error("a deletion without why was started")
|
|
}
|
|
|
|
wait := func(path string) Deletion {
|
|
t.Helper()
|
|
for i := 0; i < 200; i++ {
|
|
if d, err := b.DeletedOutcome(path); err == nil && d.Done {
|
|
return d
|
|
}
|
|
time.Sleep(5 * time.Millisecond)
|
|
}
|
|
t.Fatalf("the deletion of %s never finished", path)
|
|
return Deletion{}
|
|
}
|
|
|
|
mu.Lock()
|
|
failBackup = true
|
|
mu.Unlock()
|
|
_, err := b.DeleteRetired(ctx, "house", "config", "/var/lib/house/config", "config", "op", "moved to the anchor")
|
|
must(t, err)
|
|
if d := wait("/var/lib/house/config"); d.OK || !strings.Contains(d.Error, "nothing was deleted") {
|
|
t.Fatalf("a deletion with no restore point: %+v", d)
|
|
}
|
|
mu.Lock()
|
|
for _, c := range calls {
|
|
if strings.HasPrefix(c, "rm ") {
|
|
t.Fatal("it deleted without a last restore point")
|
|
}
|
|
}
|
|
mu.Unlock()
|
|
|
|
mu.Lock()
|
|
failBackup, calls = false, nil
|
|
mu.Unlock()
|
|
_, err = b.DeleteRetired(ctx, "house", "config", "/var/lib/house/config", "config", "op", "moved to the anchor")
|
|
must(t, err)
|
|
d := wait("/var/lib/house/config")
|
|
if !d.OK || d.Snapshot != "01234567" {
|
|
t.Fatalf("%+v", d)
|
|
}
|
|
var backup, rm = -1, -1
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
for i, c := range calls {
|
|
switch {
|
|
case c == "restic backup --json --tag module=house --tag retired=config /var/lib/house/config":
|
|
backup = i
|
|
case c == "rm -rf --one-file-system -- /var/lib/house/config":
|
|
rm = i
|
|
}
|
|
}
|
|
if backup < 0 || rm < 0 || rm < backup {
|
|
t.Fatalf("ran %v", calls)
|
|
}
|
|
// Asked again, it answers how it went rather than starting over.
|
|
again, err := b.DeleteRetired(ctx, "house", "config", "/var/lib/house/config", "config", "op", "moved")
|
|
if err != nil || !again.Done || !again.OK {
|
|
t.Fatalf("%+v, %v", again, err)
|
|
}
|
|
}
|