Files
mesh-sdk/go/delivery/builder_test.go
T

188 lines
7.5 KiB
Go

package delivery
import (
"errors"
"reflect"
"strings"
"testing"
)
func doneBuild() BuilderBuild {
return BuilderBuild{
State: BuildDone,
By: "builder.novox.1",
Step: "pushed",
Manifest: digestOf('4'),
Artifacts: []BuiltArtifact{
{Name: "code", Kind: "bundle", Digest: "sha256:" + digestOf('5')},
{Name: "server", Kind: "image", Digest: "sha256:" + digestOf('6'), Reference: "registry.example/mail@sha256:" + digestOf('6')},
},
Toolchains: map[string]ToolchainUsed{
"go": {Version: "1.26.0", Recipe: "3"},
"typescript": {Version: "5.9.2", Recipe: "1"},
},
}
}
func builderSamples() map[string]BuilderBuild {
failed := BuilderBuild{State: BuildFailed, By: "builder.novox.1", Step: "test", Error: "go test: 2 failed"}
return map[string]BuilderBuild{
"queued": {State: BuildQueued, Manifest: digestOf('4')},
"building": {State: BuildBuilding, By: "builder.novox.1", Step: "fetch", Manifest: digestOf('4')},
"done": doneBuild(),
"failed": failed,
}
}
// 1358/AC1: the key is <module>.<hex>, parsing gives both back; a module with a dot, an empty one, and a fingerprint not
// of the new form are refused, naming it.
func TestBuilderBuildKey(t *testing.T) {
fp := fp2of('a')
key, err := BuilderBuildKey("mesh-mail", fp)
if err != nil || key != "mesh-mail."+strings.Repeat("a", 64) {
t.Fatalf("key = %q, %v", key, err)
}
module, back, err := ParseBuilderBuildKey(key)
if err != nil || module != "mesh-mail" || back != fp {
t.Errorf("parse = %q, %q, %v", module, back, err)
}
src1, _ := SourceFingerprint(SourceInputs{Module: "m", Tree: "t"})
for module, fp := range map[string]string{"a.b": fp, "": fp, "mesh-mail": src1, "mail ": fp} {
if _, err := BuilderBuildKey(module, fp); err == nil {
t.Errorf("BuilderBuildKey(%q, %q) accepted", module, fp)
}
}
if _, err := BuilderBuildKey("a.b", fp); err == nil || !strings.Contains(err.Error(), "a.b") {
t.Errorf("a dotted module is not named: %v", err)
}
if _, err := BuilderBuildKey("mesh-mail", src1); err == nil || !strings.Contains(err.Error(), src1) {
t.Errorf("a src1 fingerprint is not named: %v", err)
}
for _, bad := range []string{"", "mesh-mail", "mesh-mail.", "mesh-mail.abc", "mesh-mail." + strings.Repeat("A", 64), ".x", "mesh-mail.build.7", "a.b." + strings.Repeat("a", 64)} {
if _, _, err := ParseBuilderBuildKey(bad); err == nil || !strings.Contains(err.Error(), "\""+bad+"\"") {
t.Errorf("ParseBuilderBuildKey(%q) = %v, want a refusal naming it", bad, err)
}
}
}
// 1358/AC2: a build value in each state encodes and decodes unchanged.
func TestBuilderBuildRoundTrip(t *testing.T) {
for name, b := range builderSamples() {
data, err := EncodeBuilderBuild(b)
if err != nil {
t.Fatalf("%s: %v", name, err)
}
if !strings.HasPrefix(string(data), `{"v":1,`) {
t.Errorf("%s at %s", name, data)
}
got, err := DecodeBuilderBuild(data)
b.V = Version
if err != nil || !SameBuilderBuild(got, b) {
t.Errorf("%s: %+v %v", name, got, err)
}
}
}
// 1358/AC3: done without artifacts or without a manifest digest, failed without an error, an artifact without a digest
// and an unknown state are refused.
func TestBuilderBuildChecked(t *testing.T) {
cases := map[string]struct {
edit func(*BuilderBuild)
words []string
}{
"done without artifacts": {func(b *BuilderBuild) { b.Artifacts = nil }, []string{"artifact"}},
"done without a manifest": {func(b *BuilderBuild) { b.Manifest = "" }, []string{"manifest"}},
"a malformed manifest": {func(b *BuilderBuild) { b.Manifest = "abc" }, []string{"abc"}},
"an artifact without a digest": {func(b *BuilderBuild) { b.Artifacts[1].Digest = "" }, []string{"digest", "server"}},
"an artifact without a name": {func(b *BuilderBuild) { b.Artifacts[0].Name = "" }, []string{"name"}},
"an unknown state": {func(b *BuilderBuild) { b.State = "paused" }, []string{"paused"}},
"a toolchain without a version": {func(b *BuilderBuild) { b.Toolchains["go"] = ToolchainUsed{Recipe: "3"} }, []string{"go", "version"}},
"a toolchain without a recipe": {func(b *BuilderBuild) { b.Toolchains["go"] = ToolchainUsed{Version: "1.26.0"} }, []string{"go", "recipe"}},
}
for name, c := range cases {
b := doneBuild()
c.edit(&b)
_, err := EncodeBuilderBuild(b)
if err == nil {
t.Errorf("%s accepted", name)
continue
}
for _, w := range c.words {
if !strings.Contains(err.Error(), w) {
t.Errorf("%s: error %q does not name %q", name, err, w)
}
}
}
f := builderSamples()["failed"]
f.Error = ""
if _, err := EncodeBuilderBuild(f); err == nil || !strings.Contains(err.Error(), "error") {
t.Errorf("failed without an error: %v", err)
}
// a queued or building value may not already carry an outcome
q := builderSamples()["queued"]
q.Artifacts = doneBuild().Artifacts
if _, err := EncodeBuilderBuild(q); err == nil {
t.Error("a queued build with artifacts accepted")
}
// decode refuses what encode refuses
if _, err := DecodeBuilderBuild([]byte(`{"v":1,"state":"done","manifest":"` + digestOf('4') + `"}`)); err == nil {
t.Error("decoded a done build without artifacts")
}
}
// 1358/AC4: a done value names the toolchain version per language and the recipe version; two values that differ only
// there are not the same.
func TestBuilderBuildNamesItsToolchains(t *testing.T) {
a, b, c := doneBuild(), doneBuild(), doneBuild()
b.Toolchains["go"] = ToolchainUsed{Version: "1.26.1", Recipe: "3"}
c.Toolchains["go"] = ToolchainUsed{Version: "1.26.0", Recipe: "4"}
if SameBuilderBuild(a, b) || SameBuilderBuild(a, c) || SameBuilderBuild(b, c) {
t.Error("values differing only in a toolchain's version or recipe are the same")
}
if !SameBuilderBuild(a, doneBuild()) {
t.Error("equal values differ")
}
data, err := EncodeBuilderBuild(a)
if err != nil {
t.Fatal(err)
}
got, err := DecodeBuilderBuild(data)
if err != nil || !reflect.DeepEqual(got.Toolchains, a.Toolchains) {
t.Errorf("toolchains = %+v, %v", got.Toolchains, err)
}
}
// 1358/AC5: a value of another version, or none, is refused with ErrUnknownVersion.
func TestBuilderBuildVersion(t *testing.T) {
for _, in := range []string{`{"v":2,"state":"queued"}`, `{"state":"queued"}`} {
if _, err := DecodeBuilderBuild([]byte(in)); !errors.Is(err, ErrUnknownVersion) {
t.Errorf("DecodeBuilderBuild(%s) = %v", in, err)
}
}
}
// 1358/AC6: the builder's bucket and module do not collide with the controller's build layout (buildrecord.go), which
// this change leaves as it was.
func TestBuilderLayoutDoesNotCollideWithTheControllers(t *testing.T) {
if BuilderBuildsBucket != "mesh-delivery-builder_builds" || BuilderBuildsHistory != 5 || BuilderModule != "mesh-delivery-builder" {
t.Errorf("bucket %s history %d module %s", BuilderBuildsBucket, BuilderBuildsHistory, BuilderModule)
}
if BuilderBuildsBucket == BuildsBucket || BuilderModule == "mesh-controller" {
t.Error("the builder's bucket is the controller's")
}
if BuildsBucket != "mesh-controller_builds" || BuildKey("m", "7") != "m.build.7" || NewestKey("m") != "m.newest" {
t.Error("the controller's layout changed")
}
// the two key forms are never read as each other
key, _ := BuilderBuildKey("m", fp2of('a'))
if key == BuildKey("m", "a") || key == NewestKey("m") {
t.Errorf("builder key %q equals a controller key", key)
}
if _, _, err := ParseBuilderBuildKey(BuildKey("m", digestOf('a'))); err == nil {
t.Error("a controller build key parsed as the builder's")
}
if _, _, err := ParseBuilderBuildKey(NewestKey("m")); err == nil {
t.Error("a controller newest key parsed as the builder's")
}
}