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) } }