// Copyright Earl Warren // Copyright Loïc Dachary // SPDX-License-Identifier: MIT package generic import ( "context" "fmt" "path/filepath" "sort" "testing" "code.forgejo.org/f3/gof3/v3/kind" "code.forgejo.org/f3/gof3/v3/path" "code.forgejo.org/f3/gof3/v3/tree/generic" "code.forgejo.org/f3/gof3/v3/tree/memory" "github.com/stretchr/testify/assert" ) func TestUnifyPathSimpleRemap(t *testing.T) { ctx := context.Background() for _, testCase := range []struct { path string expected []string }{ { path: "", expected: []string{}, }, { path: "/O-A", expected: []string{"/O-A:O-A=content O-A => /D-A:D-A=content O-A"}, }, { path: "/O-A/O-B", expected: []string{"/O-A:O-A=content O-A => /D-A:D-A=content O-A", "/O-A/O-B:O-B=content O-B => /D-A/D-B:D-B=content O-B"}, }, { path: "/O-A/O-B/O-C", expected: []string{"/O-A:O-A=content O-A => /D-A:D-A=content O-A", "/O-A/O-B:O-B=content O-B => /D-A/D-B:D-B=content O-B", "/O-A/O-B/O-C:O-C=content O-C => /D-A/D-B/D-C:D-C=content O-C"}, }, } { t.Run(" "+testCase.path, func(t *testing.T) { originTree := NewMemoryTree(ctx, "O") testTreeBuild(t, originTree, 2) destinationTree := NewMemoryTree(ctx, "D") collected := make([]string, 0, 10) p := generic.NewPathFromString(testCase.path) upsert := func(ctx context.Context, origin generic.NodeInterface, originPath path.Path, destination generic.NodeInterface, destinationPath path.Path) { fmt.Printf("origin %v destination %v\n", origin, destination) originPath = originPath.Append(origin) destinationPath = destinationPath.Append(destination) collected = append(collected, originPath.String()+":"+origin.String()+" => "+destinationPath.String()+":"+destination.String()) } generic.TreeUnifyPath(ctx, originTree, p, destinationTree, generic.NewUnifyOptions(destinationTree).SetUpsert(upsert)) assert.EqualValues(t, testCase.expected, collected) collected = make([]string, 0, 10) generic.TreeUnifyPath(ctx, originTree, p, destinationTree, generic.NewUnifyOptions(destinationTree).SetUpsert(upsert)) assert.EqualValues(t, testCase.expected, collected) }) } } func TestUnifyPathSimpleNoRemap(t *testing.T) { ctx := context.Background() for _, testCase := range []struct { noremap bool path string expected []string }{ { noremap: false, path: "/O-A/O-B", expected: []string{"/O-A:O-A=content O-A => /D-A:D-A=content O-A", "/O-A/O-B:O-B=content O-B => /D-A/D-B:D-B=content O-B"}, }, { noremap: true, path: "/O-A/O-B", expected: []string{"/O-A:O-A=content O-A => /O-A:O-A=content O-A", "/O-A/O-B:O-B=content O-B => /O-A/O-B:O-B=content O-B"}, }, } { t.Run(fmt.Sprintf("noremap=%v,path=%s", testCase.noremap, testCase.path), func(t *testing.T) { originName := "O" originTree := NewMemoryTree(ctx, originName) testTreeBuild(t, originTree, 2) var destinationName string if testCase.noremap { destinationName = originName } else { destinationName = "D" } destinationTree := NewMemoryTree(ctx, destinationName) collected := make([]string, 0, 10) p := generic.NewPathFromString(testCase.path) upsert := func(ctx context.Context, origin generic.NodeInterface, originPath path.Path, destination generic.NodeInterface, destinationPath path.Path) { fmt.Printf("origin %v destination %v\n", origin, destination) originPath = originPath.Append(origin) destinationPath = destinationPath.Append(destination) collected = append(collected, originPath.String()+":"+origin.String()+" => "+destinationPath.String()+":"+destination.String()) } generic.TreeUnifyPath(ctx, originTree, p, destinationTree, generic.NewUnifyOptions(destinationTree).SetUpsert(upsert).SetNoRemap(testCase.noremap)) assert.EqualValues(t, testCase.expected, collected) collected = make([]string, 0, 10) generic.TreeUnifyPath(ctx, originTree, p, destinationTree, generic.NewUnifyOptions(destinationTree).SetUpsert(upsert).SetNoRemap(testCase.noremap)) assert.EqualValues(t, testCase.expected, collected) }) } } func TestUnifyPathRelative(t *testing.T) { ctx := context.Background() for _, testCase := range []struct { start string destination string expected []string }{ { start: "/O-A/O-B", destination: "O-C", expected: []string{"cd: /O-A/O-B:O-B=content O-B => /D-A/D-B:D-B=content O-B", "unify: /O-A/O-B:O-B=content O-B => /D-A/D-B:D-B=content O-B", "unify: /O-A/O-B/O-C:O-C=content O-C => /D-A/D-B/D-C:D-C=content O-C"}, }, { start: "/O-A/O-B", destination: ".", expected: []string{"cd: /O-A/O-B:O-B=content O-B => /D-A/D-B:D-B=content O-B", "unify: /O-A/O-B:O-B=content O-B => /D-A/D-B:D-B=content O-B"}, }, { start: "/O-A/O-B", destination: "../O-F/O-G", expected: []string{"cd: /O-A/O-B:O-B=content O-B => /D-A/D-B:D-B=content O-B", "unify: /O-A:O-A=content O-A => /D-A:D-A=content O-A", "unify: /O-A/O-F:O-F=content O-F => /D-A/D-C:D-C=content O-F", "unify: /O-A/O-F/O-G:O-G=content O-G => /D-A/D-C/D-D:D-D=content O-G"}, }, { start: "/O-A/O-B/O-C", destination: "../O-E", expected: []string{"cd: /O-A/O-B/O-C:O-C=content O-C => /D-A/D-B/D-C:D-C=content O-C", "unify: /O-A/O-B:O-B=content O-B => /D-A/D-B:D-B=content O-B", "unify: /O-A/O-B/O-E:O-E=content O-E => /D-A/D-B/D-D:D-D=content O-E"}, }, } { t.Run(" "+testCase.start+" => "+testCase.destination, func(t *testing.T) { originTree := NewMemoryTree(ctx, "O") testTreeBuild(t, originTree, 2) destinationTree := NewMemoryTree(ctx, "D") var collected []string start := generic.NewPathFromString(testCase.start) collect := func(prefix string, origin, destination generic.NodeInterface) { originPath := origin.GetCurrentPath().String() destinationPath := destination.GetCurrentPath().String() collected = append(collected, prefix+originPath+":"+origin.GetSelf().String()+" => "+destinationPath+":"+destination.GetSelf().String()) } // // Unify testCase.start // upsert := func(ctx context.Context, origin generic.NodeInterface, originParent path.Path, destination generic.NodeInterface, destinationParent path.Path) { collect("unify: ", origin, destination) } generic.TreeUnifyPath(ctx, originTree, start, destinationTree, generic.NewUnifyOptions(destinationTree).SetUpsert(upsert)) // // Beginning from testCase.start, unify testCase.destination // cd := func(ctx context.Context, origin, destination generic.NodeInterface) { collect("cd: ", origin, destination) path := generic.NewPathFromString(testCase.destination) originParent := origin.GetParent().GetCurrentPath() destinationParent := destination.GetParent().GetCurrentPath() generic.NodeUnifyPath(ctx, origin.GetSelf(), originParent, path, destinationParent, generic.NewUnifyOptions(destinationTree).SetUpsert(upsert)) } // // // collected = make([]string, 0, 10) generic.TreeParallelApply(ctx, originTree, start, destinationTree, generic.NewParallelApplyOptions(cd)) assert.EqualValues(t, testCase.expected, collected) // // Do it twice to verify it is idempotent // collected = make([]string, 0, 10) generic.TreeParallelApply(ctx, originTree, start, destinationTree, generic.NewParallelApplyOptions(cd)) assert.EqualValues(t, testCase.expected, collected) }) } } func TestUnifyPathScenario(t *testing.T) { ctx := context.Background() // // build and populate a tree // originTree := NewMemoryTree(ctx, "O") testTreeBuild(t, originTree, 2) // // build an empty tree // destinationTree := NewMemoryTree(ctx, "D") // // accumulate the call results for verification // var collected []string upsert := func(ctx context.Context, origin generic.NodeInterface, originPath path.Path, destination generic.NodeInterface, destinationPath path.Path) { originPath = originPath.Append(origin) destinationPath = destinationPath.Append(destination) what := originPath.String() + ":" + origin.String() + " => " + destinationPath.String() + ":" + destination.String() fmt.Printf("unify: %T => %T | %s\n", origin, destination, what) collected = append(collected, what) } assertTree := func(tree generic.TreeInterface, expected []string) { collected := make([]string, 0, 10) tree.Walk(ctx, generic.NewWalkOptions(func(ctx context.Context, path path.Path, node generic.NodeInterface) { if node.GetKind() == kind.KindRoot { return } path = path.Append(node) collected = append(collected, path.String()+":"+node.String()) })) sort.Strings(collected) assert.EqualValues(t, expected, collected) } // // unify the originTree with the destinationTree on the specified path // fullPath := generic.NewPathFromString("/O-A/O-B/O-C") collected = make([]string, 0, 10) generic.TreeUnifyPath(ctx, originTree, fullPath, destinationTree, generic.NewUnifyOptions(destinationTree).SetUpsert(upsert)) sort.Strings(collected) assert.EqualValues(t, []string{"/O-A/O-B/O-C:O-C=content O-C => /D-A/D-B/D-C:D-C=content O-C", "/O-A/O-B:O-B=content O-B => /D-A/D-B:D-B=content O-B", "/O-A:O-A=content O-A => /D-A:D-A=content O-A"}, collected) assertTree(destinationTree, []string{"/D-A/D-B/D-C:D-C=content O-C", "/D-A/D-B:D-B=content O-B", "/D-A:D-A=content O-A"}) // // Add a node unrelated to the unification path // var unrelatedOriginPath path.Path { originTree.Apply(ctx, generic.NewPathFromString("/O-A/O-B"), generic.NewApplyOptions(func(ctx context.Context, parent, path path.Path, node generic.NodeInterface) { assert.EqualValues(t, parent.Length()+1, node.GetCurrentPath().Length()) unrelated := node.CreateChild(ctx) unrelated.Upsert(ctx) memory.SetContent(unrelated, "content "+unrelated.GetID().String()) unrelated.Upsert(ctx) unrelatedOriginPath = unrelated.GetCurrentPath() assert.EqualValues(t, "/O-A/O-B/O-N", (unrelatedOriginPath.PathString().Join())) })) } // // Replace the content of the last node // lastContent := "LAST" { lastPath := generic.NewPathFromString("/O-A/O-B/O-C") originTree.Apply(ctx, lastPath, generic.NewApplyOptions(func(ctx context.Context, parent, path path.Path, node generic.NodeInterface) { memory.SetContent(node, lastContent) })) collected = make([]string, 0, 10) generic.TreeUnifyPath(ctx, originTree, lastPath, destinationTree, generic.NewUnifyOptions(destinationTree).SetUpsert(upsert)) sort.Strings(collected) assert.EqualValues(t, []string{"/O-A/O-B/O-C:O-C=LAST => /D-A/D-B/D-C:D-C=LAST", "/O-A/O-B:O-B=content O-B => /D-A/D-B:D-B=content O-B", "/O-A:O-A=content O-A => /D-A:D-A=content O-A"}, collected) assertTree(destinationTree, []string{"/D-A/D-B/D-C:D-C=LAST", "/D-A/D-B:D-B=content O-B", "/D-A:D-A=content O-A"}) } // // Replace the content of the first node // firstContent := "FIRST" { firstPath := generic.NewPathFromString("/O-A") originTree.Apply(ctx, firstPath, generic.NewApplyOptions(func(ctx context.Context, parent, path path.Path, node generic.NodeInterface) { memory.SetContent(node, firstContent) })) collected = make([]string, 0, 10) generic.TreeUnifyPath(ctx, originTree, firstPath, destinationTree, generic.NewUnifyOptions(destinationTree).SetUpsert(upsert)) sort.Strings(collected) assert.EqualValues(t, []string{"/O-A:O-A=" + firstContent + " => /D-A:D-A=" + firstContent}, collected) assertTree(destinationTree, []string{"/D-A/D-B/D-C:D-C=LAST", "/D-A/D-B:D-B=content O-B", "/D-A:D-A=FIRST"}) } // // Replace the content of the second node // secondContent := "SECOND" { secondPath := generic.NewPathFromString("/O-A/O-B") originTree.Apply(ctx, secondPath, generic.NewApplyOptions(func(ctx context.Context, parent, path path.Path, node generic.NodeInterface) { memory.SetContent(node, secondContent) })) collected = make([]string, 0, 10) generic.TreeUnifyPath(ctx, originTree, secondPath, destinationTree, generic.NewUnifyOptions(destinationTree).SetUpsert(upsert)) assert.EqualValues(t, []string{"/O-A:O-A=" + firstContent + " => /D-A:D-A=" + firstContent, "/O-A/O-B:O-B=" + secondContent + " => /D-A/D-B:D-B=" + secondContent}, collected) sort.Strings(collected) assertTree(destinationTree, []string{"/D-A/D-B/D-C:D-C=LAST", "/D-A/D-B:D-B=SECOND", "/D-A:D-A=FIRST"}) } // // verify the node unrelated to the unification is still there // { var found bool originTree.Apply(ctx, unrelatedOriginPath, generic.NewApplyOptions(func(ctx context.Context, parent, path path.Path, node generic.NodeInterface) { found = true })) assert.True(t, found) } } func TestUnifyMirror(t *testing.T) { ctx := context.Background() // // build and populate a tree // originTree := NewMemoryTree(ctx, "O") log := originTree.GetLogger() testTreeBuild(t, originTree, 2) // // build an empty tree // destinationTree := NewMemoryTree(ctx, "D") upsert := func(ctx context.Context, origin generic.NodeInterface, originPath path.Path, destination generic.NodeInterface, destinationPath path.Path) { assert.NotNil(t, origin.GetDriver().(*memory.Driver)) assert.NotNil(t, destination.GetDriver().(*memory.Driver)) originPath = originPath.Append(origin) destinationPath = destinationPath.Append(destination) what := fmt.Sprintf("%s:%s => %s:%s", originPath, origin.GetSelf(), destinationPath, destination.GetSelf()) log.Trace("mirror upsert: %T => %T | %s", origin, destination, what) } delete := func(ctx context.Context, destination generic.NodeInterface, destinationPath path.Path) { assert.NotNil(t, destination.GetDriver().(*memory.Driver)) destinationPath = destinationPath.Append(destination) log.Trace("mirror delete: %T | %s:%s", destination, destinationPath, destination) } var sameTree func(origin, destination generic.NodeInterface) bool sameTree = func(origin, destination generic.NodeInterface) bool { what := origin.GetCurrentPath().String() + ":" + origin.GetSelf().String() + " => " + destination.GetCurrentPath().String() + ":" + destination.GetSelf().String() log.Trace("sameTree: %T => %T | %s", origin.GetSelf(), destination.GetSelf(), what) if origin.GetMappedID() != destination.GetID() { log.Trace("sameTree: different: %s != %s", origin.GetMappedID(), destination.GetID()) return false } originChildren := origin.GetChildren() destinationChildren := destination.GetChildren() if len(originChildren) != len(destinationChildren) { log.Trace("sameTree: different: length %v != %v", len(originChildren), len(destinationChildren)) return false } for _, originChild := range originChildren { destinationChild := destination.GetChild(originChild.GetMappedID()) if destinationChild == generic.NilNode { log.Trace("sameTree: different: %s not found", originChild.GetMappedID()) return false } if !sameTree(originChild, destinationChild) { return false } } return true } // // unify the originTree with the destinationTree // assert.False(t, sameTree(originTree.GetRoot(), destinationTree.GetRoot())) generic.TreeUnify(ctx, originTree, destinationTree, generic.NewUnifyOptions(destinationTree).SetUpsert(upsert).SetDelete(delete)) assert.True(t, sameTree(originTree.GetRoot(), destinationTree.GetRoot())) generic.TreeUnify(ctx, originTree, destinationTree, generic.NewUnifyOptions(destinationTree).SetUpsert(upsert).SetDelete(delete)) assert.True(t, sameTree(originTree.GetRoot(), destinationTree.GetRoot())) { addNode := func(tree generic.TreeInterface, pathString string) { tree.Apply(ctx, generic.NewPathFromString(pathString), generic.NewApplyOptions(func(ctx context.Context, parent, path path.Path, node generic.NodeInterface) { new := node.CreateChild(ctx) new.Upsert(ctx) memory.SetContent(new, "content "+new.GetID().String()) new.Upsert(ctx) log.Trace("add: %s", parent.ReadableString()) log.Trace("add: %s", node.GetCurrentPath().ReadableString()) log.Trace("add: %s", new.GetCurrentPath().ReadableString()) })) } for _, testCase := range []struct { existingPath string newPath string }{ { existingPath: "/O-A/O-B", newPath: "/D-A/D-B/D-N", }, { existingPath: "/O-A", newPath: "/D-A/D-O", }, { existingPath: "/O-A/O-J/O-K", newPath: "/D-A/D-J/D-K/D-P", }, } { t.Run("add"+testCase.newPath, func(t *testing.T) { destinationPath := generic.NewPathFromString(testCase.newPath) addNode(originTree, testCase.existingPath) assert.False(t, sameTree(originTree.GetRoot(), destinationTree.GetRoot())) assert.False(t, destinationTree.Exists(ctx, destinationPath), destinationPath.String()) generic.TreeUnify(ctx, originTree, destinationTree, generic.NewUnifyOptions(destinationTree).SetUpsert(upsert).SetDelete(delete)) assert.True(t, destinationTree.Exists(ctx, destinationPath), destinationPath.String()) assert.True(t, sameTree(originTree.GetRoot(), destinationTree.GetRoot())) }) } } { deleteNode := func(tree generic.TreeInterface, toDelete path.Path) { tree.Apply(ctx, toDelete, generic.NewApplyOptions(func(ctx context.Context, parent, path path.Path, node generic.NodeInterface) { node.Delete(ctx) })) } for _, testCase := range []struct { originPath string destinationPath string }{ { originPath: "/O-A/O-F", destinationPath: "/D-A/D-F", }, } { t.Run("delete"+testCase.originPath, func(t *testing.T) { originPath := generic.NewPathFromString(testCase.originPath) destinationPath := generic.NewPathFromString(testCase.destinationPath) assert.True(t, originTree.Exists(ctx, originPath)) assert.True(t, destinationTree.Exists(ctx, destinationPath), destinationPath.String()) deleteNode(originTree, originPath) assert.False(t, originTree.Exists(ctx, originPath)) generic.TreeUnify(ctx, originTree, destinationTree, generic.NewUnifyOptions(destinationTree).SetUpsert(upsert).SetDelete(delete)) assert.False(t, destinationTree.Exists(ctx, destinationPath), destinationPath.String()) }) } } } func TestNodeParallelApplyFound(t *testing.T) { ctx := context.Background() for _, testCase := range []struct { start string destination string expected []string expectedRemapped string }{ { start: "/O-A/O-B", destination: "O-C", expected: []string{"/O-A/O-B => /D-A/D-B", "/O-A/O-B/O-C => /D-A/D-B/D-C"}, expectedRemapped: "/D-A/D-B/D-C", }, { start: "/O-A/O-B/O-D", destination: ".", expected: []string{"/O-A/O-B/O-D => /D-A/D-B/D-D"}, expectedRemapped: "/D-A/D-B/D-D", }, { start: ".", destination: ".", expected: []string{" => "}, expectedRemapped: "", }, { start: "/O-A/O-B/O-C", destination: "../O-D", expected: []string{"/O-A/O-B => /D-A/D-B", "/O-A/O-B/O-D => /D-A/D-B/D-D"}, expectedRemapped: "/D-A/D-B/D-D", }, { start: "/", destination: ".", expected: []string{" => "}, expectedRemapped: "", }, } { t.Run(" "+testCase.start+" => "+testCase.destination, func(t *testing.T) { originTree := NewMemoryTree(ctx, "O") testTreeBuild(t, originTree, 2) destinationTree := NewMemoryTree(ctx, "D") // // Mirror two trees // generic.TreeMirror(ctx, originTree, destinationTree, generic.NewPathFromString("/"), generic.NewMirrorOptions()) // // collect all nodes traversed by the apply function along testCase.destination // collected := make([]string, 0, 10) collect := func(ctx context.Context, origin, destination generic.NodeInterface) { collected = append(collected, fmt.Sprintf("%s => %s", origin.GetCurrentPath().String(), destination.GetCurrentPath().String())) } // // get to testCase.start and from there run the apply function to reach testCase.destination // nodeApply := func(ctx context.Context, origin, destination generic.NodeInterface) { assert.True(t, generic.NodeParallelApply(ctx, origin, generic.NewPathFromString(testCase.destination), destination, generic.NewParallelApplyOptions(collect).SetWhere(generic.ApplyEachNode))) } assert.True(t, generic.TreeParallelApply(ctx, originTree, generic.NewPathFromString(testCase.start), destinationTree, generic.NewParallelApplyOptions(nodeApply))) assert.EqualValues(t, testCase.expected, collected) // // test TreePathRemap // remappedPath := generic.TreePathRemap(ctx, originTree, generic.NewPathFromString(filepath.Join(testCase.start, testCase.destination)), destinationTree) assert.EqualValues(t, testCase.expectedRemapped, remappedPath.String()) }) } } func TestNodeParallelApplyNoRemap(t *testing.T) { ctx := context.Background() for _, testCase := range []struct { noremap bool start string expected []string expectedRemapped string }{ { noremap: false, start: "/O-A/O-B", expected: []string{" => ", "/O-A => /D-A", "/O-A/O-B => /D-A/D-B"}, expectedRemapped: "/D-A/D-B", }, { noremap: true, start: "/O-A/O-B", expected: []string{" => ", "/O-A => /O-A", "/O-A/O-B => /O-A/O-B"}, expectedRemapped: "/O-A/O-B", }, } { t.Run(fmt.Sprintf("noremap=%v,start=%s", testCase.noremap, testCase.start), func(t *testing.T) { originName := "O" originTree := NewMemoryTree(ctx, originName) testTreeBuild(t, originTree, 2) var destinationName string if testCase.noremap { destinationName = originName } else { destinationName = "D" } destinationTree := NewMemoryTree(ctx, destinationName) // // Mirror two trees // generic.TreeMirror(ctx, originTree, destinationTree, generic.NewPathFromString("/"), generic.NewMirrorOptions()) // // collect all nodes traversed by the apply function along testCase.destination // collected := make([]string, 0, 10) collect := func(ctx context.Context, origin, destination generic.NodeInterface) { collected = append(collected, fmt.Sprintf("%s => %s", origin.GetCurrentPath(), destination.GetCurrentPath())) } // // get to testCase.start and from there run the apply function to reach testCase.destination // assert.True(t, generic.TreeParallelApply(ctx, originTree, generic.NewPathFromString(testCase.start), destinationTree, generic.NewParallelApplyOptions(collect).SetWhere(generic.ApplyEachNode).SetNoRemap(testCase.noremap))) assert.EqualValues(t, testCase.expected, collected) // // test TreePathRemap // remappedPath := generic.TreePathRemap(ctx, originTree, generic.NewPathFromString(testCase.start), destinationTree) assert.EqualValues(t, testCase.expectedRemapped, remappedPath.String()) }) } } func TestNodeParallelApplyNotFound(t *testing.T) { ctx := context.Background() for _, testCase := range []struct { start string destination string }{ { start: "/O-A", destination: "O-B/???", }, { start: "/O-A/O-B", destination: "???", }, { start: "/O-A/O-B", destination: "../???", }, } { t.Run(" "+testCase.start+" => "+testCase.destination, func(t *testing.T) { originTree := NewMemoryTree(ctx, "O") testTreeBuild(t, originTree, 2) destinationTree := NewMemoryTree(ctx, "D") // // Mirror two trees // generic.TreeMirror(ctx, originTree, destinationTree, generic.NewPathFromString("/"), generic.NewMirrorOptions()) // // get to testCase.start and from there run the apply function to reach testCase.destination // var called bool nodeApply := func(ctx context.Context, origin, destination generic.NodeInterface) { called = true found := generic.NodeParallelApply(ctx, origin, generic.NewPathFromString(testCase.destination), destination, generic.NewParallelApplyOptions(nil)) assert.False(t, found) } assert.True(t, generic.TreeParallelApply(ctx, originTree, generic.NewPathFromString(testCase.start), destinationTree, generic.NewParallelApplyOptions(nodeApply))) assert.True(t, called) }) } }