A held module is composed exactly as before it joined, at the build its machine runs, so its port, resources and contributions stay. A joining module's directories belong to its user and its own group, and its tools' directories are refused closed to that group; the runner joins it where the engine can say so. A take records the root and is taken only when the engine states the act done. Libraries' paths and verdicts count modules pulled in by a requirement, and a unit takes the identity.
394 lines
16 KiB
Go
394 lines
16 KiB
Go
package inventory
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"sort"
|
||
"strings"
|
||
"testing"
|
||
|
||
"github.com/novox/mesh-controller/internal/catalogue"
|
||
)
|
||
|
||
// novox/hq ADR 0328 §3, §4, §15: one number per joining module (its user's and its own group's) and one per library's
|
||
// group, from one sequence and one range — never 0 or a root group's, never shared between a module and a library,
|
||
// never given twice, the same however often asked.
|
||
func TestOneNumberPerModuleAndLibraryNeverRoot(t *testing.T) {
|
||
inv := fresh(t)
|
||
ctx := t.Context()
|
||
radarr, err := inv.AllocateIdentity(ctx, "radarr")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
again, err := inv.AllocateIdentity(ctx, "radarr")
|
||
if err != nil || again != radarr {
|
||
t.Fatalf("radarr was given %+v, then %+v (%v): one number per module", radarr, again, err)
|
||
}
|
||
media, err := inv.AllocateLibraryGroup(ctx, "media")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if mediaAgain, err := inv.AllocateLibraryGroup(ctx, "media"); err != nil || mediaAgain != media {
|
||
t.Fatalf("the media group was given %d, then %d (%v)", media, mediaAgain, err)
|
||
}
|
||
plex, err := inv.AllocateIdentity(ctx, "plex")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
seen := map[int]string{}
|
||
for who, n := range map[string]int{"radarr": radarr.UID, "plex": plex.UID, "mesh-media": media} {
|
||
if n == 0 || !catalogue.AllocatableID(n) {
|
||
t.Errorf("%s was given %d: within %d–%d, never 0", who, n, catalogue.IdentityLow, catalogue.IdentityHigh)
|
||
}
|
||
if other, twice := seen[n]; twice {
|
||
t.Errorf("%s and %s share %d: a module's own group and a library's never do", who, other, n)
|
||
}
|
||
seen[n] = who
|
||
}
|
||
// Read back as every machine's composition reads them: the same numbers.
|
||
ids, err := inv.Identities(ctx)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
groups, err := inv.LibraryGroups(ctx)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if ids["radarr"].UID != radarr.UID || groups["media"] != media {
|
||
t.Fatalf("read back %+v and %v", ids, groups)
|
||
}
|
||
// A forgotten row frees no number: the next one is new.
|
||
if _, err := inv.store.Pool().Exec(ctx, `delete from module_identity where module = 'plex'`); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
sonarr, err := inv.AllocateIdentity(ctx, "sonarr")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if _, given := seen[sonarr.UID]; given {
|
||
t.Fatalf("sonarr was given %d, a number given before", sonarr.UID)
|
||
}
|
||
// The store refuses a number outside the range, root's above all.
|
||
for _, n := range []int{0, 4, 10, 1000, 49999, 60000} {
|
||
if _, err := inv.store.Pool().Exec(ctx, `insert into module_identity (module, uid) values ('byhand', $1)`, n); err == nil {
|
||
t.Errorf("the store took %d as a module's number", n)
|
||
}
|
||
if _, err := inv.store.Pool().Exec(ctx, `insert into library_group (library, gid) values ('byhand', $1)`, n); err == nil {
|
||
t.Errorf("the store took %d as a library group's number", n)
|
||
}
|
||
}
|
||
}
|
||
|
||
// libraryController registers the controller's own module, whose mesh-wide layer defines the libraries.
|
||
func libraryController(t *testing.T, inv *Inventory) {
|
||
t.Helper()
|
||
m := catalogue.Manifest{Module: catalogue.LibrariesModule, Version: "1",
|
||
Resources: []map[string]any{{"id": "server", "type": "container", "name": "controller", "image": "x"}}}
|
||
if err := inv.RegisterModule(t.Context(), m, Source{}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
|
||
// novox/hq ADR 0328 §1, §4, §13: a library is defined only on the controller's mesh-wide layer, as a trusted setting;
|
||
// never on one machine; never overlapping another on a machine it reaches; never named as a registered module — and no
|
||
// module is registered under a defined library's name.
|
||
func TestALibraryIsDefinedOnceForTheMeshAndNeverOverlaps(t *testing.T) {
|
||
inv := fresh(t)
|
||
ctx := t.Context()
|
||
for _, n := range []string{"ace", "novox"} {
|
||
if _, err := inv.AddNode(ctx, n); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
libraryController(t, inv)
|
||
radarr := catalogue.Manifest{Module: "radarr", Version: "1",
|
||
Resources: []map[string]any{{"id": "server", "type": "container", "name": "radarr", "image": "x"}}}
|
||
if err := inv.RegisterModule(ctx, radarr, Source{}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
media := map[string]any{"home": "ace", "root": "/storage/media", "reach": []any{"novox"}}
|
||
define := func(libs map[string]any) map[string]any {
|
||
return map[string]any{catalogue.LibrariesSetting: libs}
|
||
}
|
||
if err := inv.SetSettings(ctx, "", catalogue.LibrariesModule, define(map[string]any{"media": media})); err != nil {
|
||
t.Fatalf("a library was not defined: %v", err)
|
||
}
|
||
libs, err := inv.Libraries(ctx)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if l := libs["media"]; l.Home != "ace" || l.Root != "/storage/media" || !l.Reaches("novox") || !l.Reaches("ace") {
|
||
t.Fatalf("the library reads %+v", libs["media"])
|
||
}
|
||
for name, c := range map[string]struct {
|
||
node string
|
||
libs map[string]any
|
||
want string
|
||
}{
|
||
"on one machine": {"ace", map[string]any{"media": media}, "never on one machine"},
|
||
"inside another": {"", map[string]any{"media": media,
|
||
"movies": map[string]any{"home": "novox", "root": "/storage/media/movies"}}, "overlap on"},
|
||
"around another": {"", map[string]any{"movies": map[string]any{"home": "ace", "root": "/storage/media/movies"},
|
||
"media": media}, "overlap on"},
|
||
"named as a module": {"", map[string]any{"radarr": map[string]any{"home": "ace", "root": "/srv/radarr-data"}},
|
||
"name of the module radarr"},
|
||
"with an owner": {"", map[string]any{"books": map[string]any{"home": "ace", "root": "/srv/books",
|
||
"owner": "1000"}}, "nothing about an owner"},
|
||
} {
|
||
err := inv.SetSettings(ctx, c.node, catalogue.LibrariesModule, define(c.libs))
|
||
if err == nil || !strings.Contains(err.Error(), c.want) {
|
||
t.Errorf("%s: %v; want refused saying %q", name, err, c.want)
|
||
}
|
||
}
|
||
// Two libraries apart on different machines may share a path's prefix only where they do not meet.
|
||
apart := map[string]any{"media": media, "photos": map[string]any{"home": "elsewhere", "root": "/storage/media/photos"}}
|
||
if err := inv.JudgeSettings(ctx, "", catalogue.LibrariesModule, define(apart)); err != nil {
|
||
t.Errorf("libraries that meet on no machine were refused: %v", err)
|
||
}
|
||
// A module under a defined library's name is refused.
|
||
named := catalogue.Manifest{Module: "media", Version: "1",
|
||
Resources: []map[string]any{{"id": "s", "type": "container", "name": "media", "image": "x"}}}
|
||
if err := inv.RegisterModule(ctx, named, Source{}); err == nil || !strings.Contains(err.Error(), "name of the library media") {
|
||
t.Fatalf("a module named as a library: %v", err)
|
||
}
|
||
}
|
||
|
||
// novox/hq ADR 0328 §7: a library taken is recorded once per home and filesystem, under a warrant; one taken again on
|
||
// another filesystem replaces it.
|
||
func TestALibraryIsTakenOnceUnderAWarrant(t *testing.T) {
|
||
inv := fresh(t)
|
||
ctx := t.Context()
|
||
if err := inv.RecordTakeStarted(ctx, TakenLibrary{Library: "media", Home: "ace", By: "the operator"}); err == nil {
|
||
t.Fatal("a library was recorded taken with no warrant")
|
||
}
|
||
first := TakenLibrary{Library: "media", Home: "ace", Root: "/storage/media", Placed: "fs-000000000001",
|
||
By: "the operator", Ask: "c01", Warrant: "c01"}
|
||
if err := inv.RecordTakeStarted(ctx, first); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
again := first
|
||
again.Ask, again.Warrant, again.By = "c02", "c02", "someone else"
|
||
if err := inv.RecordTakeStarted(ctx, again); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
taken, err := inv.TakenLibraries(ctx)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if got := taken["media"]; got.Ask != "c01" {
|
||
t.Fatalf("a second take of the same overwrote the first: %+v", got)
|
||
}
|
||
moved := first
|
||
moved.Placed, moved.Ask, moved.Warrant = "fs-0000000000ff", "c03", "c03"
|
||
if err := inv.RecordTakeStarted(ctx, moved); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if taken, err = inv.TakenLibraries(ctx); err != nil || taken["media"].Ask != "c03" {
|
||
t.Fatalf("a take on another filesystem: %+v (%v)", taken["media"], err)
|
||
}
|
||
// The review of 7ea6d36e, item 3: the root is part of what was taken — a library whose root changed is taken
|
||
// again, and the new take replaces the old.
|
||
if got := taken["media"].Root; got != "/storage/media" {
|
||
t.Fatalf("the take recorded the root %q", got)
|
||
}
|
||
rooted := moved
|
||
rooted.Root, rooted.Ask, rooted.Warrant = "/storage/media2", "c04", "c04"
|
||
if err := inv.RecordTakeStarted(ctx, rooted); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if taken, err = inv.TakenLibraries(ctx); err != nil || taken["media"].Ask != "c04" || taken["media"].Root != "/storage/media2" {
|
||
t.Fatalf("a take at another root: %+v (%v)", taken["media"], err)
|
||
}
|
||
}
|
||
|
||
// novox/hq issue 575: a module registered without the access movies, whose newest build on its branch declares it —
|
||
// the build the delivery under way brings — takes a proposal placing movies: judged against that build, not refused.
|
||
// Without such a build it is refused, as before.
|
||
func TestAPlacementIsJudgedAgainstTheBuildThatBringsTheAccess(t *testing.T) {
|
||
inv := fresh(t)
|
||
ctx := t.Context()
|
||
if _, err := inv.AddNode(ctx, "ace"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
registered := catalogue.Manifest{Module: "media-importer", Version: "1",
|
||
Accesses: []catalogue.Access{{ID: "downloads", Path: "/services/media/downloads", Mode: catalogue.AccessReadWrite}},
|
||
Resources: []map[string]any{{"id": "server", "type": "container", "name": "importer", "image": "x",
|
||
"volumes": []any{"${access:downloads}:/downloads"}}}}
|
||
if err := inv.RegisterModule(ctx, registered, Source{Repository: "https://forge.invalid/media.git",
|
||
Ref: "main", BuiltFrom: "c0ffee33"}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
placing := map[string]any{catalogue.AccessesSetting: map[string]any{"movies": "/storage/media/movies"}}
|
||
|
||
err := inv.JudgeSettings(ctx, "ace", "media-importer", placing)
|
||
if err == nil || !strings.Contains(err.Error(), `places the access "movies", which it does not declare`) {
|
||
t.Fatalf("a placement no build declares: %v", err)
|
||
}
|
||
|
||
// main's manifest — the build of #34 — declares movies.
|
||
ahead := registered
|
||
ahead.Accesses = append([]catalogue.Access{}, registered.Accesses...)
|
||
ahead.Accesses = append(ahead.Accesses, catalogue.Access{ID: "movies", Path: "/services/media/movies",
|
||
Mode: catalogue.AccessReadWrite})
|
||
raw, err := json.Marshal(ahead)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
b := aBuild("b34", "media-importer", "")
|
||
b.Ref, b.Manifest = "main", raw
|
||
if err := inv.RecordBuild(ctx, b); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := inv.JudgeSettings(ctx, "ace", "media-importer", placing); err != nil {
|
||
t.Fatalf("the proposal placing movies, which main's build declares, was refused: %v", err)
|
||
}
|
||
if err := inv.SetSettings(ctx, "ace", "media-importer", placing); err != nil {
|
||
t.Fatalf("the layer placing movies was not stored: %v", err)
|
||
}
|
||
// Stored ahead, it does not cost the registered module its place: composition passes it by.
|
||
layers, err := inv.SettingsFor(ctx, "ace", "media-importer")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := catalogue.ComposesWith(registered, layers, false); err != nil {
|
||
t.Fatalf("the registered module no longer composes with the layer stored ahead: %v", err)
|
||
}
|
||
// A typo is still a typo: an access neither build declares is refused.
|
||
typo := map[string]any{catalogue.AccessesSetting: map[string]any{"moveis": "/storage/media/movies"}}
|
||
if err := inv.JudgeSettings(ctx, "ace", "media-importer", typo); err == nil {
|
||
t.Fatal("a placement no build declares was taken")
|
||
}
|
||
}
|
||
|
||
// Review of e1c2b744: only a build of the branch the module's record follows judges a placement ahead of it — never one
|
||
// from another branch, nor one with no branch, and a module registered with no branch is refused as before, saying so.
|
||
func TestOnlyABuildOfTheFollowedBranchJudgesAPlacementAhead(t *testing.T) {
|
||
inv := fresh(t)
|
||
ctx := t.Context()
|
||
if _, err := inv.AddNode(ctx, "ace"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
registered := catalogue.Manifest{Module: "importer", Version: "1",
|
||
Resources: []map[string]any{{"id": "server", "type": "container", "name": "importer", "image": "x"}}}
|
||
ahead := registered
|
||
ahead.Accesses = []catalogue.Access{{ID: "movies", Path: "/services/media/movies", Mode: catalogue.AccessReadWrite}}
|
||
raw, err := json.Marshal(ahead)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
placing := map[string]any{catalogue.AccessesSetting: map[string]any{"movies": "/storage/media/movies"}}
|
||
if err := inv.RegisterModule(ctx, registered, Source{Repository: "https://forge.invalid/media.git", Ref: "main",
|
||
BuiltFrom: "c0ffee"}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
for i, ref := range []string{"feature", ""} {
|
||
b := aBuild(fmt.Sprintf("elsewhere%d", i), "importer", "")
|
||
b.Ref, b.Manifest = ref, raw
|
||
if err := inv.RecordBuild(ctx, b); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
if err := inv.JudgeSettings(ctx, "ace", "importer", placing); err == nil {
|
||
t.Fatal("a build of another branch, or of none, judged the placement")
|
||
}
|
||
// Registered by hand, with no branch: refused, saying why.
|
||
byHand := registered
|
||
byHand.Module = "byhand"
|
||
if err := inv.RegisterModule(ctx, byHand, Source{}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
aheadByHand := ahead
|
||
aheadByHand.Module = "byhand"
|
||
if raw, err = json.Marshal(aheadByHand); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
b := aBuild("onmain", "byhand", "")
|
||
b.Ref, b.Manifest = "main", raw
|
||
if err := inv.RecordBuild(ctx, b); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := inv.JudgeSettings(ctx, "ace", "byhand", placing); err == nil ||
|
||
!strings.Contains(err.Error(), "follows no branch") {
|
||
t.Fatalf("a module with no branch recorded: %v", err)
|
||
}
|
||
}
|
||
|
||
// The review of 7ea6d36e, item 4: every sub-path a module asks of a library is kept on its home, whether the module
|
||
// was assigned or pulled in by a requirement (it is in what its machine was last sent).
|
||
func TestTheSubPathsOfModulesPulledInByARequirementAreKept(t *testing.T) {
|
||
inv := fresh(t)
|
||
ctx := t.Context()
|
||
record, err := inv.AddNode(ctx, "ace")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
for _, m := range []catalogue.Manifest{
|
||
{Module: "radarr", Version: "1", Libraries: []catalogue.LibraryUse{{ID: "m", Library: "media", Path: "movies", Use: "read-write"}}},
|
||
{Module: "importer", Version: "1", Libraries: []catalogue.LibraryUse{{ID: "i", Library: "media", Path: "incoming", Use: "read-write"}}},
|
||
} {
|
||
if err := inv.RegisterModule(ctx, m, Source{}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
if _, err := inv.Assign(ctx, "ace", "radarr"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := inv.RecordSentWith(ctx, record.ID, "d1", map[string]string{"radarr": "c1", "importer": "c2"}, 1, 1,
|
||
Send{Sequence: 1}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
paths, err := inv.LibraryPaths(ctx)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
got := append([]string{}, paths["media"]...)
|
||
sort.Strings(got)
|
||
if strings.Join(got, ",") != "incoming,movies" {
|
||
t.Fatalf("the paths kept of media are %v; want the importer's, pulled in by a requirement, too", got)
|
||
}
|
||
}
|
||
|
||
// The review of 7ea6d36e, item 5: a take the engine says it started is recorded as started, not taken; it is taken when
|
||
// the engine states the act done under that warrant, and forgotten when it states it failed, so the operator is asked
|
||
// again.
|
||
func TestALibraryIsTakenWhenTheActIsDone(t *testing.T) {
|
||
inv := fresh(t)
|
||
ctx := t.Context()
|
||
started := TakenLibrary{Library: "media", Home: "ace", Root: "/storage/media", Placed: "fs-000000000001",
|
||
By: "the operator", Ask: "c01", Warrant: "c01"}
|
||
if err := inv.RecordTakeStarted(ctx, started); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
taken, err := inv.TakenLibraries(ctx)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if got := taken["media"]; got.Done || got.Ask != "c01" {
|
||
t.Fatalf("a take only started reads %+v; want started, not done", got)
|
||
}
|
||
if err := inv.TakeDone(ctx, "media", "c99"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if taken, _ = inv.TakenLibraries(ctx); taken["media"].Done {
|
||
t.Fatal("another warrant's act made the take done")
|
||
}
|
||
if err := inv.TakeDone(ctx, "media", "c01"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if taken, _ = inv.TakenLibraries(ctx); !taken["media"].Done {
|
||
t.Fatal("the act done under the take's warrant did not make it taken")
|
||
}
|
||
// A failed act forgets a take not done, so it is asked again; never one done.
|
||
again := started
|
||
again.Root, again.Ask, again.Warrant = "/storage/media2", "c02", "c02"
|
||
if err := inv.RecordTakeStarted(ctx, again); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := inv.TakeFailed(ctx, "media", "c02"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if taken, _ = inv.TakenLibraries(ctx); taken["media"].Ask == "c02" {
|
||
t.Fatalf("a failed take is still recorded: %+v", taken["media"])
|
||
}
|
||
}
|