mirror of
https://github.com/kovidgoyal/kitty.git
synced 2026-05-13 16:37:27 +00:00
Agent-Logs-Url: https://github.com/kovidgoyal/kitty/sessions/2fffdd1f-2d67-4a28-b124-437c26794e25 Co-authored-by: kovidgoyal <1308621+kovidgoyal@users.noreply.github.com>
425 lines
13 KiB
Go
425 lines
13 KiB
Go
// License: GPLv3 Copyright: 2025, Kovid Goyal, <kovid at kovidgoyal.net>
|
|
|
|
package dnd
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"testing"
|
|
)
|
|
|
|
// openDir opens a directory for use as an *os.File, closing it on test cleanup.
|
|
func openDir(t *testing.T, path string) *os.File {
|
|
t.Helper()
|
|
d, err := os.Open(path)
|
|
if err != nil {
|
|
t.Fatalf("openDir %s: %v", path, err)
|
|
}
|
|
t.Cleanup(func() { d.Close() })
|
|
return d
|
|
}
|
|
|
|
// buildTree creates a directory tree described by a map of relative path ->
|
|
// content. Each entry creates a regular file with the given content (parent
|
|
// directories are created automatically). Symlink entries are expressed via
|
|
// the separate symlinks map: relative link name -> target string.
|
|
func buildTree(t *testing.T, base string, files map[string]string, symlinks map[string]string) {
|
|
t.Helper()
|
|
for rel, content := range files {
|
|
full := filepath.Join(base, rel)
|
|
if err := os.MkdirAll(filepath.Dir(full), 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(full, []byte(content), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
for name, target := range symlinks {
|
|
full := filepath.Join(base, name)
|
|
if err := os.MkdirAll(filepath.Dir(full), 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Symlink(target, full); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// sortedStrings returns a sorted copy of the slice.
|
|
func sortedStrings(s []string) []string {
|
|
out := append([]string(nil), s...)
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
// TestFindOverwrites_NoOverlap verifies that an empty result is returned when
|
|
// source and destination have no names in common.
|
|
func TestFindOverwrites_NoOverlap(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
src := filepath.Join(tmp, "src")
|
|
dst := filepath.Join(tmp, "dst")
|
|
os.MkdirAll(src, 0755)
|
|
os.MkdirAll(dst, 0755)
|
|
buildTree(t, src, map[string]string{"a.txt": "a", "b.txt": "b"}, nil)
|
|
buildTree(t, dst, map[string]string{"c.txt": "c"}, nil)
|
|
|
|
srcDir := openDir(t, src)
|
|
dstDir := openDir(t, dst)
|
|
|
|
got, err := find_overwrites(srcDir, dstDir)
|
|
if err != nil {
|
|
t.Fatalf("find_overwrites: %v", err)
|
|
}
|
|
if len(got) != 0 {
|
|
t.Errorf("expected no overwrites, got %v", got)
|
|
}
|
|
}
|
|
|
|
// TestFindOverwrites_FileOverlap verifies that files existing in both trees
|
|
// are reported.
|
|
func TestFindOverwrites_FileOverlap(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
src := filepath.Join(tmp, "src")
|
|
dst := filepath.Join(tmp, "dst")
|
|
os.MkdirAll(src, 0755)
|
|
os.MkdirAll(dst, 0755)
|
|
buildTree(t, src, map[string]string{"shared.txt": "src", "only_src.txt": "x"}, nil)
|
|
buildTree(t, dst, map[string]string{"shared.txt": "dst", "only_dst.txt": "y"}, nil)
|
|
|
|
srcDir := openDir(t, src)
|
|
dstDir := openDir(t, dst)
|
|
|
|
got, err := find_overwrites(srcDir, dstDir)
|
|
if err != nil {
|
|
t.Fatalf("find_overwrites: %v", err)
|
|
}
|
|
if len(got) != 1 || got[0] != "shared.txt" {
|
|
t.Errorf("expected [shared.txt], got %v", got)
|
|
}
|
|
}
|
|
|
|
// TestFindOverwrites_DirsNotReported verifies that matching *directories* in
|
|
// both trees are not reported as overwrites — only non-directory conflicts are.
|
|
func TestFindOverwrites_DirsNotReported(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
src := filepath.Join(tmp, "src")
|
|
dst := filepath.Join(tmp, "dst")
|
|
os.MkdirAll(src, 0755)
|
|
os.MkdirAll(dst, 0755)
|
|
// Both have "subdir/" — it should NOT appear in the overwrite list.
|
|
os.MkdirAll(filepath.Join(src, "subdir"), 0755)
|
|
os.MkdirAll(filepath.Join(dst, "subdir"), 0755)
|
|
buildTree(t, src, map[string]string{"subdir/file.txt": "s"}, nil)
|
|
buildTree(t, dst, map[string]string{"subdir/file.txt": "d"}, nil)
|
|
|
|
srcDir := openDir(t, src)
|
|
dstDir := openDir(t, dst)
|
|
|
|
got, err := find_overwrites(srcDir, dstDir)
|
|
if err != nil {
|
|
t.Fatalf("find_overwrites: %v", err)
|
|
}
|
|
// Only the nested file should be reported, not "subdir".
|
|
if len(got) != 1 || got[0] != "subdir/file.txt" {
|
|
t.Errorf("expected [subdir/file.txt], got %v", got)
|
|
}
|
|
}
|
|
|
|
// TestFindOverwrites_NestedTree tests a multi-level tree with a mix of
|
|
// regular files, nested directories, and symlinks.
|
|
func TestFindOverwrites_NestedTree(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
src := filepath.Join(tmp, "src")
|
|
dst := filepath.Join(tmp, "dst")
|
|
os.MkdirAll(src, 0755)
|
|
os.MkdirAll(dst, 0755)
|
|
|
|
// src layout:
|
|
// top.txt (overwrite — exists in dst too)
|
|
// only_src.txt
|
|
// sub/
|
|
// nested.txt (overwrite)
|
|
// deep/
|
|
// file.txt (overwrite)
|
|
// link -> target (symlink overwrite)
|
|
os.MkdirAll(filepath.Join(src, "sub", "deep"), 0755)
|
|
buildTree(t, src, map[string]string{
|
|
"top.txt": "src",
|
|
"only_src.txt": "x",
|
|
"sub/nested.txt": "src",
|
|
"sub/deep/file.txt": "src",
|
|
}, map[string]string{"link": "target"})
|
|
|
|
// dst layout:
|
|
// top.txt (shared file)
|
|
// only_dst.txt
|
|
// sub/
|
|
// nested.txt (shared file)
|
|
// deep/
|
|
// file.txt (shared file)
|
|
// link -> other (symlink — same name, different target)
|
|
os.MkdirAll(filepath.Join(dst, "sub", "deep"), 0755)
|
|
buildTree(t, dst, map[string]string{
|
|
"top.txt": "dst",
|
|
"only_dst.txt": "y",
|
|
"sub/nested.txt": "dst",
|
|
"sub/deep/file.txt": "dst",
|
|
}, map[string]string{"link": "other"})
|
|
|
|
srcDir := openDir(t, src)
|
|
dstDir := openDir(t, dst)
|
|
|
|
got, err := find_overwrites(srcDir, dstDir)
|
|
if err != nil {
|
|
t.Fatalf("find_overwrites: %v", err)
|
|
}
|
|
want := []string{"link", "sub/deep/file.txt", "sub/nested.txt", "top.txt"}
|
|
if got2 := sortedStrings(got); !equalStringSlices(got2, want) {
|
|
t.Errorf("find_overwrites: got %v, want %v", got2, want)
|
|
}
|
|
}
|
|
|
|
// TestFindOverwrites_SymlinksTransferredAsIs verifies that a symlink in the
|
|
// source with the same name as a symlink in the destination is reported as an
|
|
// overwrite regardless of the symlink targets, and that symlinks are not
|
|
// followed.
|
|
func TestFindOverwrites_SymlinksTransferredAsIs(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
src := filepath.Join(tmp, "src")
|
|
dst := filepath.Join(tmp, "dst")
|
|
os.MkdirAll(src, 0755)
|
|
os.MkdirAll(dst, 0755)
|
|
// Both have a symlink named "link" pointing to different targets.
|
|
os.Symlink("target_a", filepath.Join(src, "link"))
|
|
os.Symlink("target_b", filepath.Join(dst, "link"))
|
|
|
|
srcDir := openDir(t, src)
|
|
dstDir := openDir(t, dst)
|
|
|
|
got, err := find_overwrites(srcDir, dstDir)
|
|
if err != nil {
|
|
t.Fatalf("find_overwrites: %v", err)
|
|
}
|
|
if len(got) != 1 || got[0] != "link" {
|
|
t.Errorf("expected [link], got %v", got)
|
|
}
|
|
}
|
|
|
|
// TestFindOverwrites_DirVsFile reports the case where src has a directory and
|
|
// dst has a file with the same name (or vice versa).
|
|
func TestFindOverwrites_DirVsFile(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
src := filepath.Join(tmp, "src")
|
|
dst := filepath.Join(tmp, "dst")
|
|
os.MkdirAll(src, 0755)
|
|
os.MkdirAll(dst, 0755)
|
|
// src has a directory named "conflict"; dst has a regular file with the same name.
|
|
os.MkdirAll(filepath.Join(src, "conflict"), 0755)
|
|
os.WriteFile(filepath.Join(dst, "conflict"), []byte("file"), 0644)
|
|
|
|
srcDir := openDir(t, src)
|
|
dstDir := openDir(t, dst)
|
|
|
|
got, err := find_overwrites(srcDir, dstDir)
|
|
if err != nil {
|
|
t.Fatalf("find_overwrites: %v", err)
|
|
}
|
|
// A directory in src vs a file in dst should be reported.
|
|
if len(got) != 1 || got[0] != "conflict" {
|
|
t.Errorf("expected [conflict], got %v", got)
|
|
}
|
|
}
|
|
|
|
// --- rename_contents tests ---
|
|
|
|
// TestRenameContents_SimpleFiles verifies that plain files are moved from src
|
|
// to dst.
|
|
func TestRenameContents_SimpleFiles(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
src := filepath.Join(tmp, "src")
|
|
dst := filepath.Join(tmp, "dst")
|
|
os.MkdirAll(src, 0755)
|
|
os.MkdirAll(dst, 0755)
|
|
buildTree(t, src, map[string]string{"a.txt": "aaa", "b.txt": "bbb"}, nil)
|
|
|
|
srcDir := openDir(t, src)
|
|
dstDir := openDir(t, dst)
|
|
|
|
if err := rename_contents(srcDir, dstDir); err != nil {
|
|
t.Fatalf("rename_contents: %v", err)
|
|
}
|
|
|
|
for _, name := range []string{"a.txt", "b.txt"} {
|
|
if _, err := os.Stat(filepath.Join(src, name)); !os.IsNotExist(err) {
|
|
t.Errorf("%s should have been moved out of src", name)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(dst, name)); err != nil {
|
|
t.Errorf("%s should exist in dst: %v", name, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestRenameContents_MergesNestedDirs verifies that when both src and dst
|
|
// already have a subdirectory with the same name, their contents are merged
|
|
// recursively.
|
|
func TestRenameContents_MergesNestedDirs(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
src := filepath.Join(tmp, "src")
|
|
dst := filepath.Join(tmp, "dst")
|
|
os.MkdirAll(filepath.Join(src, "sub"), 0755)
|
|
os.MkdirAll(filepath.Join(dst, "sub"), 0755)
|
|
buildTree(t, src, map[string]string{
|
|
"sub/from_src.txt": "src",
|
|
}, nil)
|
|
buildTree(t, dst, map[string]string{
|
|
"sub/from_dst.txt": "dst",
|
|
}, nil)
|
|
|
|
srcDir := openDir(t, src)
|
|
dstDir := openDir(t, dst)
|
|
|
|
if err := rename_contents(srcDir, dstDir); err != nil {
|
|
t.Fatalf("rename_contents: %v", err)
|
|
}
|
|
|
|
// Both files must now live under dst/sub/.
|
|
for _, name := range []string{"from_src.txt", "from_dst.txt"} {
|
|
if _, err := os.Stat(filepath.Join(dst, "sub", name)); err != nil {
|
|
t.Errorf("dst/sub/%s missing: %v", name, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestRenameContents_NestedMultiLevel verifies correct merging across multiple
|
|
// nesting levels.
|
|
func TestRenameContents_NestedMultiLevel(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
src := filepath.Join(tmp, "src")
|
|
dst := filepath.Join(tmp, "dst")
|
|
|
|
// Build a 3-level nested source tree.
|
|
os.MkdirAll(filepath.Join(src, "a", "b"), 0755)
|
|
os.MkdirAll(filepath.Join(dst, "a", "b"), 0755)
|
|
buildTree(t, src, map[string]string{
|
|
"top.txt": "top",
|
|
"a/mid.txt": "mid",
|
|
"a/b/deep.txt": "deep",
|
|
}, nil)
|
|
buildTree(t, dst, map[string]string{
|
|
"a/existing.txt": "existing",
|
|
}, nil)
|
|
|
|
srcDir := openDir(t, src)
|
|
dstDir := openDir(t, dst)
|
|
|
|
if err := rename_contents(srcDir, dstDir); err != nil {
|
|
t.Fatalf("rename_contents: %v", err)
|
|
}
|
|
|
|
expected := []string{"top.txt", "a/mid.txt", "a/b/deep.txt", "a/existing.txt"}
|
|
for _, rel := range expected {
|
|
if _, err := os.Stat(filepath.Join(dst, rel)); err != nil {
|
|
t.Errorf("dst/%s missing: %v", rel, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestRenameContents_SymlinksMovedAsIs verifies that symlinks are moved as-is
|
|
// (not followed), preserving both the link and its original target string.
|
|
func TestRenameContents_SymlinksMovedAsIs(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
src := filepath.Join(tmp, "src")
|
|
dst := filepath.Join(tmp, "dst")
|
|
os.MkdirAll(src, 0755)
|
|
os.MkdirAll(dst, 0755)
|
|
// A symlink pointing to a non-existent target — if followed it would fail.
|
|
os.Symlink("does_not_exist", filepath.Join(src, "link"))
|
|
|
|
srcDir := openDir(t, src)
|
|
dstDir := openDir(t, dst)
|
|
|
|
if err := rename_contents(srcDir, dstDir); err != nil {
|
|
t.Fatalf("rename_contents: %v", err)
|
|
}
|
|
|
|
// Symlink must have arrived in dst.
|
|
target, err := os.Readlink(filepath.Join(dst, "link"))
|
|
if err != nil {
|
|
t.Fatalf("readlink dst/link: %v", err)
|
|
}
|
|
if target != "does_not_exist" {
|
|
t.Errorf("symlink target: got %q, want %q", target, "does_not_exist")
|
|
}
|
|
// Must no longer be in src.
|
|
if _, err := os.Lstat(filepath.Join(src, "link")); !os.IsNotExist(err) {
|
|
t.Error("link should have been moved out of src")
|
|
}
|
|
}
|
|
|
|
// TestRenameContents_SymlinksInSubdirMovedAsIs checks that symlinks inside a
|
|
// nested directory are also moved without following.
|
|
func TestRenameContents_SymlinksInSubdirMovedAsIs(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
src := filepath.Join(tmp, "src")
|
|
dst := filepath.Join(tmp, "dst")
|
|
os.MkdirAll(filepath.Join(src, "sub"), 0755)
|
|
os.MkdirAll(filepath.Join(dst, "sub"), 0755)
|
|
os.WriteFile(filepath.Join(src, "sub", "file.txt"), []byte("data"), 0644)
|
|
// Symlink with an absolute target to ensure it is not resolved.
|
|
os.Symlink("/absolute/path", filepath.Join(src, "sub", "abslink"))
|
|
|
|
srcDir := openDir(t, src)
|
|
dstDir := openDir(t, dst)
|
|
|
|
if err := rename_contents(srcDir, dstDir); err != nil {
|
|
t.Fatalf("rename_contents: %v", err)
|
|
}
|
|
|
|
target, err := os.Readlink(filepath.Join(dst, "sub", "abslink"))
|
|
if err != nil {
|
|
t.Fatalf("readlink dst/sub/abslink: %v", err)
|
|
}
|
|
if target != "/absolute/path" {
|
|
t.Errorf("symlink target: got %q, want %q", target, "/absolute/path")
|
|
}
|
|
}
|
|
|
|
// TestRenameContents_DirExistsInDest verifies that directories already
|
|
// existing in dest are not treated as overwrites — their contents are merged
|
|
// and no error is returned.
|
|
func TestRenameContents_DirExistsInDest(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
src := filepath.Join(tmp, "src")
|
|
dst := filepath.Join(tmp, "dst")
|
|
os.MkdirAll(filepath.Join(src, "shared"), 0755)
|
|
os.MkdirAll(filepath.Join(dst, "shared"), 0755)
|
|
buildTree(t, src, map[string]string{"shared/new.txt": "new"}, nil)
|
|
buildTree(t, dst, map[string]string{"shared/old.txt": "old"}, nil)
|
|
|
|
srcDir := openDir(t, src)
|
|
dstDir := openDir(t, dst)
|
|
|
|
if err := rename_contents(srcDir, dstDir); err != nil {
|
|
t.Fatalf("rename_contents should succeed when dir exists in dest: %v", err)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(dst, "shared", "new.txt")); err != nil {
|
|
t.Errorf("dst/shared/new.txt missing: %v", err)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(dst, "shared", "old.txt")); err != nil {
|
|
t.Errorf("dst/shared/old.txt missing: %v", err)
|
|
}
|
|
}
|
|
|
|
// equalStringSlices returns true when two sorted slices are equal.
|
|
func equalStringSlices(a, b []string) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
for i := range a {
|
|
if a[i] != b[i] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|