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 ., 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") } }