Plan a manifest's artifacts for the build modules and refuse image, upstream and package by kind (issue 1607, AC2)
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
// Package build is the builder's logic (novox/hq ADR 0453 clause 4, ADR 0478), apart from the bus: what a manifest asks
|
||||
// to be built and where, the claim of a build key, the choice of build nodes, the build modules' long steps and the run of
|
||||
// one build. The bus is reached through the small interfaces here, so each part is tested with fakes.
|
||||
package build
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
)
|
||||
|
||||
// The build modules a task is given to, and the language whose toolchain each reports (the indexer's ToolchainModules).
|
||||
const (
|
||||
GoBuildModule = "mesh-build-go"
|
||||
NodeBuildModule = "mesh-build-node"
|
||||
)
|
||||
|
||||
// RefusedPrefix opens the error of a build key that is failed for a kind no builder builds yet (ADR 0478 clause 2): such
|
||||
// a key is not built again as a new revision.
|
||||
const RefusedPrefix = "refused kind:"
|
||||
|
||||
// Task is one artifact to build: its name and kind in the manifest, the build module that builds it and the language
|
||||
// of the toolchain it is built with.
|
||||
type Task struct {
|
||||
Artifact string
|
||||
Kind string
|
||||
Module string
|
||||
Language string
|
||||
}
|
||||
|
||||
// Plan is what a manifest asks the builder to build, by artifact name.
|
||||
type Plan struct{ Tasks []Task }
|
||||
|
||||
// Modules are the build modules the plan calls, sorted.
|
||||
func (p Plan) Modules() []string {
|
||||
seen := map[string]bool{}
|
||||
var out []string
|
||||
for _, t := range p.Tasks {
|
||||
if !seen[t.Module] {
|
||||
seen[t.Module] = true
|
||||
out = append(out, t.Module)
|
||||
}
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// Refused is the error of a plan the builder does not build: an artifact of a kind (or of a language) it has no build
|
||||
// module for.
|
||||
type Refused struct{ Artifact, What string }
|
||||
|
||||
func (r *Refused) Error() string {
|
||||
return fmt.Sprintf("%s %s (artifact %s): the builder builds Go and TypeScript bundles and archives; build-agent builds it", RefusedPrefix, r.What, r.Artifact)
|
||||
}
|
||||
|
||||
// PlanOf is the plan of a manifest (ADR 0478 clauses 1 and 2): a Go bundle and an archive are built by the Go build
|
||||
// module, a TypeScript bundle by the Node one, each in the order of the artifacts' names. A manifest with an image,
|
||||
// upstream or package artifact, or a bundle of another language, is refused as a whole, naming the first such artifact
|
||||
// by name. A manifest with no artifact has an empty plan.
|
||||
func PlanOf(m catalogue.Manifest) (Plan, error) {
|
||||
if m.Build == nil {
|
||||
return Plan{}, nil
|
||||
}
|
||||
artifacts := append([]catalogue.Artifact(nil), m.Build.Artifacts...)
|
||||
sort.Slice(artifacts, func(i, j int) bool { return artifacts[i].Name < artifacts[j].Name })
|
||||
var p Plan
|
||||
for _, a := range artifacts {
|
||||
switch {
|
||||
case a.Kind == catalogue.ArtifactArchive:
|
||||
p.Tasks = append(p.Tasks, Task{Artifact: a.Name, Kind: a.Kind, Module: GoBuildModule, Language: "go"})
|
||||
case a.Kind == catalogue.ArtifactBundle && a.Language == "go":
|
||||
p.Tasks = append(p.Tasks, Task{Artifact: a.Name, Kind: a.Kind, Module: GoBuildModule, Language: "go"})
|
||||
case a.Kind == catalogue.ArtifactBundle && a.Language == "typescript":
|
||||
p.Tasks = append(p.Tasks, Task{Artifact: a.Name, Kind: a.Kind, Module: NodeBuildModule, Language: "typescript"})
|
||||
case a.Kind == catalogue.ArtifactBundle:
|
||||
return Plan{}, &Refused{Artifact: a.Name, What: fmt.Sprintf("bundle of language %q", a.Language)}
|
||||
default:
|
||||
return Plan{}, &Refused{Artifact: a.Name, What: a.Kind}
|
||||
}
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package build
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
)
|
||||
|
||||
// manifestOf is a manifest holding these artifacts: the plan reads only their name, kind and language.
|
||||
func manifestOf(artifacts ...catalogue.Artifact) catalogue.Manifest {
|
||||
return catalogue.Manifest{Module: "m", Build: &catalogue.Build{Artifacts: artifacts}}
|
||||
}
|
||||
|
||||
func TestPlanRoutesEachKindToItsBuildModule(t *testing.T) { // 1607/AC2
|
||||
m := manifestOf(
|
||||
catalogue.Artifact{Name: "tool", Kind: "bundle", Language: "go"},
|
||||
catalogue.Artifact{Name: "web", Kind: "bundle", Language: "typescript"},
|
||||
catalogue.Artifact{Name: "files", Kind: "archive"})
|
||||
p, err := PlanOf(m)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := []Task{
|
||||
{Artifact: "files", Kind: "archive", Module: GoBuildModule, Language: "go"},
|
||||
{Artifact: "tool", Kind: "bundle", Module: GoBuildModule, Language: "go"},
|
||||
{Artifact: "web", Kind: "bundle", Module: NodeBuildModule, Language: "typescript"},
|
||||
}
|
||||
if len(p.Tasks) != len(want) {
|
||||
t.Fatalf("tasks = %+v, want %+v", p.Tasks, want)
|
||||
}
|
||||
for i := range want {
|
||||
if p.Tasks[i] != want[i] {
|
||||
t.Errorf("task %d = %+v, want %+v", i, p.Tasks[i], want[i])
|
||||
}
|
||||
}
|
||||
if modules := p.Modules(); len(modules) != 2 || modules[0] != GoBuildModule || modules[1] != NodeBuildModule {
|
||||
t.Errorf("modules = %v", modules)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanRefusesImageUpstreamAndPackageByName(t *testing.T) { // 1607/AC2, ADR 0478 clause 2
|
||||
for _, c := range []struct {
|
||||
artifact catalogue.Artifact
|
||||
kind string
|
||||
}{
|
||||
{catalogue.Artifact{Name: "img", Kind: "image"}, "image"},
|
||||
{catalogue.Artifact{Name: "up", Kind: "upstream"}, "upstream"},
|
||||
{catalogue.Artifact{Name: "pkg", Kind: "package", Language: "typescript"}, "package"},
|
||||
} {
|
||||
m := manifestOf(catalogue.Artifact{Name: "ok", Kind: "archive"}, c.artifact)
|
||||
p, err := PlanOf(m)
|
||||
if err == nil {
|
||||
t.Errorf("%s: plan %+v, want a refusal", c.kind, p)
|
||||
continue
|
||||
}
|
||||
var r *Refused
|
||||
if !asRefused(err, &r) || !strings.HasPrefix(err.Error(), RefusedPrefix) || !strings.Contains(err.Error(), c.kind) || !strings.Contains(err.Error(), "build-agent") {
|
||||
t.Errorf("%s: error = %v, want it to open %q and name the kind and build-agent", c.kind, err, RefusedPrefix)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanRefusesABundleOfAnotherLanguageAndAModuleWithNoArtifact(t *testing.T) {
|
||||
if _, err := PlanOf(manifestOf(catalogue.Artifact{Name: "x", Kind: "bundle", Language: "python"})); err == nil || !strings.HasPrefix(err.Error(), RefusedPrefix) {
|
||||
t.Errorf("a python bundle: error = %v, want a refusal", err)
|
||||
}
|
||||
p, err := PlanOf(manifestOf())
|
||||
if err != nil || len(p.Tasks) != 0 {
|
||||
t.Errorf("no artifact: plan %+v error %v, want an empty plan", p, err)
|
||||
}
|
||||
}
|
||||
|
||||
func asRefused(err error, target **Refused) bool { return errors.As(err, target) }
|
||||
Reference in New Issue
Block a user