refactor fstree to separate package
This commit is contained in:
+123
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
This file has the raw file io operations that are used by FsTree
|
||||
or elsewhere in the app. Nowhere else should there be a direct
|
||||
fileopen
|
||||
|
||||
- ruinivist, 30Dec25
|
||||
*/
|
||||
|
||||
package fstree
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func walkFileSystemAndBuildTree(rootPath string, node *FsNode) error {
|
||||
if node == nil {
|
||||
return errors.New("node cannot be nil")
|
||||
}
|
||||
if len(node.children) > 0 {
|
||||
return errors.New("node already has children")
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(rootPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
childPath := filepath.Join(rootPath, entry.Name())
|
||||
|
||||
var nodeType FsNodeType
|
||||
if entry.IsDir() {
|
||||
nodeType = FolderNode
|
||||
} else {
|
||||
nodeType = FileNode
|
||||
}
|
||||
|
||||
childNode := &FsNode{
|
||||
nodeType: nodeType,
|
||||
path: childPath,
|
||||
children: make([]*FsNode, 0),
|
||||
parent: node,
|
||||
expanded: false,
|
||||
}
|
||||
|
||||
node.children = append(node.children, childNode)
|
||||
|
||||
// Recursively walk subdirectories
|
||||
if entry.IsDir() {
|
||||
if err := walkFileSystemAndBuildTree(childPath, childNode); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createFile(path string, content []byte) error {
|
||||
if path == "" {
|
||||
return errors.New("file path cannot be empty")
|
||||
}
|
||||
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return errors.New("file already exists")
|
||||
}
|
||||
|
||||
return os.WriteFile(path, content, 0644)
|
||||
}
|
||||
|
||||
func createFolder(path string) error {
|
||||
if path == "" {
|
||||
return errors.New("folder path cannot be empty")
|
||||
}
|
||||
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return errors.New("folder already exists")
|
||||
}
|
||||
|
||||
return os.Mkdir(path, 0755)
|
||||
}
|
||||
|
||||
func deleteFile(path string) error {
|
||||
if path == "" {
|
||||
return errors.New("file path cannot be empty")
|
||||
}
|
||||
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return errors.New("file does not exist")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
return errors.New("path is a directory, not a file")
|
||||
}
|
||||
|
||||
return os.Remove(path)
|
||||
}
|
||||
|
||||
func deleteFolder(path string) error {
|
||||
if path == "" {
|
||||
return errors.New("folder path cannot be empty")
|
||||
}
|
||||
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return errors.New("folder does not exist")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if !info.IsDir() {
|
||||
return errors.New("path is a file, not a directory")
|
||||
}
|
||||
|
||||
return os.RemoveAll(path)
|
||||
}
|
||||
@@ -0,0 +1,379 @@
|
||||
/*
|
||||
another test that is entirely ae ayee generated
|
||||
- ruinivist, 30Dec25
|
||||
*/
|
||||
|
||||
package fstree
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWalkFileSystemAndBuildTree(t *testing.T) {
|
||||
t.Run("build tree from real directory", func(t *testing.T) {
|
||||
// Create temporary directory structure
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Create test structure:
|
||||
// tmpDir/
|
||||
// file1.txt
|
||||
// file2.go
|
||||
// folder1/
|
||||
// nested.txt
|
||||
// folder2/
|
||||
os.WriteFile(filepath.Join(tmpDir, "file1.txt"), []byte("content"), 0644)
|
||||
os.WriteFile(filepath.Join(tmpDir, "file2.go"), []byte("code"), 0644)
|
||||
os.Mkdir(filepath.Join(tmpDir, "folder1"), 0755)
|
||||
os.WriteFile(filepath.Join(tmpDir, "folder1", "nested.txt"), []byte("nested"), 0644)
|
||||
os.Mkdir(filepath.Join(tmpDir, "folder2"), 0755)
|
||||
|
||||
// Build tree
|
||||
root := &FsNode{
|
||||
nodeType: FolderNode,
|
||||
path: tmpDir,
|
||||
children: make([]*FsNode, 0),
|
||||
expanded: true,
|
||||
}
|
||||
|
||||
err := walkFileSystemAndBuildTree(tmpDir, root)
|
||||
if err != nil {
|
||||
t.Fatalf("walkFileSystemAndBuildTree() error = %v", err)
|
||||
}
|
||||
|
||||
// Verify structure
|
||||
if len(root.children) != 4 {
|
||||
t.Fatalf("root children count = %d, want 4", len(root.children))
|
||||
}
|
||||
|
||||
// Check children exist and have correct types
|
||||
fileCount := 0
|
||||
folderCount := 0
|
||||
var folder1 *FsNode
|
||||
|
||||
for _, child := range root.children {
|
||||
if child.parent != root {
|
||||
t.Errorf("child %q parent pointer incorrect", child.FileName())
|
||||
}
|
||||
|
||||
if child.nodeType == FileNode {
|
||||
fileCount++
|
||||
} else if child.nodeType == FolderNode {
|
||||
folderCount++
|
||||
if child.FileName() == "folder1" {
|
||||
folder1 = child
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if fileCount != 2 {
|
||||
t.Errorf("file count = %d, want 2", fileCount)
|
||||
}
|
||||
if folderCount != 2 {
|
||||
t.Errorf("folder count = %d, want 2", folderCount)
|
||||
}
|
||||
|
||||
// Verify nested structure
|
||||
if folder1 == nil {
|
||||
t.Fatal("folder1 not found")
|
||||
}
|
||||
if len(folder1.children) != 1 {
|
||||
t.Errorf("folder1 children count = %d, want 1", len(folder1.children))
|
||||
}
|
||||
if folder1.children[0].FileName() != "nested.txt" {
|
||||
t.Errorf("nested file name = %q, want %q", folder1.children[0].FileName(), "nested.txt")
|
||||
}
|
||||
if folder1.children[0].nodeType != FileNode {
|
||||
t.Errorf("nested node type = %v, want %v", folder1.children[0].nodeType, FileNode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("error when node is nil", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
err := walkFileSystemAndBuildTree(tmpDir, nil)
|
||||
if err == nil {
|
||||
t.Fatal("walkFileSystemAndBuildTree() with nil node should return error")
|
||||
}
|
||||
if err.Error() != "node cannot be nil" {
|
||||
t.Errorf("error message = %q", err.Error())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("error when node already has children", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
root := &FsNode{
|
||||
nodeType: FolderNode,
|
||||
path: tmpDir,
|
||||
children: []*FsNode{{nodeType: FileNode, path: "dummy"}},
|
||||
expanded: true,
|
||||
}
|
||||
|
||||
err := walkFileSystemAndBuildTree(tmpDir, root)
|
||||
if err == nil {
|
||||
t.Fatal("walkFileSystemAndBuildTree() with existing children should return error")
|
||||
}
|
||||
if err.Error() != "node already has children" {
|
||||
t.Errorf("error message = %q", err.Error())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("error when path does not exist", func(t *testing.T) {
|
||||
root := &FsNode{
|
||||
nodeType: FolderNode,
|
||||
path: "/nonexistent/path",
|
||||
children: make([]*FsNode, 0),
|
||||
expanded: true,
|
||||
}
|
||||
|
||||
err := walkFileSystemAndBuildTree("/nonexistent/path", root)
|
||||
if err == nil {
|
||||
t.Fatal("walkFileSystemAndBuildTree() with nonexistent path should return error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty directory", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
root := &FsNode{
|
||||
nodeType: FolderNode,
|
||||
path: tmpDir,
|
||||
children: make([]*FsNode, 0),
|
||||
expanded: true,
|
||||
}
|
||||
|
||||
err := walkFileSystemAndBuildTree(tmpDir, root)
|
||||
if err != nil {
|
||||
t.Fatalf("walkFileSystemAndBuildTree() error = %v", err)
|
||||
}
|
||||
|
||||
if len(root.children) != 0 {
|
||||
t.Errorf("empty directory should have 0 children, got %d", len(root.children))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCreateFile(t *testing.T) {
|
||||
t.Run("create file successfully", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
filePath := filepath.Join(tmpDir, "test.txt")
|
||||
content := []byte("test content")
|
||||
|
||||
err := createFile(filePath, content)
|
||||
if err != nil {
|
||||
t.Fatalf("createFile() error = %v", err)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read created file: %v", err)
|
||||
}
|
||||
if string(data) != string(content) {
|
||||
t.Errorf("file content = %q, want %q", string(data), string(content))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("create empty file", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
filePath := filepath.Join(tmpDir, "empty.txt")
|
||||
|
||||
err := createFile(filePath, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("createFile() error = %v", err)
|
||||
}
|
||||
|
||||
info, err := os.Stat(filePath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to stat file: %v", err)
|
||||
}
|
||||
if info.Size() != 0 {
|
||||
t.Errorf("file size = %d, want 0", info.Size())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("error when path is empty", func(t *testing.T) {
|
||||
err := createFile("", []byte("content"))
|
||||
if err == nil {
|
||||
t.Fatal("createFile() with empty path should return error")
|
||||
}
|
||||
if err.Error() != "file path cannot be empty" {
|
||||
t.Errorf("error message = %q", err.Error())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("error when file already exists", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
filePath := filepath.Join(tmpDir, "existing.txt")
|
||||
os.WriteFile(filePath, []byte("existing"), 0644)
|
||||
|
||||
err := createFile(filePath, []byte("new"))
|
||||
if err == nil {
|
||||
t.Fatal("createFile() on existing file should return error")
|
||||
}
|
||||
if err.Error() != "file already exists" {
|
||||
t.Errorf("error message = %q", err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCreateFolder(t *testing.T) {
|
||||
t.Run("create folder successfully", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
folderPath := filepath.Join(tmpDir, "testfolder")
|
||||
|
||||
err := createFolder(folderPath)
|
||||
if err != nil {
|
||||
t.Fatalf("createFolder() error = %v", err)
|
||||
}
|
||||
|
||||
info, err := os.Stat(folderPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to stat folder: %v", err)
|
||||
}
|
||||
if !info.IsDir() {
|
||||
t.Error("created path is not a directory")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("error when path is empty", func(t *testing.T) {
|
||||
err := createFolder("")
|
||||
if err == nil {
|
||||
t.Fatal("createFolder() with empty path should return error")
|
||||
}
|
||||
if err.Error() != "folder path cannot be empty" {
|
||||
t.Errorf("error message = %q", err.Error())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("error when folder already exists", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
folderPath := filepath.Join(tmpDir, "existing")
|
||||
os.Mkdir(folderPath, 0755)
|
||||
|
||||
err := createFolder(folderPath)
|
||||
if err == nil {
|
||||
t.Fatal("createFolder() on existing folder should return error")
|
||||
}
|
||||
if err.Error() != "folder already exists" {
|
||||
t.Errorf("error message = %q", err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeleteFile(t *testing.T) {
|
||||
t.Run("delete file successfully", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
filePath := filepath.Join(tmpDir, "delete.txt")
|
||||
os.WriteFile(filePath, []byte("content"), 0644)
|
||||
|
||||
err := deleteFile(filePath)
|
||||
if err != nil {
|
||||
t.Fatalf("deleteFile() error = %v", err)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(filePath); !os.IsNotExist(err) {
|
||||
t.Error("file still exists after deletion")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("error when path is empty", func(t *testing.T) {
|
||||
err := deleteFile("")
|
||||
if err == nil {
|
||||
t.Fatal("deleteFile() with empty path should return error")
|
||||
}
|
||||
if err.Error() != "file path cannot be empty" {
|
||||
t.Errorf("error message = %q", err.Error())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("error when file does not exist", func(t *testing.T) {
|
||||
err := deleteFile("/nonexistent/file.txt")
|
||||
if err == nil {
|
||||
t.Fatal("deleteFile() on nonexistent file should return error")
|
||||
}
|
||||
if err.Error() != "file does not exist" {
|
||||
t.Errorf("error message = %q", err.Error())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("error when path is a directory", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
folderPath := filepath.Join(tmpDir, "folder")
|
||||
os.Mkdir(folderPath, 0755)
|
||||
|
||||
err := deleteFile(folderPath)
|
||||
if err == nil {
|
||||
t.Fatal("deleteFile() on directory should return error")
|
||||
}
|
||||
if err.Error() != "path is a directory, not a file" {
|
||||
t.Errorf("error message = %q", err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeleteFolder(t *testing.T) {
|
||||
t.Run("delete empty folder successfully", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
folderPath := filepath.Join(tmpDir, "emptyfolder")
|
||||
os.Mkdir(folderPath, 0755)
|
||||
|
||||
err := deleteFolder(folderPath)
|
||||
if err != nil {
|
||||
t.Fatalf("deleteFolder() error = %v", err)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(folderPath); !os.IsNotExist(err) {
|
||||
t.Error("folder still exists after deletion")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("delete folder with contents", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
folderPath := filepath.Join(tmpDir, "withcontents")
|
||||
os.Mkdir(folderPath, 0755)
|
||||
os.WriteFile(filepath.Join(folderPath, "file.txt"), []byte("content"), 0644)
|
||||
os.Mkdir(filepath.Join(folderPath, "subfolder"), 0755)
|
||||
|
||||
err := deleteFolder(folderPath)
|
||||
if err != nil {
|
||||
t.Fatalf("deleteFolder() error = %v", err)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(folderPath); !os.IsNotExist(err) {
|
||||
t.Error("folder still exists after deletion")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("error when path is empty", func(t *testing.T) {
|
||||
err := deleteFolder("")
|
||||
if err == nil {
|
||||
t.Fatal("deleteFolder() with empty path should return error")
|
||||
}
|
||||
if err.Error() != "folder path cannot be empty" {
|
||||
t.Errorf("error message = %q", err.Error())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("error when folder does not exist", func(t *testing.T) {
|
||||
err := deleteFolder("/nonexistent/folder")
|
||||
if err == nil {
|
||||
t.Fatal("deleteFolder() on nonexistent folder should return error")
|
||||
}
|
||||
if err.Error() != "folder does not exist" {
|
||||
t.Errorf("error message = %q", err.Error())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("error when path is a file", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
filePath := filepath.Join(tmpDir, "file.txt")
|
||||
os.WriteFile(filePath, []byte("content"), 0644)
|
||||
|
||||
err := deleteFolder(filePath)
|
||||
if err == nil {
|
||||
t.Fatal("deleteFolder() on file should return error")
|
||||
}
|
||||
if err.Error() != "path is a file, not a directory" {
|
||||
t.Errorf("error message = %q", err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
This file contains the implementation of an in-memory tree of files and folders that the app used.
|
||||
It is initialised at startup and used for state changes in UI and later persisted to disk.
|
||||
|
||||
- ruinivist, 30Dec25
|
||||
*/
|
||||
|
||||
package fstree
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"mend/utils"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// enums for node types
|
||||
type FsNodeType int
|
||||
|
||||
const (
|
||||
FileNode FsNodeType = iota
|
||||
FolderNode
|
||||
)
|
||||
|
||||
// ==================== FsNode definition ====================
|
||||
// a single node, Fs => deals with file system related info mostly
|
||||
// a name for example can be content derived as well
|
||||
type FsNode struct {
|
||||
nodeType FsNodeType
|
||||
path string
|
||||
children []*FsNode
|
||||
parent *FsNode // for fast traversal up the tree
|
||||
// for view layer
|
||||
expanded bool
|
||||
}
|
||||
|
||||
func (n *FsNode) FileName() string {
|
||||
return filepath.Base(n.path)
|
||||
}
|
||||
|
||||
// ==================== FsNode definition ====================
|
||||
|
||||
// ==================== FsTree definition ====================
|
||||
type FsTree interface {
|
||||
CreateNode(parent *FsNode, name string, nodeType FsNodeType) error
|
||||
DeleteNode(node *FsNode) error
|
||||
ToggleExpand(node *FsNode) error
|
||||
}
|
||||
|
||||
// reminder: a bit redundant but I this above shows what funcs are there at a glace
|
||||
// so I still like to use this pattern ( 30Dec25)
|
||||
type FsTreeImpl struct {
|
||||
root *FsNode
|
||||
}
|
||||
|
||||
func NewFsTree(rootPath string) *FsTreeImpl {
|
||||
root := &FsNode{
|
||||
nodeType: FolderNode,
|
||||
path: rootPath,
|
||||
children: make([]*FsNode, 0),
|
||||
expanded: true,
|
||||
}
|
||||
walkFileSystemAndBuildTree(rootPath, root)
|
||||
|
||||
return &FsTreeImpl{
|
||||
root: root,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *FsTreeImpl) CreateNode(parent *FsNode, name string, nodeType FsNodeType) error {
|
||||
if parent == nil {
|
||||
return errors.New("parent node cannot be nil")
|
||||
}
|
||||
if parent.nodeType != FolderNode {
|
||||
return errors.New("can only add nodes to folder nodes")
|
||||
}
|
||||
if name == "" {
|
||||
return errors.New("node name cannot be empty")
|
||||
}
|
||||
|
||||
newNode := &FsNode{
|
||||
nodeType: nodeType,
|
||||
path: filepath.Join(parent.path, name),
|
||||
children: make([]*FsNode, 0),
|
||||
parent: parent,
|
||||
expanded: false,
|
||||
}
|
||||
|
||||
parent.children = append(parent.children, newNode)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *FsTreeImpl) DeleteNode(node *FsNode) error {
|
||||
if node == nil {
|
||||
return errors.New("node to delete cannot be nil")
|
||||
}
|
||||
parent := node.parent
|
||||
if parent == nil {
|
||||
return errors.New("node to delete must have a parent")
|
||||
}
|
||||
|
||||
parent.children = utils.RemoveFromSlice(parent.children, node)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToggleExpand toggles the expanded state of a node
|
||||
func (t *FsTreeImpl) ToggleExpand(node *FsNode) error {
|
||||
if node == nil {
|
||||
return errors.New("node cannot be nil")
|
||||
}
|
||||
if node.nodeType != FolderNode {
|
||||
return errors.New("only folder nodes can be expanded or collapsed")
|
||||
}
|
||||
|
||||
node.expanded = !node.expanded
|
||||
return nil
|
||||
}
|
||||
|
||||
// ==================== FsTree definition ====================
|
||||
@@ -0,0 +1,280 @@
|
||||
/*
|
||||
This file is almost entirely ae ayee generated. That's one of the
|
||||
few good uses of ae ayee in my opinion
|
||||
|
||||
- ruinivist, 30Dec25
|
||||
*/
|
||||
|
||||
package fstree
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFsNode_FileName(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "simple filename",
|
||||
path: "/home/user/test.txt",
|
||||
expected: "test.txt",
|
||||
},
|
||||
{
|
||||
name: "folder name",
|
||||
path: "/home/user/folder",
|
||||
expected: "folder",
|
||||
},
|
||||
{
|
||||
name: "root path",
|
||||
path: "/",
|
||||
expected: "/",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
node := &FsNode{path: tt.path}
|
||||
got := node.FileName()
|
||||
if got != tt.expected {
|
||||
t.Errorf("FileName() = %q, want %q", got, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewFsTree(t *testing.T) {
|
||||
rootPath := "/home/test"
|
||||
tree := NewFsTree(rootPath)
|
||||
|
||||
if tree == nil {
|
||||
t.Fatal("NewFsTree() returned nil")
|
||||
}
|
||||
if tree.root == nil {
|
||||
t.Fatal("tree.root is nil")
|
||||
}
|
||||
if tree.root.nodeType != FolderNode {
|
||||
t.Errorf("root nodeType = %v, want %v", tree.root.nodeType, FolderNode)
|
||||
}
|
||||
if tree.root.path != rootPath {
|
||||
t.Errorf("root path = %q, want %q", tree.root.path, rootPath)
|
||||
}
|
||||
if !tree.root.expanded {
|
||||
t.Error("root should be expanded by default")
|
||||
}
|
||||
if tree.root.children == nil {
|
||||
t.Error("root children slice should be initialized")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFsTreeImpl_CreateNode(t *testing.T) {
|
||||
t.Run("create file node successfully", func(t *testing.T) {
|
||||
tree := NewFsTree("/home/test")
|
||||
err := tree.CreateNode(tree.root, "file.txt", FileNode)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateNode() error = %v, want nil", err)
|
||||
}
|
||||
if len(tree.root.children) != 1 {
|
||||
t.Fatalf("children count = %d, want 1", len(tree.root.children))
|
||||
}
|
||||
|
||||
child := tree.root.children[0]
|
||||
if child.nodeType != FileNode {
|
||||
t.Errorf("child nodeType = %v, want %v", child.nodeType, FileNode)
|
||||
}
|
||||
expectedPath := filepath.Join("/home/test", "file.txt")
|
||||
if child.path != expectedPath {
|
||||
t.Errorf("child path = %q, want %q", child.path, expectedPath)
|
||||
}
|
||||
if child.parent != tree.root {
|
||||
t.Error("child parent should point to root")
|
||||
}
|
||||
if child.expanded {
|
||||
t.Error("new node should not be expanded by default")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("create folder node successfully", func(t *testing.T) {
|
||||
tree := NewFsTree("/home/test")
|
||||
err := tree.CreateNode(tree.root, "subfolder", FolderNode)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateNode() error = %v, want nil", err)
|
||||
}
|
||||
if len(tree.root.children) != 1 {
|
||||
t.Fatalf("children count = %d, want 1", len(tree.root.children))
|
||||
}
|
||||
|
||||
child := tree.root.children[0]
|
||||
if child.nodeType != FolderNode {
|
||||
t.Errorf("child nodeType = %v, want %v", child.nodeType, FolderNode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("error when parent is nil", func(t *testing.T) {
|
||||
tree := NewFsTree("/home/test")
|
||||
err := tree.CreateNode(nil, "file.txt", FileNode)
|
||||
if err == nil {
|
||||
t.Fatal("CreateNode() with nil parent should return error")
|
||||
}
|
||||
if err.Error() != "parent node cannot be nil" {
|
||||
t.Errorf("error message = %q, want %q", err.Error(), "parent node cannot be nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("error when parent is file node", func(t *testing.T) {
|
||||
tree := NewFsTree("/home/test")
|
||||
fileNode := &FsNode{
|
||||
nodeType: FileNode,
|
||||
path: "/home/test/file.txt",
|
||||
parent: tree.root,
|
||||
}
|
||||
err := tree.CreateNode(fileNode, "child.txt", FileNode)
|
||||
if err == nil {
|
||||
t.Fatal("CreateNode() with file parent should return error")
|
||||
}
|
||||
if err.Error() != "can only add nodes to folder nodes" {
|
||||
t.Errorf("error message = %q, want %q", err.Error(), "can only add nodes to folder nodes")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("error when name is empty", func(t *testing.T) {
|
||||
tree := NewFsTree("/home/test")
|
||||
err := tree.CreateNode(tree.root, "", FileNode)
|
||||
if err == nil {
|
||||
t.Fatal("CreateNode() with empty name should return error")
|
||||
}
|
||||
if err.Error() != "node name cannot be empty" {
|
||||
t.Errorf("error message = %q, want %q", err.Error(), "node name cannot be empty")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("create nested nodes", func(t *testing.T) {
|
||||
tree := NewFsTree("/home/test")
|
||||
err := tree.CreateNode(tree.root, "folder1", FolderNode)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateNode() error = %v", err)
|
||||
}
|
||||
folder1 := tree.root.children[0]
|
||||
|
||||
err = tree.CreateNode(folder1, "file.txt", FileNode)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateNode() error = %v", err)
|
||||
}
|
||||
|
||||
if len(folder1.children) != 1 {
|
||||
t.Fatalf("folder1 children count = %d, want 1", len(folder1.children))
|
||||
}
|
||||
expectedPath := filepath.Join("/home/test", "folder1", "file.txt")
|
||||
if folder1.children[0].path != expectedPath {
|
||||
t.Errorf("nested file path = %q, want %q", folder1.children[0].path, expectedPath)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestFsTreeImpl_DeleteNode(t *testing.T) {
|
||||
t.Run("delete node successfully", func(t *testing.T) {
|
||||
tree := NewFsTree("/home/test")
|
||||
tree.CreateNode(tree.root, "file1.txt", FileNode)
|
||||
tree.CreateNode(tree.root, "file2.txt", FileNode)
|
||||
|
||||
if len(tree.root.children) != 2 {
|
||||
t.Fatalf("initial children count = %d, want 2", len(tree.root.children))
|
||||
}
|
||||
|
||||
nodeToDelete := tree.root.children[0]
|
||||
err := tree.DeleteNode(nodeToDelete)
|
||||
if err != nil {
|
||||
t.Fatalf("DeleteNode() error = %v, want nil", err)
|
||||
}
|
||||
|
||||
if len(tree.root.children) != 1 {
|
||||
t.Errorf("children count after delete = %d, want 1", len(tree.root.children))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("error when node is nil", func(t *testing.T) {
|
||||
tree := NewFsTree("/home/test")
|
||||
err := tree.DeleteNode(nil)
|
||||
if err == nil {
|
||||
t.Fatal("DeleteNode() with nil node should return error")
|
||||
}
|
||||
if err.Error() != "node to delete cannot be nil" {
|
||||
t.Errorf("error message = %q, want %q", err.Error(), "node to delete cannot be nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("error when node has no parent", func(t *testing.T) {
|
||||
tree := NewFsTree("/home/test")
|
||||
orphanNode := &FsNode{
|
||||
nodeType: FileNode,
|
||||
path: "/orphan.txt",
|
||||
parent: nil,
|
||||
}
|
||||
err := tree.DeleteNode(orphanNode)
|
||||
if err == nil {
|
||||
t.Fatal("DeleteNode() with no parent should return error")
|
||||
}
|
||||
if err.Error() != "node to delete must have a parent" {
|
||||
t.Errorf("error message = %q, want %q", err.Error(), "node to delete must have a parent")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestFsTreeImpl_ToggleExpand(t *testing.T) {
|
||||
t.Run("toggle folder expansion", func(t *testing.T) {
|
||||
tree := NewFsTree("/home/test")
|
||||
tree.CreateNode(tree.root, "folder1", FolderNode)
|
||||
folder := tree.root.children[0]
|
||||
|
||||
// Initially not expanded
|
||||
if folder.expanded {
|
||||
t.Error("new folder should not be expanded")
|
||||
}
|
||||
|
||||
// Toggle to expanded
|
||||
err := tree.ToggleExpand(folder)
|
||||
if err != nil {
|
||||
t.Fatalf("ToggleExpand() error = %v", err)
|
||||
}
|
||||
if !folder.expanded {
|
||||
t.Error("folder should be expanded after toggle")
|
||||
}
|
||||
|
||||
// Toggle back to collapsed
|
||||
err = tree.ToggleExpand(folder)
|
||||
if err != nil {
|
||||
t.Fatalf("ToggleExpand() error = %v", err)
|
||||
}
|
||||
if folder.expanded {
|
||||
t.Error("folder should be collapsed after second toggle")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("error when node is nil", func(t *testing.T) {
|
||||
tree := NewFsTree("/home/test")
|
||||
err := tree.ToggleExpand(nil)
|
||||
if err == nil {
|
||||
t.Fatal("ToggleExpand() with nil node should return error")
|
||||
}
|
||||
if err.Error() != "node cannot be nil" {
|
||||
t.Errorf("error message = %q, want %q", err.Error(), "node cannot be nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("error when node is file", func(t *testing.T) {
|
||||
tree := NewFsTree("/home/test")
|
||||
tree.CreateNode(tree.root, "file.txt", FileNode)
|
||||
fileNode := tree.root.children[0]
|
||||
|
||||
err := tree.ToggleExpand(fileNode)
|
||||
if err == nil {
|
||||
t.Fatal("ToggleExpand() on file node should return error")
|
||||
}
|
||||
if err.Error() != "only folder nodes can be expanded or collapsed" {
|
||||
t.Errorf("error message = %q, want %q", err.Error(), "only folder nodes can be expanded or collapsed")
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user