feat: embed.go and data.go return Result throughout
Mount, MountEmbed, Open, ReadFile, ReadString, Sub, GetAsset, GetAssetBytes, ScanAssets, GeneratePack, Extract → all return Result. Data.ReadFile, ReadString, List, ListNames, Extract → Result. Data.New uses Mount's Result internally. Internal helpers (WalkDir callback, copyFile) stay error — they're not public API. 231 tests, 77.4% coverage. Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
94f2e54abe
commit
2d6415b3aa
5 changed files with 240 additions and 297 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue