mesh-delivery-sdk buildstep: Toolchain, ToolchainKey and the codec of a node's toolchains value, shared by the three build modules, move here from mesh-build-go's own package; a test reads each seat's fixture strictly into it (B2g; ADR 0634 section 6; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
package buildstep
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ToolchainVersion is the layout version of a toolchains value.
|
||||
const ToolchainVersion = 1
|
||||
|
||||
// Toolchain is the value of a build module's state toolchains under the key <node>.<language>: the version of the
|
||||
// language's toolchain installed on the node, the build module's recipe version, and when the module saw them. It is
|
||||
// the part build-step's `Toolchain`, the layout every node-build seat gives that state; the three build modules share
|
||||
// this neutral form (a seat's generated type is one package per seat). A test reads each seat's fixture strictly into it.
|
||||
type Toolchain struct {
|
||||
V int `json:"v,omitempty"`
|
||||
Language string `json:"language"`
|
||||
Version string `json:"version"`
|
||||
Recipe string `json:"recipe"`
|
||||
At time.Time `json:"at,omitzero"`
|
||||
}
|
||||
|
||||
func checkToolchainLanguage(language string) error {
|
||||
switch language {
|
||||
case "go", "typescript":
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("buildstep: the toolchain language %q is not go or typescript", language)
|
||||
}
|
||||
|
||||
// ToolchainKey is the key of a node's toolchain of one language: <node>.<language>. It refuses an empty node, a node
|
||||
// with a dot and a language other than go or typescript.
|
||||
func ToolchainKey(node, language string) (string, error) {
|
||||
if node == "" || strings.ContainsAny(node, ". ") {
|
||||
return "", fmt.Errorf("buildstep: %q is not a node name", node)
|
||||
}
|
||||
if err := checkToolchainLanguage(language); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return node + "." + language, nil
|
||||
}
|
||||
|
||||
func (t Toolchain) check() error {
|
||||
if err := checkToolchainLanguage(t.Language); err != nil {
|
||||
return err
|
||||
}
|
||||
if t.Version == "" || t.Recipe == "" {
|
||||
return fmt.Errorf("buildstep: a %s toolchain needs its version and its recipe", t.Language)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// EncodeToolchain is t as JSON; it refuses a language other than go or typescript and an empty version or recipe.
|
||||
func EncodeToolchain(t Toolchain) ([]byte, error) {
|
||||
if err := t.check(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t.V = ToolchainVersion
|
||||
return json.Marshal(t)
|
||||
}
|
||||
|
||||
// DecodeToolchain reads a toolchains value, refusing an unknown field, another layout version and what
|
||||
// EncodeToolchain refuses.
|
||||
func DecodeToolchain(data []byte) (Toolchain, error) {
|
||||
var t Toolchain
|
||||
dec := json.NewDecoder(bytes.NewReader(data))
|
||||
dec.DisallowUnknownFields()
|
||||
if err := dec.Decode(&t); err != nil {
|
||||
return Toolchain{}, fmt.Errorf("buildstep: a toolchain does not read: %w", err)
|
||||
}
|
||||
if t.V != ToolchainVersion {
|
||||
return Toolchain{}, fmt.Errorf("buildstep: a toolchain of version %d, this reads %d", t.V, ToolchainVersion)
|
||||
}
|
||||
if err := t.check(); err != nil {
|
||||
return Toolchain{}, err
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package buildstep
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// The toolchains value and key are the layout all three node-build seats generate: each seat's fixture of the state
|
||||
// reads strictly into Toolchain, and the key is the seat's key form <node>.<language>.
|
||||
func TestTheToolchainsValueAndKeyAreTheSeatsLayout(t *testing.T) {
|
||||
read := 0
|
||||
for _, seat := range []string{"node-build-go", "node-build-image", "node-build-node"} {
|
||||
raw, err := os.ReadFile(filepath.Join("..", "..", "contracts", "seats", seat, "fixtures", "states.json"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var fixtures []struct {
|
||||
Of string `json:"of"`
|
||||
Given json.RawMessage `json:"given"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &fixtures); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, f := range fixtures {
|
||||
if f.Of != "state:toolchains/<node>.<language>" {
|
||||
continue
|
||||
}
|
||||
if back, err := DecodeToolchain(f.Given); err != nil || back.Version == "" {
|
||||
t.Errorf("%s: the fixture reads as %+v, %v", seat, back, err)
|
||||
}
|
||||
if _, err := EncodeToolchain(mustToolchain(t, f.Given)); err != nil {
|
||||
t.Errorf("%s: %v", seat, err)
|
||||
}
|
||||
dec := json.NewDecoder(bytes.NewReader(f.Given))
|
||||
dec.DisallowUnknownFields()
|
||||
if err := dec.Decode(&Toolchain{}); err != nil {
|
||||
t.Errorf("%s: %v", seat, err)
|
||||
}
|
||||
read++
|
||||
}
|
||||
}
|
||||
if read != 3 {
|
||||
t.Errorf("%d toolchain fixtures read, want 3", read)
|
||||
}
|
||||
raw, err := EncodeToolchain(Toolchain{Language: "go", Version: "go1.27.1", Recipe: "3", At: time.Date(2026, 11, 5, 10, 0, 0, 0, time.UTC)})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if back, err := DecodeToolchain(raw); err != nil || back.Version != "go1.27.1" || back.At.IsZero() {
|
||||
t.Errorf("the value reads back as %+v, %v", back, err)
|
||||
}
|
||||
if key, err := ToolchainKey("ace", "typescript"); err != nil || key != "ace.typescript" {
|
||||
t.Errorf("key %q, %v", key, err)
|
||||
}
|
||||
for _, bad := range [][2]string{{"", "go"}, {"a.b", "go"}, {"ace", "rust"}} {
|
||||
if _, err := ToolchainKey(bad[0], bad[1]); err == nil {
|
||||
t.Errorf("the key of %v was made", bad)
|
||||
}
|
||||
}
|
||||
if _, err := EncodeToolchain(Toolchain{Language: "go", Version: "go1.27"}); err == nil {
|
||||
t.Error("a toolchain without its recipe was written")
|
||||
}
|
||||
if _, err := DecodeToolchain([]byte(`{"v":1,"language":"go","version":"x","recipe":"1","extra":1}`)); err == nil || !strings.Contains(err.Error(), "extra") {
|
||||
t.Errorf("a field the layout lacks was read: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func mustToolchain(t *testing.T, raw []byte) Toolchain {
|
||||
t.Helper()
|
||||
var tc Toolchain
|
||||
if err := json.Unmarshal(raw, &tc); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return tc
|
||||
}
|
||||
Reference in New Issue
Block a user