feat(zeta): manifest model, validation, package.lua emitter
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <[email protected]>
This commit is contained in:
@@ -0,0 +1,251 @@
|
|||||||
|
// Package zeta models the Zeta package formats (package.lua manifests and
|
||||||
|
// .recipe files) and emits them byte-for-byte the way the Zeta toolchain does.
|
||||||
|
//
|
||||||
|
// The Manifest here covers archive mode only (v1 of zeta-reconstruct emits
|
||||||
|
// only archive-mode package.lua); install/build strategies are out of scope.
|
||||||
|
package zeta
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Manifest is the Go model of a Zeta package.lua manifest (archive mode).
|
||||||
|
//
|
||||||
|
// Arch and Files mirror Zeta's KNOWN_KEYS (ZETA/lib/manifest.lua) and are kept
|
||||||
|
// for forward compatibility; they are not populated in v1 and never emitted.
|
||||||
|
// Archive is the (v1-only) install strategy: `archive = { strip = N }`.
|
||||||
|
type Manifest struct {
|
||||||
|
Name string
|
||||||
|
Version string
|
||||||
|
Summary string
|
||||||
|
URL string
|
||||||
|
SHA256 string
|
||||||
|
Arch string
|
||||||
|
Deps []string
|
||||||
|
Files []string
|
||||||
|
Archive *Archive
|
||||||
|
}
|
||||||
|
|
||||||
|
// Archive declares the tarball strip level, mirroring Zeta's
|
||||||
|
// `archive = { strip = N }` install strategy.
|
||||||
|
type Archive struct {
|
||||||
|
Strip int
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// nameRe mirrors ZETA/lib/path.lua sanitize_name's character class
|
||||||
|
// ([%w%._+%-]+, i.e. [A-Za-z0-9_.+-]).
|
||||||
|
nameRe = regexp.MustCompile(`^[A-Za-z0-9_.+-]+$`)
|
||||||
|
|
||||||
|
// sha256Re mirrors manifest.lua's HEX64: ^ + 64 × %x + $.
|
||||||
|
sha256Re = regexp.MustCompile(`^[0-9a-f]{64}$`)
|
||||||
|
|
||||||
|
// httpRe mirrors manifest.lua's `^https?://` remote-URL test.
|
||||||
|
httpRe = regexp.MustCompile(`^https?://`)
|
||||||
|
|
||||||
|
// unsafeURLRe mirrors manifest.lua's local-URL safety test: a URL that is
|
||||||
|
// not http(s) may not contain whitespace or backslashes.
|
||||||
|
unsafeURLRe = regexp.MustCompile(`[\s\\]`)
|
||||||
|
|
||||||
|
// depSpecRe mirrors vercmp.lua parse_dep's first step:
|
||||||
|
// leading whitespace, a name of [%w%._%+%-]+ characters, whitespace, and
|
||||||
|
// the rest of the spec (constraint or empty).
|
||||||
|
depSpecRe = regexp.MustCompile(`^\s*([A-Za-z0-9_.+-]+)\s*(.*)$`)
|
||||||
|
|
||||||
|
// depOpRe and depVerRe mirror parse_dep's operator/version extraction.
|
||||||
|
depOpRe = regexp.MustCompile(`^([<>=~]+)\s*`)
|
||||||
|
depVerRe = regexp.MustCompile(`^[<>=~]+\s*([A-Za-z0-9_.+-]+)\s*$`)
|
||||||
|
|
||||||
|
// validDepOps mirrors ZETA/lib/vercmp.lua's OPS table.
|
||||||
|
validDepOps = map[string]bool{
|
||||||
|
">=": true, "<=": true, "==": true, "~=": true,
|
||||||
|
">": true, "<": true, "=": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanReDotSlash/cleanReLeadSlash/cleanReTrailSlash mirror the three
|
||||||
|
// anchored gsubs of manifest.lua's clean_rel, in the same order.
|
||||||
|
cleanReDotSlash = regexp.MustCompile(`^\./+`)
|
||||||
|
cleanReLeadSlash = regexp.MustCompile(`^/+`)
|
||||||
|
cleanReTrailSlash = regexp.MustCompile(`/+$`)
|
||||||
|
)
|
||||||
|
|
||||||
|
// validName mirrors ZETA/lib/path.lua sanitize_name: non-empty, no leading
|
||||||
|
// dot, and every character in [A-Za-z0-9_.+-].
|
||||||
|
func validName(name string) bool {
|
||||||
|
if name == "" || strings.HasPrefix(name, ".") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return nameRe.MatchString(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanRel mirrors manifest.lua's clean_rel: strip a "./" prefix, a "/"
|
||||||
|
// prefix, and a "/" suffix (in that order), producing the canonical
|
||||||
|
// root-relative form.
|
||||||
|
func cleanRel(p string) string {
|
||||||
|
p = cleanReDotSlash.ReplaceAllString(p, "")
|
||||||
|
p = cleanReLeadSlash.ReplaceAllString(p, "")
|
||||||
|
p = cleanReTrailSlash.ReplaceAllString(p, "")
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// relativeInside mirrors ZETA/lib/path.lua relative_inside: true if the
|
||||||
|
// relative path never climbs above its root via ".." segments.
|
||||||
|
func relativeInside(p string) bool {
|
||||||
|
depth := 0
|
||||||
|
for _, seg := range strings.Split(p, "/") {
|
||||||
|
switch seg {
|
||||||
|
case "..":
|
||||||
|
depth--
|
||||||
|
if depth < 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case ".", "":
|
||||||
|
// "." and empty segments (from "//") neither climb nor descend.
|
||||||
|
default:
|
||||||
|
depth++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// escape mirrors packager.lua's escape: quote backslash-quoting first, then
|
||||||
|
// newline escaping, in the same order as the Lua gsub chain.
|
||||||
|
func escape(s string) string {
|
||||||
|
s = strings.ReplaceAll(s, `"`, `\"`)
|
||||||
|
s = strings.ReplaceAll(s, "\n", `\n`)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseDep mirrors ZETA/lib/vercmp.lua parse_dep:
|
||||||
|
// "pcre2>=10.42" -> ("pcre2", ">=", "10.42"), "libffi" -> ("libffi", "", "").
|
||||||
|
// A bare "=" operator is normalized to "==", as in vercmp.lua.
|
||||||
|
func parseDep(spec string) (name, op, version string, err error) {
|
||||||
|
sm := depSpecRe.FindStringSubmatch(spec)
|
||||||
|
if sm == nil {
|
||||||
|
return "", "", "", fmt.Errorf("bad dependency %q", spec)
|
||||||
|
}
|
||||||
|
name, rest := sm[1], sm[2]
|
||||||
|
if rest == "" {
|
||||||
|
return name, "", "", nil
|
||||||
|
}
|
||||||
|
om := depOpRe.FindStringSubmatch(rest)
|
||||||
|
vm := depVerRe.FindStringSubmatch(rest)
|
||||||
|
if om == nil || vm == nil || !validDepOps[om[1]] {
|
||||||
|
return "", "", "", fmt.Errorf("bad dependency constraint %q (expected NAME OP VERSION)", spec)
|
||||||
|
}
|
||||||
|
op = om[1]
|
||||||
|
version = vm[1]
|
||||||
|
if op == "=" {
|
||||||
|
op = "=="
|
||||||
|
}
|
||||||
|
return name, op, version, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate checks the manifest against Zeta's own rules, mirroring
|
||||||
|
// ZETA/lib/manifest.lua normalize exactly (archive path only):
|
||||||
|
//
|
||||||
|
// 1. name: non-empty, no leading dot, all of [A-Za-z0-9_.+-]
|
||||||
|
// 2. version: non-empty
|
||||||
|
// 3. sha256: when set, exactly 64 hex characters (uppercase accepted and
|
||||||
|
// normalized to lowercase, as manifest.lua does)
|
||||||
|
// 4. url: non-http(s) URLs must not contain whitespace or backslashes
|
||||||
|
// 5. deps: each entry must parse as NAME [OP VERSION] with a known operator,
|
||||||
|
// and must not depend on the package itself
|
||||||
|
// 6. files: entries must be non-empty, clean to a path that stays inside the
|
||||||
|
// install root, and appear only once
|
||||||
|
// 7. archive.strip: non-negative integer
|
||||||
|
// 8. archive mode requires a url (v1 is archive-only, so url is always
|
||||||
|
// required)
|
||||||
|
// 9. remote http(s) urls must declare sha256
|
||||||
|
func (m *Manifest) Validate() error {
|
||||||
|
if !validName(m.Name) {
|
||||||
|
return fmt.Errorf("manifest: missing or invalid package name")
|
||||||
|
}
|
||||||
|
if m.Version == "" {
|
||||||
|
return fmt.Errorf("manifest: package %q missing version", m.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.SHA256 != "" {
|
||||||
|
lower := strings.ToLower(m.SHA256)
|
||||||
|
if !sha256Re.MatchString(lower) {
|
||||||
|
return fmt.Errorf("manifest: package %q sha256 must be exactly 64 hex characters", m.Name)
|
||||||
|
}
|
||||||
|
m.SHA256 = lower
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.URL != "" && !httpRe.MatchString(m.URL) && unsafeURLRe.MatchString(m.URL) {
|
||||||
|
return fmt.Errorf("manifest: package %q url %q looks unsafe", m.Name, m.URL)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, d := range m.Deps {
|
||||||
|
depName, _, _, err := parseDep(d)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("manifest: package %q: %w", m.Name, err)
|
||||||
|
}
|
||||||
|
if depName == m.Name {
|
||||||
|
return fmt.Errorf("manifest: package %q depends on itself", m.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
seen := make(map[string]bool, len(m.Files))
|
||||||
|
for _, f := range m.Files {
|
||||||
|
if f == "" {
|
||||||
|
return fmt.Errorf("manifest: package %q has an empty files entry", m.Name)
|
||||||
|
}
|
||||||
|
rel := cleanRel(f)
|
||||||
|
if rel == "" || !relativeInside(rel) {
|
||||||
|
return fmt.Errorf("manifest: package %q files entry %q escapes the root", m.Name, f)
|
||||||
|
}
|
||||||
|
if seen[rel] {
|
||||||
|
return fmt.Errorf("manifest: package %q lists %q twice in files", m.Name, rel)
|
||||||
|
}
|
||||||
|
seen[rel] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.Archive != nil && m.Archive.Strip < 0 {
|
||||||
|
return fmt.Errorf("manifest: package %q archive.strip must be a non-negative integer", m.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.URL == "" {
|
||||||
|
return fmt.Errorf("manifest: package %q archive mode requires a url", m.Name)
|
||||||
|
}
|
||||||
|
if httpRe.MatchString(m.URL) && m.SHA256 == "" {
|
||||||
|
return fmt.Errorf("manifest: package %q must declare sha256 for a remote url", m.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit renders the archive-mode package.lua with the exact byte format of
|
||||||
|
// packager.lua's write_manifest: 2-space indent, "="-aligned field names in
|
||||||
|
// the order name/version/summary/url/sha256/deps/archive, `archive = { strip = 1 }`,
|
||||||
|
// and no test function. Summary and deps values are escaped (quote/newline);
|
||||||
|
// name/version/url/sha256 are emitted raw (they are validated-safe). Emit
|
||||||
|
// fails if the manifest does not Validate.
|
||||||
|
func (m *Manifest) Emit() (string, error) {
|
||||||
|
if err := m.Validate(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString("return {\n")
|
||||||
|
b.WriteString(fmt.Sprintf(" %-8s= \"%s\",\n", "name", m.Name))
|
||||||
|
b.WriteString(fmt.Sprintf(" %-8s= \"%s\",\n", "version", m.Version))
|
||||||
|
b.WriteString(fmt.Sprintf(" %-8s= \"%s\",\n", "summary", escape(m.Summary)))
|
||||||
|
b.WriteString(fmt.Sprintf(" %-8s= \"%s\",\n", "url", m.URL))
|
||||||
|
b.WriteString(fmt.Sprintf(" %-8s= \"%s\",\n", "sha256", m.SHA256))
|
||||||
|
if len(m.Deps) > 0 {
|
||||||
|
items := make([]string, len(m.Deps))
|
||||||
|
for i, d := range m.Deps {
|
||||||
|
items[i] = `"` + escape(d) + `"`
|
||||||
|
}
|
||||||
|
b.WriteString(fmt.Sprintf(" %-8s= { %s },\n", "deps", strings.Join(items, ", ")))
|
||||||
|
} else {
|
||||||
|
b.WriteString(fmt.Sprintf(" %-8s= {},\n", "deps"))
|
||||||
|
}
|
||||||
|
b.WriteString(fmt.Sprintf(" %-8s= { strip = 1 },\n", "archive"))
|
||||||
|
b.WriteString("}\n")
|
||||||
|
return b.String(), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,276 @@
|
|||||||
|
package zeta
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// validManifest returns a fully valid archive-mode manifest that every
|
||||||
|
// table case can copy-and-mutate.
|
||||||
|
func validManifest() *Manifest {
|
||||||
|
return &Manifest{
|
||||||
|
Name: "hello",
|
||||||
|
Version: "1.0",
|
||||||
|
Summary: "A tiny demonstration program",
|
||||||
|
URL: "https://example.org/packages/hello/hello-1.0.tar.gz",
|
||||||
|
SHA256: "731887527d1a72c57d1e64bad85cc341a4285354e773b2cd277420b744df2943",
|
||||||
|
Archive: &Archive{Strip: 1},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestManifest(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
mutate func(m *Manifest)
|
||||||
|
wantErr bool
|
||||||
|
wantErrSub string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid archive manifest",
|
||||||
|
mutate: func(m *Manifest) {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing version",
|
||||||
|
mutate: func(m *Manifest) {
|
||||||
|
m.Version = ""
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
wantErrSub: "missing version",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bad sha256",
|
||||||
|
mutate: func(m *Manifest) {
|
||||||
|
m.SHA256 = "zzz"
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
wantErrSub: "64 hex",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "archive without url",
|
||||||
|
mutate: func(m *Manifest) {
|
||||||
|
m.URL = ""
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
wantErrSub: "archive mode requires a url",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "remote url without sha256",
|
||||||
|
mutate: func(m *Manifest) {
|
||||||
|
m.SHA256 = ""
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
wantErrSub: "must declare sha256 for a remote url",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "self dep",
|
||||||
|
mutate: func(m *Manifest) {
|
||||||
|
m.Deps = []string{"hello>=1.0"}
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
wantErrSub: "depends on itself",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "dep with bad operator",
|
||||||
|
mutate: func(m *Manifest) {
|
||||||
|
m.Deps = []string{"pcre2<<10.42"}
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
wantErrSub: "bad dependency constraint",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "dep without operator",
|
||||||
|
mutate: func(m *Manifest) {
|
||||||
|
m.Deps = []string{"pcre2 10.42"}
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
wantErrSub: "bad dependency constraint",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "dotdot in files",
|
||||||
|
mutate: func(m *Manifest) {
|
||||||
|
m.Files = []string{"../evil"}
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
wantErrSub: "escapes the root",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty files entry",
|
||||||
|
mutate: func(m *Manifest) {
|
||||||
|
m.Files = []string{""}
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
wantErrSub: "empty files entry",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "duplicate files entry",
|
||||||
|
mutate: func(m *Manifest) {
|
||||||
|
m.Files = []string{"usr/bin/hello", "usr/bin/hello"}
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
wantErrSub: "twice",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "negative strip",
|
||||||
|
mutate: func(m *Manifest) {
|
||||||
|
m.Archive = &Archive{Strip: -1}
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
wantErrSub: "non-negative integer",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "leading dot name",
|
||||||
|
mutate: func(m *Manifest) {
|
||||||
|
m.Name = ".hidden"
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
wantErrSub: "invalid package name",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "slash in name",
|
||||||
|
mutate: func(m *Manifest) {
|
||||||
|
m.Name = "a/b"
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
wantErrSub: "invalid package name",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unsafe local url",
|
||||||
|
mutate: func(m *Manifest) {
|
||||||
|
m.URL = "/tmp/foo bar"
|
||||||
|
m.SHA256 = ""
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
wantErrSub: "looks unsafe",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid deps with constraints",
|
||||||
|
mutate: func(m *Manifest) {
|
||||||
|
m.Deps = []string{"pcre2>=10.42", "libffi", "libz=1.2.13", "foo == 1.0"}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "local url without sha256 ok",
|
||||||
|
mutate: func(m *Manifest) {
|
||||||
|
m.URL = "/var/cache/zeta/hello-1.0.tar.gz"
|
||||||
|
m.SHA256 = ""
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid files entries cleaned",
|
||||||
|
mutate: func(m *Manifest) {
|
||||||
|
m.Files = []string{"usr/bin/hello", "/usr/share/doc/", "./etc/zeta"}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty summary ok",
|
||||||
|
mutate: func(m *Manifest) {
|
||||||
|
m.Summary = ""
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
m := validManifest()
|
||||||
|
tt.mutate(m)
|
||||||
|
err := m.Validate()
|
||||||
|
if tt.wantErr {
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Validate() = nil, want error containing %q", tt.wantErrSub)
|
||||||
|
}
|
||||||
|
if tt.wantErrSub != "" && !strings.Contains(err.Error(), tt.wantErrSub) {
|
||||||
|
t.Fatalf("Validate() error = %q, want it to contain %q", err, tt.wantErrSub)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Validate() = %v, want nil", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestManifestSHA256Lowered(t *testing.T) {
|
||||||
|
m := validManifest()
|
||||||
|
m.SHA256 = strings.ToUpper(m.SHA256)
|
||||||
|
if err := m.Validate(); err != nil {
|
||||||
|
t.Fatalf("Validate() = %v, want nil (uppercase hex accepted)", err)
|
||||||
|
}
|
||||||
|
if m.SHA256 != strings.ToLower(m.SHA256) {
|
||||||
|
t.Fatalf("SHA256 = %q, want it lowercased", m.SHA256)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestManifestEmitFormat(t *testing.T) {
|
||||||
|
m := &Manifest{
|
||||||
|
Name: "tool",
|
||||||
|
Version: "2.0",
|
||||||
|
Summary: "say \"hi\"\nnewline",
|
||||||
|
URL: "https://example.org/packages/tool/tool-2.0.tar.gz",
|
||||||
|
SHA256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||||
|
Deps: []string{"libz>=1.2.13", "pcre2"},
|
||||||
|
Archive: &Archive{Strip: 1},
|
||||||
|
}
|
||||||
|
want := `return {
|
||||||
|
name = "tool",
|
||||||
|
version = "2.0",
|
||||||
|
summary = "say \"hi\"\nnewline",
|
||||||
|
url = "https://example.org/packages/tool/tool-2.0.tar.gz",
|
||||||
|
sha256 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||||
|
deps = { "libz>=1.2.13", "pcre2" },
|
||||||
|
archive = { strip = 1 },
|
||||||
|
}
|
||||||
|
`
|
||||||
|
got, err := m.Emit()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Emit() error = %v", err)
|
||||||
|
}
|
||||||
|
if got != want {
|
||||||
|
t.Fatalf("Emit() output mismatch:\n--- got ---\n%s\n--- want ---\n%s", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestManifestEmitInvalid(t *testing.T) {
|
||||||
|
m := validManifest()
|
||||||
|
m.Version = ""
|
||||||
|
if _, err := m.Emit(); err == nil {
|
||||||
|
t.Fatal("Emit() = nil error, want validation error for missing version")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestManifestGolden(t *testing.T) {
|
||||||
|
m := &Manifest{
|
||||||
|
Name: "hello",
|
||||||
|
Version: "1.0",
|
||||||
|
Summary: "A tiny demonstration program",
|
||||||
|
URL: "https://example.org/packages/hello/hello-1.0.tar.gz",
|
||||||
|
SHA256: "731887527d1a72c57d1e64bad85cc341a4285354e773b2cd277420b744df2943",
|
||||||
|
Deps: []string{},
|
||||||
|
}
|
||||||
|
got, err := m.Emit()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Emit() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
goldenPath := filepath.Join("..", "..", "testdata", "golden", "hello.package.lua")
|
||||||
|
if os.Getenv("UPDATE_GOLDEN") == "1" {
|
||||||
|
if err := os.MkdirAll(filepath.Dir(goldenPath), 0o755); err != nil {
|
||||||
|
t.Fatalf("MkdirAll: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(goldenPath, []byte(got), 0o644); err != nil {
|
||||||
|
t.Fatalf("WriteFile golden: %v", err)
|
||||||
|
}
|
||||||
|
t.Logf("wrote golden %s", goldenPath)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
want, err := os.ReadFile(goldenPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ReadFile golden %s: %v (run UPDATE_GOLDEN=1 to regenerate)", goldenPath, err)
|
||||||
|
}
|
||||||
|
if got != string(want) {
|
||||||
|
t.Fatalf("Emit() does not match golden %s:\n--- got ---\n%s\n--- want ---\n%s", goldenPath, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+9
@@ -0,0 +1,9 @@
|
|||||||
|
return {
|
||||||
|
name = "hello",
|
||||||
|
version = "1.0",
|
||||||
|
summary = "A tiny demonstration program",
|
||||||
|
url = "https://example.org/packages/hello/hello-1.0.tar.gz",
|
||||||
|
sha256 = "731887527d1a72c57d1e64bad85cc341a4285354e773b2cd277420b744df2943",
|
||||||
|
deps = {},
|
||||||
|
archive = { strip = 1 },
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user