Files
mesh-catalog/modules/mesh-build-go/cmd/mesh-build-go/push_test.go
T
jschoubben b6247f837d mesh-build-go: push each done archive to the artifact store from its own process and answer its reference (issue 1587, S1-S4, ADR 0498 §1 §2, lead decision L9)
After the build ended the module pushes the packed bundle or archive with mesh-sdk's
PushBlob under <module>/<artifact> by its digest, a candidate's as a trunk build's, and the
step answers artifact-store://<module>/<artifact>/blobs/<digest> instead of a path. A push
that fails fails the step not stored: <artifact>: <the store's words>. The packed bytes are
never written to the node. The manifest requires artifact-store and places its address.
2026-10-22 18:22:17 +02:00

121 lines
4.4 KiB
Go

package main
import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"os"
"path/filepath"
"strings"
"sync"
"testing"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// pushed are the blobs the fake artifact store took, by digest, for the tests that read an archive back.
var pushed sync.Map
// fakeStore makes m push to a store of the test's own: its address in RegistryFile, each blob kept by repository and
// digest; fail, when set, is the store's refusal.
type fakeStore struct {
mu sync.Mutex
blobs map[string][]byte // "<repository>@<digest>"
fail error
addr string
}
func withStore(t *testing.T, m *Module) *fakeStore {
t.Helper()
s := &fakeStore{blobs: map[string][]byte{}}
m.RegistryFile = filepath.Join(t.TempDir(), "registry")
if err := os.WriteFile(m.RegistryFile, []byte("store.test:5000\n"), 0o644); err != nil {
t.Fatal(err)
}
m.Push = func(ctx context.Context, registry, repository, digest string, body []byte) error {
s.mu.Lock()
defer s.mu.Unlock()
s.addr = registry
if s.fail != nil {
return s.fail
}
sum := sha256.Sum256(body)
if "sha256:"+hex.EncodeToString(sum[:]) != digest {
t.Errorf("pushed %s with bytes of another digest", digest)
}
s.blobs[repository+"@"+digest] = append([]byte(nil), body...)
pushed.Store(digest, s.blobs[repository+"@"+digest])
return nil
}
return s
}
// archiveOf is the archive a build pushed, by its digest.
func archiveOf(t *testing.T, res BuildResult) []byte {
t.Helper()
raw, ok := pushed.Load(res.Digest)
if !ok {
t.Fatalf("nothing was pushed under %s", res.Digest)
}
return raw.([]byte)
}
// 1587-S2 (ADR 0498 §1): a done build answers a reference of the stated form whose tail is the build's digest, and the
// store holds the archive there; a bundle and an archive alike.
func TestADoneBuildAnswersTheReferenceOfTheArchiveItPushed(t *testing.T) {
m, states, _ := buildModule(t, manifestWith(goBundle), "the program")
store := withStore(t, m)
res, st := runBuild(t, m, states, "tools")
if st.State != delivery.StepPassed {
t.Fatalf("the step ended %s: %s", st.State, st.Reason)
}
if want := "artifact-store://demo/tools/blobs/" + res.Digest; res.Reference != want {
t.Errorf("reference %q, want %q", res.Reference, want)
}
if store.addr != "store.test:5000" || store.blobs["demo/tools@"+res.Digest] == nil {
t.Errorf("the store at %q holds %d blobs, not demo/tools@%s", store.addr, len(store.blobs), res.Digest)
}
a, states2, _ := archiveModule(t, "")
withStore(t, a)
arch, st := runBuild(t, a, states2, "files")
if st.State != delivery.StepPassed || arch.Reference != "artifact-store://demo/files/blobs/"+arch.Digest {
t.Errorf("the archive artifact: %s %q, reference %q", st.State, st.Reason, arch.Reference)
}
}
// 1587-S3 (ADR 0498 §2): a push the store refuses fails the step `not stored: <artifact>: <the store's words>`, and so
// does a store whose address the manifest did not place.
func TestARefusedPushFailsTheStepNotStored(t *testing.T) {
m, states, _ := buildModule(t, manifestWith(goBundle), "the program")
store := withStore(t, m)
store.fail = errors.New("store.test:5000 refused the blob: 500 Internal Server Error disk full")
if _, st := runBuild(t, m, states, "tools"); st.State != delivery.StepFailed || !strings.HasPrefix(st.Reason, delivery.NotStored+"tools: ") || !strings.Contains(st.Reason, "disk full") {
t.Errorf("the step ended %s: %q", st.State, st.Reason)
}
m.RegistryFile = filepath.Join(t.TempDir(), "absent")
if _, st := runBuild(t, m, states, "tools"); st.State != delivery.StepFailed || !strings.HasPrefix(st.Reason, delivery.NotStored+"tools: ") {
t.Errorf("no address: the step ended %s: %q", st.State, st.Reason)
}
}
// 1587-S4 (the lead's decision L9): the packed file is not left on the node once its push answered, a candidate's
// included: the workspace holds no archive, pushed or refused.
func TestThePackedFileLeavesTheNodeOnceItsPushAnswered(t *testing.T) {
for _, fail := range []error{nil, errors.New("refused")} {
m, states, _ := buildModule(t, manifestWith(goBundle), "the program")
withStore(t, m).fail = fail
runBuild(t, m, states, "tools")
var archives []string
filepath.WalkDir(m.Work, func(p string, d os.DirEntry, err error) error {
if err == nil && strings.HasSuffix(p, ".tar.gz") {
archives = append(archives, p)
}
return nil
})
if len(archives) != 0 {
t.Errorf("push answered %v: archives left on the node: %v", fail, archives)
}
}
}