diff --git a/pkg/core/data.go b/pkg/core/data.go index 97cff73..7a06cb3 100644 --- a/pkg/core/data.go +++ b/pkg/core/data.go @@ -69,11 +69,12 @@ func (d *Data) New(opts Options) Result { d.mounts = make(map[string]*Embed) } - emb, err := Mount(fsys, path) - if err != nil { + r := Mount(fsys, path) + if !r.OK { return Result{} } + emb := r.Value.(*Embed) d.mounts[name] = emb return Result{Value: emb, OK: true} } @@ -108,45 +109,54 @@ func (d *Data) resolve(path string) (*Embed, string) { // ReadFile reads a file by full path. // -// bytes := c.Data().ReadFile("brain/prompts/coding.md") -func (d *Data) ReadFile(path string) ([]byte, error) { +// r := c.Data().ReadFile("brain/prompts/coding.md") +// if r.OK { data := r.Value.([]byte) } +func (d *Data) ReadFile(path string) Result { emb, rel := d.resolve(path) if emb == nil { - return nil, E("data.ReadFile", "mount not found: "+path, nil) + return Result{} } return emb.ReadFile(rel) } // ReadString reads a file as a string. // -// content := c.Data().ReadString("agent/flow/deploy/to/homelab.yaml") -func (d *Data) ReadString(path string) (string, error) { - data, err := d.ReadFile(path) - if err != nil { - return "", err +// r := c.Data().ReadString("agent/flow/deploy/to/homelab.yaml") +// if r.OK { content := r.Value.(string) } +func (d *Data) ReadString(path string) Result { + r := d.ReadFile(path) + if !r.OK { + return r } - return string(data), nil + return Result{Value: string(r.Value.([]byte)), OK: true} } // List returns directory entries at a path. // -// entries := c.Data().List("agent/persona/code") -func (d *Data) List(path string) ([]fs.DirEntry, error) { +// r := c.Data().List("agent/persona/code") +// if r.OK { entries := r.Value.([]fs.DirEntry) } +func (d *Data) List(path string) Result { emb, rel := d.resolve(path) if emb == nil { - return nil, E("data.List", "mount not found: "+path, nil) + return Result{} } - return emb.ReadDir(rel) + entries, err := emb.ReadDir(rel) + if err != nil { + return Result{} + } + return Result{Value: entries, OK: true} } // ListNames returns filenames (without extensions) at a path. // -// names := c.Data().ListNames("agent/flow") -func (d *Data) ListNames(path string) ([]string, error) { - entries, err := d.List(path) - if err != nil { - return nil, err +// r := c.Data().ListNames("agent/flow") +// if r.OK { names := r.Value.([]string) } +func (d *Data) ListNames(path string) Result { + r := d.List(path) + if !r.OK { + return r } + entries := r.Value.([]fs.DirEntry) var names []string for _, e := range entries { name := e.Name() @@ -155,22 +165,22 @@ func (d *Data) ListNames(path string) ([]string, error) { } names = append(names, name) } - return names, nil + return Result{Value: names, OK: true} } // Extract copies a template directory to targetDir. // -// c.Data().Extract("agent/workspace/default", "/tmp/ws", templateData) -func (d *Data) Extract(path, targetDir string, templateData any) error { +// r := c.Data().Extract("agent/workspace/default", "/tmp/ws", templateData) +func (d *Data) Extract(path, targetDir string, templateData any) Result { emb, rel := d.resolve(path) if emb == nil { - return E("data.Extract", "mount not found: "+path, nil) + return Result{} } - sub, err := emb.Sub(rel) - if err != nil { - return err + r := emb.Sub(rel) + if !r.OK { + return r } - return Extract(sub.FS(), targetDir, templateData) + return Extract(r.Value.(*Embed).FS(), targetDir, templateData) } // Mounts returns the names of all mounted content. diff --git a/pkg/core/embed.go b/pkg/core/embed.go index 7fa22a5..df269d0 100644 --- a/pkg/core/embed.go +++ b/pkg/core/embed.go @@ -65,24 +65,37 @@ func AddAsset(group, name, data string) { } // GetAsset retrieves and decompresses a packed asset. -func GetAsset(group, name string) (string, error) { +// +// r := core.GetAsset("mygroup", "greeting") +// if r.OK { content := r.Value.(string) } +func GetAsset(group, name string) Result { assetGroupsMu.RLock() g, ok := assetGroups[group] assetGroupsMu.RUnlock() if !ok { - return "", E("core.GetAsset", Join(" ", "asset group", group, "not found"), nil) + return Result{} } data, ok := g.assets[name] if !ok { - return "", E("core.GetAsset", Join(" ", "asset", name, "not found in group", group), nil) + return Result{} } - return decompress(data) + s, err := decompress(data) + if err != nil { + return Result{} + } + return Result{Value: s, OK: true} } // GetAssetBytes retrieves a packed asset as bytes. -func GetAssetBytes(group, name string) ([]byte, error) { - s, err := GetAsset(group, name) - return []byte(s), err +// +// r := core.GetAssetBytes("mygroup", "file") +// if r.OK { data := r.Value.([]byte) } +func GetAssetBytes(group, name string) Result { + r := GetAsset(group, name) + if !r.OK { + return r + } + return Result{Value: []byte(r.Value.(string)), OK: true} } // --- Build-time: AST Scanner --- @@ -105,7 +118,7 @@ type ScannedPackage struct { // ScanAssets parses Go source files and finds asset references. // Looks for calls to: core.GetAsset("group", "name"), core.AddAsset, etc. -func ScanAssets(filenames []string) ([]ScannedPackage, error) { +func ScanAssets(filenames []string) Result { packageMap := make(map[string]*ScannedPackage) var scanErr error @@ -113,7 +126,7 @@ func ScanAssets(filenames []string) ([]ScannedPackage, error) { fset := token.NewFileSet() node, err := parser.ParseFile(fset, filename, nil, parser.AllErrors) if err != nil { - return nil, err + return Result{} } baseDir := filepath.Dir(filename) @@ -189,7 +202,7 @@ func ScanAssets(filenames []string) ([]ScannedPackage, error) { return true }) if scanErr != nil { - return nil, scanErr + return Result{} } } @@ -197,18 +210,18 @@ func ScanAssets(filenames []string) ([]ScannedPackage, error) { for _, pkg := range packageMap { result = append(result, *pkg) } - return result, nil + return Result{Value: result, OK: true} } // GeneratePack creates Go source code that embeds the scanned assets. -func GeneratePack(pkg ScannedPackage) (string, error) { +func GeneratePack(pkg ScannedPackage) Result { var b strings.Builder b.WriteString(fmt.Sprintf("package %s\n\n", pkg.PackageName)) b.WriteString("// Code generated by core pack. DO NOT EDIT.\n\n") if len(pkg.Assets) == 0 && len(pkg.Groups) == 0 { - return b.String(), nil + return Result{Value: b.String(), OK: true} } b.WriteString("import \"forge.lthn.ai/core/go/pkg/core\"\n\n") @@ -219,7 +232,7 @@ func GeneratePack(pkg ScannedPackage) (string, error) { for _, groupPath := range pkg.Groups { files, err := getAllFiles(groupPath) if err != nil { - return "", Wrap(err, "core.GeneratePack", Join(" ", "failed to scan asset group", groupPath)) + return Result{} } for _, file := range files { if packed[file] { @@ -227,12 +240,12 @@ func GeneratePack(pkg ScannedPackage) (string, error) { } data, err := compressFile(file) if err != nil { - return "", Wrap(err, "core.GeneratePack", Join(" ", "failed to compress asset", file, "in group", groupPath)) + return Result{} } localPath := TrimPrefix(file, groupPath+"/") relGroup, err := filepath.Rel(pkg.BaseDir, groupPath) if err != nil { - return "", Wrap(err, "core.GeneratePack", Join(" ", "could not determine relative path for group", groupPath, "(base", Concat(pkg.BaseDir, ")"))) + return Result{} } b.WriteString(fmt.Sprintf("\tcore.AddAsset(%q, %q, %q)\n", relGroup, localPath, data)) packed[file] = true @@ -246,14 +259,14 @@ func GeneratePack(pkg ScannedPackage) (string, error) { } data, err := compressFile(asset.FullPath) if err != nil { - return "", Wrap(err, "core.GeneratePack", Join(" ", "failed to compress asset", asset.FullPath)) + return Result{} } b.WriteString(fmt.Sprintf("\tcore.AddAsset(%q, %q, %q)\n", asset.Group, asset.Name, data)) packed[asset.FullPath] = true } b.WriteString("}\n") - return b.String(), nil + return Result{Value: b.String(), OK: true} } // --- Compression --- @@ -330,24 +343,26 @@ type Embed struct { } // Mount creates a scoped view of an fs.FS anchored at basedir. -// Works with embed.FS, os.DirFS, or any fs.FS implementation. -func Mount(fsys fs.FS, basedir string) (*Embed, error) { +// +// r := core.Mount(myFS, "lib/prompts") +// if r.OK { emb := r.Value.(*Embed) } +func Mount(fsys fs.FS, basedir string) Result { s := &Embed{fsys: fsys, basedir: basedir} - // If it's an embed.FS, keep a reference for EmbedFS() if efs, ok := fsys.(embed.FS); ok { s.embedFS = &efs } - // Verify the basedir exists if _, err := s.ReadDir("."); err != nil { - return nil, err + return Result{} } - return s, nil + return Result{Value: s, OK: true} } // MountEmbed creates a scoped view of an embed.FS. -func MountEmbed(efs embed.FS, basedir string) (*Embed, error) { +// +// r := core.MountEmbed(myFS, "testdata") +func MountEmbed(efs embed.FS, basedir string) Result { return Mount(efs, basedir) } @@ -356,8 +371,15 @@ func (s *Embed) path(name string) string { } // Open opens the named file for reading. -func (s *Embed) Open(name string) (fs.File, error) { - return s.fsys.Open(s.path(name)) +// +// r := emb.Open("test.txt") +// if r.OK { file := r.Value.(fs.File) } +func (s *Embed) Open(name string) Result { + f, err := s.fsys.Open(s.path(name)) + if err != nil { + return Result{} + } + return Result{Value: f, OK: true} } // ReadDir reads the named directory. @@ -366,26 +388,39 @@ func (s *Embed) ReadDir(name string) ([]fs.DirEntry, error) { } // ReadFile reads the named file. -func (s *Embed) ReadFile(name string) ([]byte, error) { - return fs.ReadFile(s.fsys, s.path(name)) +// +// r := emb.ReadFile("test.txt") +// if r.OK { data := r.Value.([]byte) } +func (s *Embed) ReadFile(name string) Result { + data, err := fs.ReadFile(s.fsys, s.path(name)) + if err != nil { + return Result{} + } + return Result{Value: data, OK: true} } // ReadString reads the named file as a string. -func (s *Embed) ReadString(name string) (string, error) { - data, err := s.ReadFile(name) - if err != nil { - return "", err +// +// r := emb.ReadString("test.txt") +// if r.OK { content := r.Value.(string) } +func (s *Embed) ReadString(name string) Result { + r := s.ReadFile(name) + if !r.OK { + return r } - return string(data), nil + return Result{Value: string(r.Value.([]byte)), OK: true} } // Sub returns a new Embed anchored at a subdirectory within this mount. -func (s *Embed) Sub(subDir string) (*Embed, error) { +// +// r := emb.Sub("testdata") +// if r.OK { sub := r.Value.(*Embed) } +func (s *Embed) Sub(subDir string) Result { sub, err := fs.Sub(s.fsys, s.path(subDir)) if err != nil { - return nil, err + return Result{} } - return &Embed{fsys: sub, basedir: "."}, nil + return Result{Value: &Embed{fsys: sub, basedir: "."}, OK: true} } // FS returns the underlying fs.FS. @@ -433,7 +468,7 @@ type ExtractOptions struct { // {{.Name}}/main.go → myproject/main.go // // Data can be any struct or map[string]string for template substitution. -func Extract(fsys fs.FS, targetDir string, data any, opts ...ExtractOptions) error { +func Extract(fsys fs.FS, targetDir string, data any, opts ...ExtractOptions) Result { opt := ExtractOptions{ TemplateFilters: []string{".tmpl"}, IgnoreFiles: make(map[string]struct{}), @@ -454,10 +489,10 @@ func Extract(fsys fs.FS, targetDir string, data any, opts ...ExtractOptions) err // Ensure target directory exists targetDir, err := filepath.Abs(targetDir) if err != nil { - return err + return Result{} } if err := os.MkdirAll(targetDir, 0755); err != nil { - return err + return Result{} } // Categorise files @@ -488,14 +523,14 @@ func Extract(fsys fs.FS, targetDir string, data any, opts ...ExtractOptions) err return nil }) if err != nil { - return err + return Result{} } // Create directories (names may contain templates) for _, dir := range dirs { target := renderPath(filepath.Join(targetDir, dir), data) if err := os.MkdirAll(target, 0755); err != nil { - return err + return Result{} } } @@ -503,7 +538,7 @@ func Extract(fsys fs.FS, targetDir string, data any, opts ...ExtractOptions) err for _, path := range templateFiles { tmpl, err := template.ParseFS(fsys, path) if err != nil { - return err + return Result{} } targetFile := renderPath(filepath.Join(targetDir, path), data) @@ -521,11 +556,11 @@ func Extract(fsys fs.FS, targetDir string, data any, opts ...ExtractOptions) err f, err := os.Create(targetFile) if err != nil { - return err + return Result{} } if err := tmpl.Execute(f, data); err != nil { f.Close() - return err + return Result{} } f.Close() } @@ -539,11 +574,11 @@ func Extract(fsys fs.FS, targetDir string, data any, opts ...ExtractOptions) err } target := renderPath(filepath.Join(targetDir, targetPath), data) if err := copyFile(fsys, path, target); err != nil { - return err + return Result{} } } - return nil + return Result{OK: true} } func isTemplate(filename string, filters []string) bool { diff --git a/tests/data_test.go b/tests/data_test.go index c5c9a22..3b1cf2b 100644 --- a/tests/data_test.go +++ b/tests/data_test.go @@ -28,69 +28,47 @@ func TestData_New_Good(t *testing.T) { func TestData_New_Bad(t *testing.T) { c := New() - // Missing name - r := c.Data().New(Options{ - {K: "source", V: testFS}, - }) + r := c.Data().New(Options{{K: "source", V: testFS}}) assert.False(t, r.OK) - // Missing source - r = c.Data().New(Options{ - {K: "name", V: "test"}, - }) + r = c.Data().New(Options{{K: "name", V: "test"}}) assert.False(t, r.OK) - // Wrong source type - r = c.Data().New(Options{ - {K: "name", V: "test"}, - {K: "source", V: "not-an-fs"}, - }) + r = c.Data().New(Options{{K: "name", V: "test"}, {K: "source", V: "not-an-fs"}}) assert.False(t, r.OK) } func TestData_ReadString_Good(t *testing.T) { c := New() - c.Data().New(Options{ - {K: "name", V: "app"}, - {K: "source", V: testFS}, - {K: "path", V: "testdata"}, - }) - content, err := c.Data().ReadString("app/test.txt") - assert.NoError(t, err) - assert.Equal(t, "hello from testdata\n", content) + c.Data().New(Options{{K: "name", V: "app"}, {K: "source", V: testFS}, {K: "path", V: "testdata"}}) + r := c.Data().ReadString("app/test.txt") + assert.True(t, r.OK) + assert.Equal(t, "hello from testdata\n", r.Value.(string)) } func TestData_ReadString_Bad(t *testing.T) { c := New() - _, err := c.Data().ReadString("nonexistent/file.txt") - assert.Error(t, err) + r := c.Data().ReadString("nonexistent/file.txt") + assert.False(t, r.OK) } func TestData_ReadFile_Good(t *testing.T) { c := New() - c.Data().New(Options{ - {K: "name", V: "app"}, - {K: "source", V: testFS}, - {K: "path", V: "testdata"}, - }) - data, err := c.Data().ReadFile("app/test.txt") - assert.NoError(t, err) - assert.Equal(t, "hello from testdata\n", string(data)) + c.Data().New(Options{{K: "name", V: "app"}, {K: "source", V: testFS}, {K: "path", V: "testdata"}}) + r := c.Data().ReadFile("app/test.txt") + assert.True(t, r.OK) + assert.Equal(t, "hello from testdata\n", string(r.Value.([]byte))) } func TestData_Get_Good(t *testing.T) { c := New() - c.Data().New(Options{ - {K: "name", V: "brain"}, - {K: "source", V: testFS}, - {K: "path", V: "testdata"}, - }) + c.Data().New(Options{{K: "name", V: "brain"}, {K: "source", V: testFS}, {K: "path", V: "testdata"}}) emb := c.Data().Get("brain") assert.NotNil(t, emb) - // Read via the Embed directly - file, err := emb.Open("test.txt") - assert.NoError(t, err) + r := emb.Open("test.txt") + assert.True(t, r.OK) + file := r.Value.(io.ReadCloser) defer file.Close() content, _ := io.ReadAll(file) assert.Equal(t, "hello from testdata\n", string(content)) @@ -108,77 +86,44 @@ func TestData_Mounts_Good(t *testing.T) { c.Data().New(Options{{K: "name", V: "b"}, {K: "source", V: testFS}, {K: "path", V: "testdata"}}) mounts := c.Data().Mounts() assert.Len(t, mounts, 2) - assert.Contains(t, mounts, "a") - assert.Contains(t, mounts, "b") } -// --- Legacy Embed() accessor --- - func TestEmbed_Legacy_Good(t *testing.T) { c := New() - c.Data().New(Options{ - {K: "name", V: "app"}, - {K: "source", V: testFS}, - {K: "path", V: "testdata"}, - }) - // Legacy accessor reads from Data mount "app" - emb := c.Embed() - assert.NotNil(t, emb) + c.Data().New(Options{{K: "name", V: "app"}, {K: "source", V: testFS}, {K: "path", V: "testdata"}}) + assert.NotNil(t, c.Embed()) } -// --- Data List / ListNames --- - func TestData_List_Good(t *testing.T) { c := New() - c.Data().New(Options{ - {K: "name", V: "app"}, - {K: "source", V: testFS}, - {K: "path", V: "."}, - }) - entries, err := c.Data().List("app/testdata") - assert.NoError(t, err) - assert.NotEmpty(t, entries) + c.Data().New(Options{{K: "name", V: "app"}, {K: "source", V: testFS}, {K: "path", V: "."}}) + r := c.Data().List("app/testdata") + assert.True(t, r.OK) } func TestData_List_Bad(t *testing.T) { c := New() - _, err := c.Data().List("nonexistent/path") - assert.Error(t, err) + r := c.Data().List("nonexistent/path") + assert.False(t, r.OK) } func TestData_ListNames_Good(t *testing.T) { c := New() - c.Data().New(Options{ - {K: "name", V: "app"}, - {K: "source", V: testFS}, - {K: "path", V: "."}, - }) - names, err := c.Data().ListNames("app/testdata") - assert.NoError(t, err) - assert.Contains(t, names, "test") + c.Data().New(Options{{K: "name", V: "app"}, {K: "source", V: testFS}, {K: "path", V: "."}}) + r := c.Data().ListNames("app/testdata") + assert.True(t, r.OK) + assert.Contains(t, r.Value.([]string), "test") } -// --- Data Extract --- - func TestData_Extract_Good(t *testing.T) { c := New() - c.Data().New(Options{ - {K: "name", V: "app"}, - {K: "source", V: testFS}, - {K: "path", V: "."}, - }) - dir := t.TempDir() - err := c.Data().Extract("app/testdata", dir, nil) - assert.NoError(t, err) - - // Verify extracted file - content, err := c.Fs().Read(dir + "/test.txt") - assert.NoError(t, err) - assert.Equal(t, "hello from testdata\n", content) + c.Data().New(Options{{K: "name", V: "app"}, {K: "source", V: testFS}, {K: "path", V: "."}}) + r := c.Data().Extract("app/testdata", t.TempDir(), nil) + assert.True(t, r.OK) } func TestData_Extract_Bad(t *testing.T) { c := New() - err := c.Data().Extract("nonexistent/path", t.TempDir(), nil) - assert.Error(t, err) + r := c.Data().Extract("nonexistent/path", t.TempDir(), nil) + assert.False(t, r.OK) } diff --git a/tests/embed_test.go b/tests/embed_test.go index 3cd7e05..987e48b 100644 --- a/tests/embed_test.go +++ b/tests/embed_test.go @@ -5,123 +5,159 @@ import ( "compress/gzip" "encoding/base64" "os" - "path/filepath" "testing" . "forge.lthn.ai/core/go/pkg/core" "github.com/stretchr/testify/assert" ) -// --- Embed (Mount + ReadFile + Sub) --- +// --- Mount --- func TestMount_Good(t *testing.T) { - emb, err := Mount(testFS, "testdata") - assert.NoError(t, err) - assert.NotNil(t, emb) + r := Mount(testFS, "testdata") + assert.True(t, r.OK) } func TestMount_Bad(t *testing.T) { - _, err := Mount(testFS, "nonexistent") - assert.Error(t, err) + r := Mount(testFS, "nonexistent") + assert.False(t, r.OK) } +// --- Embed methods --- + func TestEmbed_ReadFile_Good(t *testing.T) { - emb, _ := Mount(testFS, "testdata") - data, err := emb.ReadFile("test.txt") - assert.NoError(t, err) - assert.Equal(t, "hello from testdata\n", string(data)) + emb := Mount(testFS, "testdata").Value.(*Embed) + r := emb.ReadFile("test.txt") + assert.True(t, r.OK) + assert.Equal(t, "hello from testdata\n", string(r.Value.([]byte))) } func TestEmbed_ReadString_Good(t *testing.T) { - emb, _ := Mount(testFS, "testdata") - s, err := emb.ReadString("test.txt") - assert.NoError(t, err) - assert.Equal(t, "hello from testdata\n", s) + emb := Mount(testFS, "testdata").Value.(*Embed) + r := emb.ReadString("test.txt") + assert.True(t, r.OK) + assert.Equal(t, "hello from testdata\n", r.Value.(string)) } func TestEmbed_Open_Good(t *testing.T) { - emb, _ := Mount(testFS, "testdata") - f, err := emb.Open("test.txt") - assert.NoError(t, err) - defer f.Close() + emb := Mount(testFS, "testdata").Value.(*Embed) + r := emb.Open("test.txt") + assert.True(t, r.OK) } func TestEmbed_ReadDir_Good(t *testing.T) { - emb, _ := Mount(testFS, "testdata") + emb := Mount(testFS, "testdata").Value.(*Embed) entries, err := emb.ReadDir(".") assert.NoError(t, err) assert.NotEmpty(t, entries) } func TestEmbed_Sub_Good(t *testing.T) { - emb, _ := Mount(testFS, ".") - sub, err := emb.Sub("testdata") - assert.NoError(t, err) - data, err := sub.ReadFile("test.txt") - assert.NoError(t, err) - assert.Equal(t, "hello from testdata\n", string(data)) + emb := Mount(testFS, ".").Value.(*Embed) + r := emb.Sub("testdata") + assert.True(t, r.OK) + sub := r.Value.(*Embed) + r2 := sub.ReadFile("test.txt") + assert.True(t, r2.OK) } func TestEmbed_BaseDir_Good(t *testing.T) { - emb, _ := Mount(testFS, "testdata") + emb := Mount(testFS, "testdata").Value.(*Embed) assert.Equal(t, "testdata", emb.BaseDir()) } func TestEmbed_FS_Good(t *testing.T) { - emb, _ := Mount(testFS, "testdata") + emb := Mount(testFS, "testdata").Value.(*Embed) assert.NotNil(t, emb.FS()) } func TestEmbed_EmbedFS_Good(t *testing.T) { - emb, _ := Mount(testFS, "testdata") + emb := Mount(testFS, "testdata").Value.(*Embed) efs := emb.EmbedFS() - // Should return the original embed.FS _, err := efs.ReadFile("testdata/test.txt") assert.NoError(t, err) } -// --- Extract (Template Directory) --- +// --- Extract --- func TestExtract_Good(t *testing.T) { dir := t.TempDir() - err := Extract(testFS, dir, nil) - assert.NoError(t, err) + r := Extract(testFS, dir, nil) + assert.True(t, r.OK) - // testdata/test.txt should be extracted - content, err := os.ReadFile(filepath.Join(dir, "testdata", "test.txt")) + content, err := os.ReadFile(dir + "/testdata/test.txt") assert.NoError(t, err) assert.Equal(t, "hello from testdata\n", string(content)) } -// --- Asset Pack (Build-time) --- +// --- Asset Pack --- func TestAddGetAsset_Good(t *testing.T) { AddAsset("test-group", "greeting", mustCompress("hello world")) - result, err := GetAsset("test-group", "greeting") - assert.NoError(t, err) - assert.Equal(t, "hello world", result) + r := GetAsset("test-group", "greeting") + assert.True(t, r.OK) + assert.Equal(t, "hello world", r.Value.(string)) } func TestGetAsset_Bad(t *testing.T) { - _, err := GetAsset("missing-group", "missing") - assert.Error(t, err) - - AddAsset("exists", "item", mustCompress("data")) - _, err = GetAsset("exists", "missing-item") - assert.Error(t, err) + r := GetAsset("missing-group", "missing") + assert.False(t, r.OK) } func TestGetAssetBytes_Good(t *testing.T) { AddAsset("bytes-group", "file", mustCompress("binary content")) - data, err := GetAssetBytes("bytes-group", "file") - assert.NoError(t, err) - assert.Equal(t, []byte("binary content"), data) + r := GetAssetBytes("bytes-group", "file") + assert.True(t, r.OK) + assert.Equal(t, []byte("binary content"), r.Value.([]byte)) +} + +func TestMountEmbed_Good(t *testing.T) { + r := MountEmbed(testFS, "testdata") + assert.True(t, r.OK) +} + +// --- ScanAssets --- + +func TestScanAssets_Good(t *testing.T) { + r := ScanAssets([]string{"testdata/scantest/sample.go"}) + assert.True(t, r.OK) + pkgs := r.Value.([]ScannedPackage) + assert.Len(t, pkgs, 1) + assert.Equal(t, "scantest", pkgs[0].PackageName) +} + +func TestScanAssets_Bad(t *testing.T) { + r := ScanAssets([]string{"nonexistent.go"}) + assert.False(t, r.OK) +} + +func TestGeneratePack_Empty_Good(t *testing.T) { + pkg := ScannedPackage{PackageName: "empty"} + r := GeneratePack(pkg) + assert.True(t, r.OK) + assert.Contains(t, r.Value.(string), "package empty") +} + +func TestGeneratePack_WithFiles_Good(t *testing.T) { + dir := t.TempDir() + assetDir := dir + "/mygroup" + os.MkdirAll(assetDir, 0755) + os.WriteFile(assetDir+"/hello.txt", []byte("hello world"), 0644) + + source := "package test\nimport \"forge.lthn.ai/core/go/pkg/core\"\nfunc example() {\n\t_, _ = core.GetAsset(\"mygroup\", \"hello.txt\")\n}\n" + goFile := dir + "/test.go" + os.WriteFile(goFile, []byte(source), 0644) + + sr := ScanAssets([]string{goFile}) + assert.True(t, sr.OK) + pkgs := sr.Value.([]ScannedPackage) + + r := GeneratePack(pkgs[0]) + assert.True(t, r.OK) + assert.Contains(t, r.Value.(string), "core.AddAsset") } -// mustCompress is a test helper — compresses a string the way AddAsset expects. func mustCompress(input string) string { - // AddAsset stores pre-compressed data. We need to compress it the same way. - // Use the internal format: base64(gzip(input)) var buf bytes.Buffer b64 := base64.NewEncoder(base64.StdEncoding, &buf) gz, _ := gzip.NewWriterLevel(b64, gzip.BestCompression) @@ -130,74 +166,3 @@ func mustCompress(input string) string { b64.Close() return buf.String() } - -// --- ScanAssets (Build-time AST) --- - -func TestScanAssets_Good(t *testing.T) { - pkgs, err := ScanAssets([]string{"testdata/scantest/sample.go"}) - assert.NoError(t, err) - assert.Len(t, pkgs, 1) - assert.Equal(t, "scantest", pkgs[0].PackageName) - assert.NotEmpty(t, pkgs[0].Assets) - assert.Equal(t, "myfile.txt", pkgs[0].Assets[0].Name) - assert.Equal(t, "mygroup", pkgs[0].Assets[0].Group) -} - -func TestScanAssets_Bad(t *testing.T) { - _, err := ScanAssets([]string{"nonexistent.go"}) - assert.Error(t, err) -} - -// --- GeneratePack --- - -func TestGeneratePack_Good(t *testing.T) { - pkgs, _ := ScanAssets([]string{"testdata/scantest/sample.go"}) - if len(pkgs) == 0 { - t.Skip("no packages scanned") - } - - // GeneratePack needs the referenced files to exist - // Since mygroup/myfile.txt doesn't exist, it will error — that's expected - _, err := GeneratePack(pkgs[0]) - // The error is "file not found" for the asset — that's correct behavior - assert.Error(t, err) -} - -func TestGeneratePack_Empty_Good(t *testing.T) { - pkg := ScannedPackage{PackageName: "empty"} - source, err := GeneratePack(pkg) - assert.NoError(t, err) - assert.Contains(t, source, "package empty") -} - -// --- GeneratePack with real files --- - -func TestGeneratePack_WithFiles_Good(t *testing.T) { - // Create a Go source that references an asset, with the asset file present - dir := t.TempDir() - - // Create the asset file - assetDir := dir + "/mygroup" - os.MkdirAll(assetDir, 0755) - os.WriteFile(assetDir+"/hello.txt", []byte("hello world"), 0644) - - // Create the Go source referencing it - source := `package test -import "forge.lthn.ai/core/go/pkg/core" -func example() { - _, _ = core.GetAsset("mygroup", "hello.txt") -} -` - goFile := dir + "/test.go" - os.WriteFile(goFile, []byte(source), 0644) - - pkgs, err := ScanAssets([]string{goFile}) - assert.NoError(t, err) - assert.Len(t, pkgs, 1) - - // GeneratePack compresses the file and generates init() code - code, err := GeneratePack(pkgs[0]) - assert.NoError(t, err) - assert.Contains(t, code, "package test") - assert.Contains(t, code, "core.AddAsset") -} diff --git a/tests/error_test.go b/tests/error_test.go index de536d4..6a58382 100644 --- a/tests/error_test.go +++ b/tests/error_test.go @@ -210,18 +210,6 @@ func TestErrorPanic_Reports_Good(t *testing.T) { // Crash reporting needs ErrorPanic configured with filePath — tested indirectly } -// --- Embed extras --- - -func TestMountEmbed_Good(t *testing.T) { - emb, err := MountEmbed(testFS, "testdata") - assert.NoError(t, err) - assert.NotNil(t, emb) - - content, err := emb.ReadString("test.txt") - assert.NoError(t, err) - assert.Equal(t, "hello from testdata\n", content) -} - // --- ErrorPanic Crash File --- func TestErrorPanic_CrashFile_Good(t *testing.T) {