191 lines
8.6 KiB
Go
191 lines
8.6 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
sdkdelivery "git.novox.be/novox/mesh-sdk/go/delivery"
|
|
"github.com/novox/mesh-controller/internal/builderkeys"
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
)
|
|
|
|
// planKeysOf is the indexer and the builder as a walk reads them, for a test: values by module and commit, keys by
|
|
// module.
|
|
type planKeysOf struct {
|
|
values map[string]sdkdelivery.Module // by module + "@" + commit
|
|
keys map[string]builderkeys.Key
|
|
}
|
|
|
|
func (k planKeysOf) ModuleAt(_ context.Context, module, commit string) (sdkdelivery.Module, error) {
|
|
if v, ok := k.values[module+"@"+commit]; ok {
|
|
return v, nil
|
|
}
|
|
return sdkdelivery.Module{}, builderkeys.ErrNoValue
|
|
}
|
|
|
|
func (k planKeysOf) Build(_ context.Context, module, fp string) (builderkeys.Key, error) {
|
|
if key, ok := k.keys[module]; ok && key.Fingerprint == fp {
|
|
return key, nil
|
|
}
|
|
return builderkeys.Key{Module: module, Fingerprint: fp}, nil
|
|
}
|
|
|
|
// recordsOf is the build records and refusals a settle reads, for a test.
|
|
type recordsOf struct {
|
|
builds map[string]inventory.Build
|
|
refused map[string]string
|
|
sources map[string]inventory.Source
|
|
}
|
|
|
|
func (r recordsOf) BuildByID(_ context.Context, id string) (inventory.Build, bool, error) {
|
|
b, ok := r.builds[id]
|
|
return b, ok, nil
|
|
}
|
|
|
|
func (r recordsOf) RefusedOf(_ context.Context, ids []string) (map[string]string, error) {
|
|
out := map[string]string{}
|
|
for _, id := range ids {
|
|
if why, ok := r.refused[id]; ok {
|
|
out[id] = why
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (r recordsOf) SourceOf(_ context.Context, module string) (inventory.Source, error) {
|
|
if s, ok := r.sources[module]; ok {
|
|
return s, nil
|
|
}
|
|
return inventory.Source{}, inventory.ErrNoSuchModule
|
|
}
|
|
|
|
func aWalkOfOne(module string) (*inventory.Plan, map[string]inventory.Entry) {
|
|
p := &inventory.Plan{ID: "plan-1", State: inventory.PlanBuilding, Tiers: [][]string{{module}},
|
|
Modules: map[string]*inventory.PlanModule{}, Repository: "novox/mesh-catalog", Commit: "c0ffee0123456789",
|
|
Delivery: &inventory.PlanDelivery{Merges: []inventory.PlanMerge{{Repository: "novox/mesh-catalog", Commit: "c0ffee0123456789"}}}}
|
|
byName := map[string]inventory.Entry{module: {Source: inventory.Source{Repository: "novox/mesh-catalog", Seat: "git",
|
|
Path: "modules/" + module, Ref: "main"}}}
|
|
return p, byName
|
|
}
|
|
|
|
// A6 (ADR 0503 §6, ADR 0498 check §7): a walk whose module has a done key goes asked to built with no build asked: the
|
|
// ask stub is never called, the key is taken in once, and `plans` says it was taken from the builder's key.
|
|
func TestAWalkTakesADoneKeyAndAsksNoBuild(t *testing.T) {
|
|
was := askABuild
|
|
askABuild = func(context.Context, buildSource, string, string) (string, error) {
|
|
t.Fatal("a walk asked build-agent for a module that is not the builder's own")
|
|
return "", nil
|
|
}
|
|
t.Cleanup(func() { askABuild = was })
|
|
p, byName := aWalkOfOne("sudo")
|
|
askModule(t.Context(), p, "sudo", byName)
|
|
s := p.Modules["sudo"]
|
|
if s.State != "asked" || !s.Key || s.Build != "" || s.KeyAt != "c0ffee0123456789" {
|
|
t.Fatalf("the module is not taken from a key: %+v", s)
|
|
}
|
|
fp := "src2:" + strings.Repeat("a", 64)
|
|
k := builderkeys.Key{Module: "sudo", Fingerprint: fp, Found: true, Revision: 2, Build: sdkdelivery.BuilderBuild{
|
|
State: sdkdelivery.BuildDone, Manifest: strings.Repeat("b", 64), Artifacts: []sdkdelivery.BuiltArtifact{{Name: "a",
|
|
Digest: "sha256:1", Reference: "artifact-store://sudo/a/blobs/sha256:1"}}}}
|
|
keys := planKeysOf{values: map[string]sdkdelivery.Module{"sudo@c0ffee0123456789": {FP2: fp, Commit: "c0ffee0123456789"}},
|
|
keys: map[string]builderkeys.Key{"sudo": k}}
|
|
records := recordsOf{builds: map[string]inventory.Build{}, sources: map[string]inventory.Source{}}
|
|
var taken []string
|
|
take := func(m string, _ sdkdelivery.Module, id string) { taken = append(taken, m+" "+id) }
|
|
t.Cleanup(func() {
|
|
keyTakes.Lock()
|
|
keyTakes.running, keyTakes.waits, keyTakes.taken = map[string]bool{}, map[string]string{}, map[string]bool{}
|
|
keyTakes.Unlock()
|
|
})
|
|
|
|
changed, err := settleKeys(t.Context(), records, keys, p, p.Tiers[0], take)
|
|
if err != nil || !changed || len(taken) != 1 || s.State != "asked" || s.Fingerprint != fp {
|
|
t.Fatalf("a done key not recorded yet is taken in once: %v %v %v %+v", changed, err, taken, s)
|
|
}
|
|
// A second look while the take-in runs starts no second one.
|
|
if _, err := settleKeys(t.Context(), records, keys, p, p.Tiers[0], take); err != nil || len(taken) != 1 {
|
|
t.Fatalf("a second look took the key in again: %v %v", taken, err)
|
|
}
|
|
// The take-in recorded and registered it: the look settles it built from the record.
|
|
id := keyBuildID(k)
|
|
records.builds[id] = inventory.Build{ID: id, Module: "sudo", Commit: "c0ffee0123456789", At: time.Now()}
|
|
records.sources["sudo"] = inventory.Source{BuiltFrom: "c0ffee0123456789"}
|
|
if _, err := settleKeys(t.Context(), records, keys, p, p.Tiers[0], take); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if s.State != "built" || s.Build != id || !strings.Contains(s.Made, "taken from the builder's key") {
|
|
t.Fatalf("the module is not built from its key: %+v", s)
|
|
}
|
|
if takesOutcome(*p, &inventory.PlanModule{Key: true, State: "asked", AskedAt: s.AskedAt}, time.Now(), "build-1") {
|
|
t.Fatal("a build-agent outcome answers a module taken from a key")
|
|
}
|
|
}
|
|
|
|
// A7: a key building waits and says so, then moves on when done, with no timer and no ask; a failed key fails the walk
|
|
// in the builder's words; a value the indexer has not written yet waits, named.
|
|
func TestAWalkWaitsForAKeyAndFailsWithItsWords(t *testing.T) {
|
|
asksOfNone := askABuild
|
|
askABuild = func(context.Context, buildSource, string, string) (string, error) {
|
|
t.Fatal("a walk asked a build")
|
|
return "", nil
|
|
}
|
|
t.Cleanup(func() { askABuild = asksOfNone })
|
|
t.Cleanup(func() {
|
|
keyTakes.Lock()
|
|
keyTakes.running, keyTakes.waits, keyTakes.taken = map[string]bool{}, map[string]string{}, map[string]bool{}
|
|
keyTakes.Unlock()
|
|
})
|
|
p, byName := aWalkOfOne("view")
|
|
askModule(t.Context(), p, "view", byName)
|
|
s := p.Modules["view"]
|
|
records := recordsOf{builds: map[string]inventory.Build{}}
|
|
take := func(string, sdkdelivery.Module, string) {}
|
|
keys := planKeysOf{values: map[string]sdkdelivery.Module{}, keys: map[string]builderkeys.Key{}}
|
|
if _, err := settleKeys(t.Context(), records, keys, p, p.Tiers[0], take); err != nil ||
|
|
!strings.Contains(s.Why, "waits for the indexer's value of view at c0ffee01") {
|
|
t.Fatalf("no indexer value yet: %q %v", s.Why, err)
|
|
}
|
|
fp := "src2:" + strings.Repeat("c", 64)
|
|
keys.values["view@c0ffee0123456789"] = sdkdelivery.Module{FP2: fp}
|
|
keys.keys["view"] = builderkeys.Key{Module: "view", Fingerprint: fp, Found: true,
|
|
Build: sdkdelivery.BuilderBuild{State: sdkdelivery.BuildBuilding, By: "g14"}}
|
|
if _, err := settleKeys(t.Context(), records, keys, p, p.Tiers[0], take); err != nil ||
|
|
s.State != "asked" || !strings.Contains(s.Why, "building on g14") || p.State != inventory.PlanBuilding {
|
|
t.Fatalf("a building key: %+v %v", s, err)
|
|
}
|
|
keys.keys["view"] = builderkeys.Key{Module: "view", Fingerprint: fp, Found: true,
|
|
Build: sdkdelivery.BuilderBuild{State: sdkdelivery.BuildFailed, Error: "not stored: tools: the store answered 500"}}
|
|
if _, err := settleKeys(t.Context(), records, keys, p, p.Tiers[0], take); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if s.State != "failed" || p.State != inventory.PlanFailed || !strings.Contains(p.Note, "not stored: tools") {
|
|
t.Fatalf("a failed key does not fail the walk in its words: %+v %q", s, p.Note)
|
|
}
|
|
}
|
|
|
|
// A7: a take-in refused (a module from another repository, a manifest refused) fails the walk with the refusal.
|
|
func TestARefusedTakeInFailsTheWalk(t *testing.T) {
|
|
t.Cleanup(func() {
|
|
keyTakes.Lock()
|
|
keyTakes.running, keyTakes.waits, keyTakes.taken = map[string]bool{}, map[string]string{}, map[string]bool{}
|
|
keyTakes.Unlock()
|
|
})
|
|
p, byName := aWalkOfOne("sudo")
|
|
askModule(t.Context(), p, "sudo", byName)
|
|
fp := "src2:" + strings.Repeat("d", 64)
|
|
k := builderkeys.Key{Module: "sudo", Fingerprint: fp, Found: true, Revision: 1, Build: sdkdelivery.BuilderBuild{
|
|
State: sdkdelivery.BuildDone, Manifest: strings.Repeat("b", 64), Artifacts: []sdkdelivery.BuiltArtifact{{Name: "a",
|
|
Digest: "sha256:1", Reference: "artifact-store://sudo/a/blobs/sha256:1"}}}}
|
|
keys := planKeysOf{values: map[string]sdkdelivery.Module{"sudo@c0ffee0123456789": {FP2: fp}},
|
|
keys: map[string]builderkeys.Key{"sudo": k}}
|
|
records := recordsOf{refused: map[string]string{keyBuildID(k): "sudo is built from novox/mesh-catalog, and this build is of agent/sudo"}}
|
|
if _, err := settleKeys(t.Context(), records, keys, p, p.Tiers[0], func(string, sdkdelivery.Module, string) {}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if p.State != inventory.PlanFailed || !strings.Contains(p.Note, "agent/sudo") {
|
|
t.Fatalf("a refused take-in did not fail the walk: %s %q", p.State, p.Note)
|
|
}
|
|
}
|