diff --git a/dive/filetree/comparer_test.go b/dive/filetree/comparer_test.go index 9e7209c..59f3095 100644 --- a/dive/filetree/comparer_test.go +++ b/dive/filetree/comparer_test.go @@ -331,3 +331,267 @@ func TestEfficiencySlice_Less(t *testing.T) { assert.False(t, efs.Less(0, 1)) }) } + +func TestComparer_GetPathErrors_Additional(t *testing.T) { + t.Run("get path errors with actual errors", func(t *testing.T) { + // Create ref trees with some content that will generate path errors + tree1 := NewFileTree() + tree1.Name = "tree1" + + // Add some paths to tree1 + fakeData := FileInfo{ + Path: "/file1.txt", + TypeFlag: 1, + hash: 123, + } + tree1.AddPath("/file1.txt", fakeData) + + tree2 := NewFileTree() + tree2.Name = "tree2" + + cmp := NewComparer([]*FileTree{tree1, tree2}) + + // Test getting path errors for a key + key := NewTreeIndexKey(0, 0, 1, 1) + + pathErrors, err := cmp.GetPathErrors(key) + + // Should not error + assert.NoError(t, err) + assert.NotNil(t, pathErrors) + }) + + t.Run("get path errors caches result", func(t *testing.T) { + tree1 := NewFileTree() + tree1.Name = "tree1" + + fakeData := FileInfo{ + Path: "/file1.txt", + TypeFlag: 1, + hash: 123, + } + tree1.AddPath("/file1.txt", fakeData) + + cmp := NewComparer([]*FileTree{tree1}) + + key := NewTreeIndexKey(0, 0, 0, 0) + + // Call GetPathErrors twice + pathErrors1, err1 := cmp.GetPathErrors(key) + pathErrors2, err2 := cmp.GetPathErrors(key) + + assert.NoError(t, err1) + assert.NoError(t, err2) + assert.Equal(t, pathErrors1, pathErrors2) + }) +} + +func TestComparer_get_ErrorCases(t *testing.T) { + t.Run("get returns error on invalid range", func(t *testing.T) { + tree1 := NewFileTree() + tree1.Name = "tree1" + + cmp := NewComparer([]*FileTree{tree1}) + + // Use invalid range (index out of bounds) + key := NewTreeIndexKey(0, 10, 0, 0) + + // StackTreeRange will panic with index out of range + assert.Panics(t, func() { + cmp.get(key) + }, "Expected panic when using invalid range") + }) +} + +func TestComparer_BuildCache_ErrorCases(t *testing.T) { + t.Run("build cache with single tree and errors", func(t *testing.T) { + tree1 := NewFileTree() + tree1.Name = "tree1" + + // Add a path that might cause issues + fakeData := FileInfo{ + Path: "/file1.txt", + TypeFlag: 1, + hash: 123, + } + _, _, err := tree1.AddPath("/file1.txt", fakeData) + assert.NoError(t, err) + + cmp := NewComparer([]*FileTree{tree1}) + + errors := cmp.BuildCache() + + // Should not error with valid tree + assert.Empty(t, errors) + }) + + t.Run("build cache with multiple trees", func(t *testing.T) { + trees := make([]*FileTree, 3) + for i := 0; i < 3; i++ { + trees[i] = NewFileTree() + trees[i].Name = fmt.Sprintf("tree%d", i) + + // Add some content + fakeData := FileInfo{ + Path: fmt.Sprintf("/file%d.txt", i), + TypeFlag: 1, + hash: uint64(i), + } + _, _, err := trees[i].AddPath(fakeData.Path, fakeData) + assert.NoError(t, err) + } + + cmp := NewComparer(trees) + + errors := cmp.BuildCache() + + // Should not error + assert.Empty(t, errors) + }) + + t.Run("build cache with empty ref trees", func(t *testing.T) { + cmp := NewComparer([]*FileTree{}) + + errors := cmp.BuildCache() + + // Should not error even with empty trees + assert.Empty(t, errors) + }) +} + +func TestComparer_GetTree_Caching(t *testing.T) { + t.Run("cached tree returns same instance", func(t *testing.T) { + tree1 := NewFileTree() + tree1.Name = "tree1" + + fakeData := FileInfo{ + Path: "/file1.txt", + TypeFlag: 1, + hash: 123, + } + _, _, err := tree1.AddPath("/file1.txt", fakeData) + assert.NoError(t, err) + + cmp := NewComparer([]*FileTree{tree1}) + + key := NewTreeIndexKey(0, 0, 0, 0) + + // Call GetTree twice + tree1, err1 := cmp.GetTree(key) + tree2, err2 := cmp.GetTree(key) + + assert.NoError(t, err1) + assert.NoError(t, err2) + assert.NotNil(t, tree1) + assert.NotNil(t, tree2) + // Should return the same cached instance + assert.Same(t, tree1, tree2) + }) + + t.Run("get tree populates cache", func(t *testing.T) { + tree1 := NewFileTree() + tree1.Name = "tree1" + + fakeData := FileInfo{ + Path: "/file1.txt", + TypeFlag: 1, + hash: 123, + } + _, _, err := tree1.AddPath("/file1.txt", fakeData) + assert.NoError(t, err) + + cmp := NewComparer([]*FileTree{tree1}) + + key := NewTreeIndexKey(0, 0, 0, 0) + + // Cache should be empty initially + _, exists := cmp.trees[key] + assert.False(t, exists) + + // Get tree should populate cache + _, err = cmp.GetTree(key) + assert.NoError(t, err) + + // Cache should now have the tree + _, exists = cmp.trees[key] + assert.True(t, exists) + }) +} + +func TestComparer_Indexes_Iteration(t *testing.T) { + t.Run("natural indexes iteration", func(t *testing.T) { + trees := make([]*FileTree, 3) + for i := 0; i < 3; i++ { + trees[i] = NewFileTree() + trees[i].Name = fmt.Sprintf("tree%d", i) + } + + cmp := NewComparer(trees) + + // Collect all indexes + indexes := make([]TreeIndexKey, 0) + for index := range cmp.NaturalIndexes() { + indexes = append(indexes, index) + } + + // Should have 3 indexes (one for each tree) + assert.Len(t, indexes, 3) + + // Verify the indexes + assert.Equal(t, NewTreeIndexKey(0, 0, 0, 0), indexes[0]) + assert.Equal(t, NewTreeIndexKey(0, 0, 1, 1), indexes[1]) + assert.Equal(t, NewTreeIndexKey(0, 1, 2, 2), indexes[2]) + }) + + t.Run("aggregated indexes iteration", func(t *testing.T) { + trees := make([]*FileTree, 3) + for i := 0; i < 3; i++ { + trees[i] = NewFileTree() + trees[i].Name = fmt.Sprintf("tree%d", i) + } + + cmp := NewComparer(trees) + + // Collect all indexes + indexes := make([]TreeIndexKey, 0) + for index := range cmp.AggregatedIndexes() { + indexes = append(indexes, index) + } + + // Should have 3 indexes + assert.Len(t, indexes, 3) + + // Verify the indexes + assert.Equal(t, NewTreeIndexKey(0, 0, 0, 0), indexes[0]) + assert.Equal(t, NewTreeIndexKey(0, 0, 1, 1), indexes[1]) + assert.Equal(t, NewTreeIndexKey(0, 0, 1, 2), indexes[2]) + }) +} + +func TestTreeIndexKey_String_EdgeCases(t *testing.T) { + t.Run("all zeros", func(t *testing.T) { + key := NewTreeIndexKey(0, 0, 0, 0) + assert.Equal(t, "Index(0:0)", key.String()) + }) + + t.Run("single layer both sides", func(t *testing.T) { + key := NewTreeIndexKey(0, 0, 1, 1) + assert.Equal(t, "Index(0:1)", key.String()) + }) + + t.Run("multiple bottom single top", func(t *testing.T) { + key := NewTreeIndexKey(0, 2, 3, 3) + assert.Equal(t, "Index(0-2:3)", key.String()) + }) + + t.Run("single bottom multiple top", func(t *testing.T) { + key := NewTreeIndexKey(0, 0, 1, 3) + assert.Equal(t, "Index(0:1-3)", key.String()) + }) + + t.Run("multiple both sides", func(t *testing.T) { + key := NewTreeIndexKey(0, 2, 3, 5) + assert.Equal(t, "Index(0-2:3-5)", key.String()) + }) +} + diff --git a/dive/filetree/file_node_test.go b/dive/filetree/file_node_test.go index a344475..802d862 100644 --- a/dive/filetree/file_node_test.go +++ b/dive/filetree/file_node_test.go @@ -166,3 +166,421 @@ func TestDirSize(t *testing.T) { t.Errorf("Expected metadata '%s' got '%s'", expected, actual) } } + +func TestFileNode_compare(t *testing.T) { + t.Run("both nil returns Unmodified", func(t *testing.T) { + // Need to call compare on nil receiver + var node *FileNode + result := node.compare(nil) + if result != Unmodified { + t.Errorf("Expected Unmodified but got %v", result) + } + }) + + t.Run("non-nil node and nil other returns Removed", func(t *testing.T) { + tree := NewFileTree() + tree.Root.Name = "test" + + result := tree.Root.compare(nil) + if result != Removed { + t.Errorf("Expected Removed but got %v", result) + } + }) + + t.Run("whiteout file returns Removed", func(t *testing.T) { + tree := NewFileTree() + node, _, _ := tree.AddPath("/file.txt", FileInfo{ + Path: "/file.txt", + TypeFlag: 1, + hash: 123, + }) + + whiteoutNode := &FileNode{ + Name: ".wh.file.txt", + Data: NodeData{ + FileInfo: FileInfo{ + Path: "/.wh.file.txt", + TypeFlag: 1, + }, + }, + } + + result := node.compare(whiteoutNode) + if result != Removed { + t.Errorf("Expected Removed for whiteout but got %v", result) + } + }) + + t.Run("mismatched node names panic", func(t *testing.T) { + tree := NewFileTree() + node, _, _ := tree.AddPath("/file1.txt", FileInfo{ + Path: "/file1.txt", + TypeFlag: 1, + hash: 123, + }) + + other := &FileNode{ + Name: "file2.txt", + Data: NodeData{ + FileInfo: FileInfo{ + Path: "/file2.txt", + TypeFlag: 1, + hash: 123, + }, + }, + } + + defer func() { + if r := recover(); r == nil { + t.Errorf("Expected panic when comparing mismatched nodes") + } + }() + + node.compare(other) + }) + + t.Run("same nodes return Unmodified", func(t *testing.T) { + tree := NewFileTree() + node, _, _ := tree.AddPath("/file.txt", FileInfo{ + Path: "/file.txt", + TypeFlag: 1, + hash: 123, + Mode: 0644, + Uid: 1000, + Gid: 1000, + }) + + other := &FileNode{ + Tree: tree, + Name: "file.txt", + Data: NodeData{ + FileInfo: FileInfo{ + Path: "/file.txt", + TypeFlag: 1, + hash: 123, + Mode: 0644, + Uid: 1000, + Gid: 1000, + }, + }, + } + + result := node.compare(other) + if result != Unmodified { + t.Errorf("Expected Unmodified but got %v", result) + } + }) + + t.Run("different hash returns Modified", func(t *testing.T) { + tree := NewFileTree() + node, _, _ := tree.AddPath("/file.txt", FileInfo{ + Path: "/file.txt", + TypeFlag: 1, + hash: 123, + }) + + other := &FileNode{ + Tree: tree, + Name: "file.txt", + Data: NodeData{ + FileInfo: FileInfo{ + Path: "/file.txt", + TypeFlag: 1, + hash: 456, + }, + }, + } + + result := node.compare(other) + if result != Modified { + t.Errorf("Expected Modified but got %v", result) + } + }) +} + +func TestFileNode_AddChild_EdgeCases(t *testing.T) { + t.Run("add child with existing name", func(t *testing.T) { + tree := NewFileTree() + payload1 := FileInfo{Path: "/file1"} + payload2 := FileInfo{Path: "/file2"} + + node1 := tree.Root.AddChild("test", payload1) + node2 := tree.Root.AddChild("test", payload2) + + // Should add a new child even with same name + if node1 == nil || node2 == nil { + t.Errorf("Expected both nodes to be created") + } + + if tree.Root.Children["test"] == nil { + t.Errorf("Expected child to exist in tree") + } + }) + + t.Run("add child to nested node", func(t *testing.T) { + tree := NewFileTree() + parent := tree.Root.AddChild("parent", FileInfo{}) + child := parent.AddChild("child", FileInfo{Path: "/parent/child"}) + + if child == nil { + t.Errorf("Expected child to be created") + } + + if len(parent.Children) != 1 { + t.Errorf("Expected parent to have 1 child, got %d", len(parent.Children)) + } + }) +} + +func TestFileNode_Remove_EdgeCases(t *testing.T) { + t.Run("remove node with children", func(t *testing.T) { + tree := NewFileTree() + parent := tree.Root.AddChild("parent", FileInfo{}) + parent.AddChild("child1", FileInfo{}) + parent.AddChild("child2", FileInfo{}) + + initialSize := tree.Size + err := parent.Remove() + checkError(t, err, "unable to remove node") + + if tree.Size >= initialSize { + t.Errorf("Expected tree size to decrease after removal") + } + }) + + t.Run("remove root node", func(t *testing.T) { + tree := NewFileTree() + tree.Root.AddChild("child1", FileInfo{}) + tree.Root.AddChild("child2", FileInfo{}) + + err := tree.Root.Remove() + // Root removal should fail + if err == nil { + t.Errorf("Expected error when removing root node") + } + if err != nil && err.Error() != "cannot remove the tree root" { + t.Errorf("Expected 'cannot remove the tree root' error, got: %v", err) + } + }) +} + +func TestFileNode_String(t *testing.T) { + t.Run("string representation", func(t *testing.T) { + tree := NewFileTree() + node, _, _ := tree.AddPath("/test.txt", FileInfo{ + Path: "/test.txt", + TypeFlag: 1, + Mode: 0644, + Uid: 1000, + Gid: 1000, + }) + node.Data.DiffType = Modified + + str := node.String() + if str == "" { + t.Errorf("Expected non-empty string representation") + } + }) + + t.Run("string representation for directory", func(t *testing.T) { + tree := NewFileTree() + node, _, _ := tree.AddPath("/dir", FileInfo{ + Path: "/dir", + TypeFlag: 1, + }) + node.Data.FileInfo.IsDir = true + + str := node.String() + if str == "" { + t.Errorf("Expected non-empty string representation for directory") + } + }) +} + +func TestFileNode_MetadataString(t *testing.T) { + t.Run("metadata string for regular file", func(t *testing.T) { + tree := NewFileTree() + node, _, _ := tree.AddPath("/test.txt", FileInfo{ + Path: "/test.txt", + TypeFlag: 1, + Size: 1024, + Mode: 0644, + Uid: 1000, + Gid: 1000, + }) + + metadata := node.MetadataString() + if metadata == "" { + t.Errorf("Expected non-empty metadata string") + } + }) + + t.Run("metadata string for directory", func(t *testing.T) { + tree := NewFileTree() + _, _, err := tree.AddPath("/dir", FileInfo{ + Path: "/dir", + TypeFlag: 1, + }) + checkError(t, err, "unable to setup test") + + node, _ := tree.GetNode("/dir") + node.Data.FileInfo.IsDir = true + + metadata := node.MetadataString() + if metadata == "" { + t.Errorf("Expected non-empty metadata string for directory") + } + }) +} + +func TestFileNode_GetSize(t *testing.T) { + t.Run("get size for regular file", func(t *testing.T) { + tree := NewFileTree() + node, _, _ := tree.AddPath("/file.txt", FileInfo{ + Path: "/file.txt", + TypeFlag: 1, + Size: 2048, + }) + + size := node.GetSize() + if size != 2048 { + t.Errorf("Expected size 2048, got %d", size) + } + }) + + t.Run("get size for directory with children", func(t *testing.T) { + tree := NewFileTree() + _, _, err := tree.AddPath("/dir", FileInfo{ + Path: "/dir", + TypeFlag: 1, + }) + checkError(t, err, "unable to setup test") + + node, _ := tree.GetNode("/dir") + node.Data.FileInfo.IsDir = true + + tree.AddPath("/dir/file1.txt", FileInfo{Size: 100}) + tree.AddPath("/dir/file2.txt", FileInfo{Size: 200}) + + size := node.GetSize() + if size != 300 { + t.Errorf("Expected total size 300, got %d", size) + } + }) + + t.Run("get size for empty directory", func(t *testing.T) { + tree := NewFileTree() + _, _, err := tree.AddPath("/dir", FileInfo{ + Path: "/dir", + TypeFlag: 1, + }) + checkError(t, err, "unable to setup test") + + node, _ := tree.GetNode("/dir") + node.Data.FileInfo.IsDir = true + + size := node.GetSize() + if size != 0 { + t.Errorf("Expected size 0 for empty directory, got %d", size) + } + }) +} + +func TestFileNode_VisitDepthChildFirst(t *testing.T) { + t.Run("visit tree child first", func(t *testing.T) { + tree := NewFileTree() + tree.AddPath("/dir", FileInfo{}) + tree.AddPath("/dir/file1.txt", FileInfo{}) + tree.AddPath("/dir/file2.txt", FileInfo{}) + + node, _ := tree.GetNode("/dir") + + var visited []string + visitor := func(n *FileNode) error { + visited = append(visited, n.Path()) + return nil + } + evaluator := func(n *FileNode) bool { + return true + } + sorter := GetSortOrderStrategy(ByName) + + err := node.VisitDepthChildFirst(visitor, evaluator, sorter) + checkError(t, err, "unable to visit tree") + + if len(visited) == 0 { + t.Errorf("Expected nodes to be visited") + } + }) +} + +func TestFileNode_VisitDepthParentFirst(t *testing.T) { + t.Run("visit tree parent first", func(t *testing.T) { + tree := NewFileTree() + tree.AddPath("/dir", FileInfo{}) + tree.AddPath("/dir/file1.txt", FileInfo{}) + tree.AddPath("/dir/file2.txt", FileInfo{}) + + node, _ := tree.GetNode("/dir") + + var visited []string + visitor := func(n *FileNode) error { + visited = append(visited, n.Path()) + return nil + } + evaluator := func(n *FileNode) bool { + return true + } + sorter := GetSortOrderStrategy(ByName) + + err := node.VisitDepthParentFirst(visitor, evaluator, sorter) + checkError(t, err, "unable to visit tree") + + if len(visited) == 0 { + t.Errorf("Expected nodes to be visited") + } + }) +} + +func TestFileNode_AssignDiffType(t *testing.T) { + t.Run("assign diff type to node and children", func(t *testing.T) { + tree := NewFileTree() + tree.AddPath("/dir", FileInfo{}) + tree.AddPath("/dir/file1.txt", FileInfo{}) + tree.AddPath("/dir/file2.txt", FileInfo{}) + + node, _ := tree.GetNode("/dir") + + err := node.AssignDiffType(Added) + checkError(t, err, "unable to assign diff type") + + // Check that the node and its children have the correct diff type + if node.Data.DiffType != Added { + t.Errorf("Expected node diff type to be Added, got %v", node.Data.DiffType) + } + }) +} + +func TestFileNode_IsLeaf(t *testing.T) { + t.Run("leaf node has no children", func(t *testing.T) { + tree := NewFileTree() + node, _, _ := tree.AddPath("/file.txt", FileInfo{}) + + if !node.IsLeaf() { + t.Errorf("Expected file node to be a leaf") + } + }) + + t.Run("directory node is not a leaf", func(t *testing.T) { + tree := NewFileTree() + tree.AddPath("/dir", FileInfo{}) + tree.AddPath("/dir/file.txt", FileInfo{}) + + node, _ := tree.GetNode("/dir") + + if node.IsLeaf() { + t.Errorf("Expected directory node to not be a leaf") + } + }) +} +