Files
zeta-reconstructor/internal/zeta/recipe_test.go
T

185 lines
5.7 KiB
Go

package zeta
import (
"os"
"path/filepath"
"strings"
"testing"
)
// TestRecipe mirrors the zeta-toolchain recipe.lua validation rules:
// name (sanitize_name), version, summary, url required non-empty;
// sha256 empty-or-64-hex; deps non-empty strings; build_script required
// (build_system is always "custom" in v1).
func TestRecipe(t *testing.T) {
valid := func() *Recipe {
return &Recipe{
Name: "mytool",
Version: "1.0",
Summary: "Example custom-build package",
URL: "mytool-1.0.tar.gz",
SHA256: "",
Deps: []string{"libz"},
BuildScript: "build.sh",
}
}
tests := []struct {
name string
mutate func(r *Recipe)
wantErr bool
}{
{"valid recipe", func(r *Recipe) {}, false},
{"empty name", func(r *Recipe) { r.Name = "" }, true},
{"name with slash", func(r *Recipe) { r.Name = "bad/name" }, true},
{"name with leading dot", func(r *Recipe) { r.Name = ".hidden" }, true},
{"empty version", func(r *Recipe) { r.Version = "" }, true},
{"empty summary", func(r *Recipe) { r.Summary = "" }, true},
{"empty url", func(r *Recipe) { r.URL = "" }, true},
{"empty build_script", func(r *Recipe) { r.BuildScript = "" }, true},
{"bad sha256", func(r *Recipe) { r.SHA256 = "zzz" }, true},
{"short sha256 (63 hex)", func(r *Recipe) { r.SHA256 = strings.Repeat("a", 63) }, true},
{"empty sha256 is allowed", func(r *Recipe) { r.SHA256 = "" }, false},
{"64-hex sha256 allowed", func(r *Recipe) { r.SHA256 = strings.Repeat("f", 64) }, false},
{"empty dep string", func(r *Recipe) { r.Deps = []string{"libz", ""} }, true},
{"no deps allowed", func(r *Recipe) { r.Deps = nil }, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := valid()
tt.mutate(r)
err := r.Validate()
if (err != nil) != tt.wantErr {
t.Fatalf("Validate() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestRecipeEmit(t *testing.T) {
// Byte-exact render for the canonical custom recipe. Field keys are
// padded to width 13 before " = ", matching recipe.lua and the
// custom-demo/custom.recipe example.
canonical := &Recipe{
Name: "mytool",
Version: "1.0",
Summary: "Example custom-build package",
URL: "mytool-1.0.tar.gz",
SHA256: "",
Deps: nil,
BuildScript: "build.sh",
}
want := `return {
name = "mytool",
version = "1.0",
summary = "Example custom-build package",
url = "mytool-1.0.tar.gz",
deps = {},
build_system = "custom",
build_script = "build.sh",
}
`
out, err := canonical.Emit()
if err != nil {
t.Fatalf("Emit() error = %v", err)
}
if out != want {
t.Fatalf("Emit() mismatch:\n--- want ---\n%s\n--- got ---\n%s", want, out)
}
// Critical: empty SHA256 must OMIT the sha256 line entirely. recipe.lua
// treats sha256 = "" as present (raw.sha256 ~= nil) and fails the
// 64-hex check, so the line must never be emitted empty.
if strings.Contains(out, "sha256") {
t.Fatalf("Emit() with empty SHA256 must not contain a sha256 line, got:\n%s", out)
}
// Non-empty SHA256 emits the line after url and before deps.
withSHA := *canonical
withSHA.SHA256 = strings.Repeat("a", 64)
outSHA, err := withSHA.Emit()
if err != nil {
t.Fatalf("Emit() with sha256 error = %v", err)
}
shaLine := ` sha256 = "` + strings.Repeat("a", 64) + `",`
if !strings.Contains(outSHA, shaLine) {
t.Fatalf("Emit() missing aligned sha256 line, got:\n%s", outSHA)
}
if !(strings.Index(outSHA, "url") < strings.Index(outSHA, "sha256") &&
strings.Index(outSHA, "sha256") < strings.Index(outSHA, "deps")) {
t.Fatalf("Emit() sha256 not between url and deps, got:\n%s", outSHA)
}
// deps render on one line; empty deps render as {}.
withDeps := *canonical
withDeps.Deps = []string{"libz", "libfoo"}
outDeps, err := withDeps.Emit()
if err != nil {
t.Fatalf("Emit() with deps error = %v", err)
}
if !strings.Contains(outDeps, ` deps = { "libz", "libfoo" },`) {
t.Fatalf("Emit() deps line mismatch, got:\n%s", outDeps)
}
// Escape double quotes and newlines in summary and deps values.
escaped := *canonical
escaped.Summary = "say \"hi\"\nsecond line"
escaped.Deps = []string{"say \"hi\""}
outEsc, err := escaped.Emit()
if err != nil {
t.Fatalf("Emit() with escapes error = %v", err)
}
if !strings.Contains(outEsc, ` summary = "say \"hi\"\nsecond line",`) {
t.Fatalf("Emit() summary escape mismatch, got:\n%s", outEsc)
}
if !strings.Contains(outEsc, ` deps = { "say \"hi\"" },`) {
t.Fatalf("Emit() deps escape mismatch, got:\n%s", outEsc)
}
// Emit of an invalid recipe returns the validation error.
invalid := *canonical
invalid.Name = ""
if _, err := invalid.Emit(); err == nil {
t.Fatal("Emit() of invalid recipe should return an error")
}
}
func TestRecipeGolden(t *testing.T) {
r := &Recipe{
Name: "mytool",
Version: "1.0",
Summary: "Example custom-build package",
URL: "mytool-1.0.tar.gz",
SHA256: "",
Deps: nil,
BuildScript: "build.sh",
}
out, err := r.Emit()
if err != nil {
t.Fatalf("Emit() error = %v", err)
}
// Golden files live at the repo root so todos 5/12/14 can share them.
golden := filepath.Join("..", "..", "testdata", "golden", "mytool.recipe")
if os.Getenv("UPDATE_GOLDEN") == "1" {
if err := os.MkdirAll(filepath.Dir(golden), 0o755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}
if err := os.WriteFile(golden, []byte(out), 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
t.Logf("wrote golden %s", golden)
return
}
want, err := os.ReadFile(golden)
if err != nil {
t.Fatalf("ReadFile golden %s: %v (run with UPDATE_GOLDEN=1 to create)", golden, err)
}
if string(want) != out {
t.Fatalf("golden mismatch:\n--- want ---\n%s\n--- got ---\n%s", want, out)
}
}