From 8172824b427e0db128e718468eea48fcd7b98033 Mon Sep 17 00:00:00 2001 From: Snider Date: Tue, 17 Feb 2026 22:14:06 +0000 Subject: [PATCH] fix: update tests to match current API after refactor - node: add ReadFile (fs.ReadFileFS), Walk with WalkOptions, CopyFile - node_test: fix Exists to single-return bool, FromTar as method call - cache_test: remove Medium parameter, use t.TempDir() - daemon_test: remove Medium from NewPIDFile/DaemonOptions, use os pkg Co-Authored-By: Virgil --- pkg/cache/cache_test.go | 13 +++--- pkg/cli/daemon_test.go | 37 +++++++-------- pkg/io/node/node.go | 97 ++++++++++++++++++++++++++++++++++++++++ pkg/io/node/node_test.go | 38 ++++++---------- 4 files changed, 132 insertions(+), 53 deletions(-) diff --git a/pkg/cache/cache_test.go b/pkg/cache/cache_test.go index 1a85be9..31bd742 100644 --- a/pkg/cache/cache_test.go +++ b/pkg/cache/cache_test.go @@ -5,14 +5,11 @@ import ( "time" "forge.lthn.ai/core/go/pkg/cache" - "forge.lthn.ai/core/go/pkg/io" ) func TestCache(t *testing.T) { - m := io.NewMockMedium() - // Use a path that MockMedium will understand - baseDir := "/tmp/cache" - c, err := cache.New(m, baseDir, 1*time.Minute) + baseDir := t.TempDir() + c, err := cache.New(baseDir, 1*time.Minute) if err != nil { t.Fatalf("failed to create cache: %v", err) } @@ -57,7 +54,7 @@ func TestCache(t *testing.T) { } // Test Expiry - cshort, err := cache.New(m, "/tmp/cache-short", 10*time.Millisecond) + cshort, err := cache.New(t.TempDir(), 10*time.Millisecond) if err != nil { t.Fatalf("failed to create short-lived cache: %v", err) } @@ -93,8 +90,8 @@ func TestCache(t *testing.T) { } func TestCacheDefaults(t *testing.T) { - // Test default Medium (io.Local) and default TTL - c, err := cache.New(nil, "", 0) + // Test default TTL (uses cwd/.core/cache) + c, err := cache.New("", 0) if err != nil { t.Fatalf("failed to create cache with defaults: %v", err) } diff --git a/pkg/cli/daemon_test.go b/pkg/cli/daemon_test.go index a67c162..8923b47 100644 --- a/pkg/cli/daemon_test.go +++ b/pkg/cli/daemon_test.go @@ -3,10 +3,10 @@ package cli import ( "context" "net/http" + "os" "testing" "time" - "forge.lthn.ai/core/go/pkg/io" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -27,17 +27,16 @@ func TestDetectMode(t *testing.T) { func TestPIDFile(t *testing.T) { t.Run("acquire and release", func(t *testing.T) { - m := io.NewMockMedium() - pidPath := "/tmp/test.pid" + pidPath := t.TempDir() + "/test.pid" - pid := NewPIDFile(m, pidPath) + pid := NewPIDFile(pidPath) // Acquire should succeed err := pid.Acquire() require.NoError(t, err) // File should exist with our PID - data, err := m.Read(pidPath) + data, err := os.ReadFile(pidPath) require.NoError(t, err) assert.NotEmpty(t, data) @@ -45,18 +44,18 @@ func TestPIDFile(t *testing.T) { err = pid.Release() require.NoError(t, err) - assert.False(t, m.Exists(pidPath)) + _, statErr := os.Stat(pidPath) + assert.True(t, os.IsNotExist(statErr)) }) t.Run("stale pid file", func(t *testing.T) { - m := io.NewMockMedium() - pidPath := "/tmp/stale.pid" + pidPath := t.TempDir() + "/stale.pid" // Write a stale PID (non-existent process) - err := m.Write(pidPath, "999999999") + err := os.WriteFile(pidPath, []byte("999999999"), 0644) require.NoError(t, err) - pid := NewPIDFile(m, pidPath) + pid := NewPIDFile(pidPath) // Should acquire successfully (stale PID removed) err = pid.Acquire() @@ -67,23 +66,22 @@ func TestPIDFile(t *testing.T) { }) t.Run("creates parent directory", func(t *testing.T) { - m := io.NewMockMedium() - pidPath := "/tmp/subdir/nested/test.pid" + pidPath := t.TempDir() + "/subdir/nested/test.pid" - pid := NewPIDFile(m, pidPath) + pid := NewPIDFile(pidPath) err := pid.Acquire() require.NoError(t, err) - assert.True(t, m.Exists(pidPath)) + _, statErr := os.Stat(pidPath) + assert.NoError(t, statErr) err = pid.Release() require.NoError(t, err) }) t.Run("path getter", func(t *testing.T) { - m := io.NewMockMedium() - pid := NewPIDFile(m, "/tmp/test.pid") + pid := NewPIDFile("/tmp/test.pid") assert.Equal(t, "/tmp/test.pid", pid.Path()) }) } @@ -155,11 +153,9 @@ func TestHealthServer(t *testing.T) { func TestDaemon(t *testing.T) { t.Run("start and stop", func(t *testing.T) { - m := io.NewMockMedium() - pidPath := "/tmp/test.pid" + pidPath := t.TempDir() + "/test.pid" d := NewDaemon(DaemonOptions{ - Medium: m, PIDFile: pidPath, HealthAddr: "127.0.0.1:0", ShutdownTimeout: 5 * time.Second, @@ -182,7 +178,8 @@ func TestDaemon(t *testing.T) { require.NoError(t, err) // PID file should be removed - assert.False(t, m.Exists(pidPath)) + _, statErr := os.Stat(pidPath) + assert.True(t, os.IsNotExist(statErr)) }) t.Run("double start fails", func(t *testing.T) { diff --git a/pkg/io/node/node.go b/pkg/io/node/node.go index 66ff250..6bd96ac 100644 --- a/pkg/io/node/node.go +++ b/pkg/io/node/node.go @@ -118,6 +118,89 @@ func (n *Node) WalkNode(root string, fn fs.WalkDirFunc) error { return fs.WalkDir(n, root, fn) } +// WalkOptions configures optional behaviour for Walk. +type WalkOptions struct { + // MaxDepth limits traversal depth (0 = unlimited, 1 = root children only). + MaxDepth int + // Filter, when non-nil, is called before visiting each entry. + // Return false to skip the entry (and its subtree if a directory). + Filter func(path string, d fs.DirEntry) bool + // SkipErrors suppresses errors from the root lookup and doesn't call fn. + SkipErrors bool +} + +// Walk walks the in-memory tree with optional WalkOptions. +func (n *Node) Walk(root string, fn fs.WalkDirFunc, opts ...WalkOptions) error { + var opt WalkOptions + if len(opts) > 0 { + opt = opts[0] + } + + if opt.SkipErrors { + // Check root exists — if not, silently skip. + if _, err := n.Stat(root); err != nil { + return nil + } + } + + rootDepth := 0 + if root != "." && root != "" { + rootDepth = strings.Count(root, "/") + 1 + } + + return fs.WalkDir(n, root, func(p string, d fs.DirEntry, err error) error { + if err != nil { + return fn(p, d, err) + } + + // MaxDepth check. + if opt.MaxDepth > 0 { + depth := 0 + if p != "." && p != "" { + depth = strings.Count(p, "/") + 1 + } + if depth-rootDepth > opt.MaxDepth { + if d.IsDir() { + return fs.SkipDir + } + return nil + } + } + + // Filter check. + if opt.Filter != nil && !opt.Filter(p, d) { + if d.IsDir() { + return fs.SkipDir + } + return nil + } + + return fn(p, d, err) + }) +} + +// CopyFile copies a single file from the node to the OS filesystem. +func (n *Node) CopyFile(src, dst string, perm os.FileMode) error { + src = strings.TrimPrefix(src, "/") + f, ok := n.files[src] + if !ok { + // Check if it's a directory — can't copy a directory as a file. + if info, err := n.Stat(src); err == nil && info.IsDir() { + return &fs.PathError{Op: "copyfile", Path: src, Err: fs.ErrInvalid} + } + return &fs.PathError{Op: "copyfile", Path: src, Err: fs.ErrNotExist} + } + + dir := path.Dir(dst) + if dir != "." { + if err := os.MkdirAll(dir, 0755); err != nil { + return err + } + } + + return os.WriteFile(dst, f.content, perm) +} + // CopyTo copies a file (or directory tree) from the node to any Medium. func (n *Node) CopyTo(target coreio.Medium, sourcePath, destPath string) error { sourcePath = strings.TrimPrefix(sourcePath, "/") @@ -247,6 +330,20 @@ func (n *Node) ReadDir(name string) ([]fs.DirEntry, error) { return entries, nil } +// ReadFile returns the content of a file as a byte slice. +// Implements fs.ReadFileFS. +func (n *Node) ReadFile(name string) ([]byte, error) { + name = strings.TrimPrefix(name, "/") + f, ok := n.files[name] + if !ok { + return nil, fs.ErrNotExist + } + // Return a copy to prevent mutation of internal state. + out := make([]byte, len(f.content)) + copy(out, f.content) + return out, nil +} + // ---------- Medium interface: read/write ---------- // Read retrieves the content of a file as a string. diff --git a/pkg/io/node/node_test.go b/pkg/io/node/node_test.go index 5ef1afa..401c467 100644 --- a/pkg/io/node/node_test.go +++ b/pkg/io/node/node_test.go @@ -243,33 +243,21 @@ func TestExists_Good(t *testing.T) { n.AddData("foo.txt", []byte("foo")) n.AddData("bar/baz.txt", []byte("baz")) - exists, err := n.Exists("foo.txt") - require.NoError(t, err) - assert.True(t, exists) - - exists, err = n.Exists("bar") - require.NoError(t, err) - assert.True(t, exists) + assert.True(t, n.Exists("foo.txt")) + assert.True(t, n.Exists("bar")) } func TestExists_Bad(t *testing.T) { n := New() - exists, err := n.Exists("nonexistent") - require.NoError(t, err) - assert.False(t, exists) + assert.False(t, n.Exists("nonexistent")) } func TestExists_Ugly(t *testing.T) { n := New() n.AddData("dummy.txt", []byte("dummy")) - exists, err := n.Exists(".") - require.NoError(t, err) - assert.True(t, exists, "root '.' must exist") - - exists, err = n.Exists("") - require.NoError(t, err) - assert.True(t, exists, "empty path (root) must exist") + assert.True(t, n.Exists("."), "root '.' must exist") + assert.True(t, n.Exists(""), "empty path (root) must exist") } // --------------------------------------------------------------------------- @@ -463,20 +451,19 @@ func TestFromTar_Good(t *testing.T) { } require.NoError(t, tw.Close()) - n, err := FromTar(buf.Bytes()) + n := New() + err := n.FromTar(buf.Bytes()) require.NoError(t, err) - exists, _ := n.Exists("foo.txt") - assert.True(t, exists, "foo.txt should exist") - - exists, _ = n.Exists("bar/baz.txt") - assert.True(t, exists, "bar/baz.txt should exist") + assert.True(t, n.Exists("foo.txt"), "foo.txt should exist") + assert.True(t, n.Exists("bar/baz.txt"), "bar/baz.txt should exist") } func TestFromTar_Bad(t *testing.T) { // Truncated data that cannot be a valid tar. truncated := make([]byte, 100) - _, err := FromTar(truncated) + n := New() + err := n.FromTar(truncated) assert.Error(t, err, "truncated data should produce an error") } @@ -488,7 +475,8 @@ func TestTarRoundTrip_Good(t *testing.T) { tarball, err := n1.ToTar() require.NoError(t, err) - n2, err := FromTar(tarball) + n2 := New() + err = n2.FromTar(tarball) require.NoError(t, err) // Verify n2 matches n1.