mirror of
https://github.com/kovidgoyal/kitty
synced 2026-06-06 01:05:48 +02:00
Merge branch 'copilot/add-tests-for-functions-8c2c32a' of https://github.com/kovidgoyal/kitty
This commit is contained in:
425
kittens/dnd/drop_test.go
Normal file
425
kittens/dnd/drop_test.go
Normal file
@@ -0,0 +1,425 @@
|
||||
// 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
|
||||
}
|
||||
@@ -350,6 +350,69 @@ func TestDupFile(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenameAt(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
os.WriteFile(filepath.Join(tmp, "src.txt"), []byte("hello"), 0644)
|
||||
d := openTestDir(t, tmp)
|
||||
|
||||
// Rename within the same directory.
|
||||
if err := RenameAt(d, "src.txt", d, "dst.txt"); err != nil {
|
||||
t.Fatalf("RenameAt same dir: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(tmp, "src.txt")); !os.IsNotExist(err) {
|
||||
t.Error("src.txt should no longer exist after rename")
|
||||
}
|
||||
if got := mustReadFile(t, filepath.Join(tmp, "dst.txt")); got != "hello" {
|
||||
t.Errorf("dst.txt after rename: got %q, want %q", got, "hello")
|
||||
}
|
||||
|
||||
// Rename to a different directory.
|
||||
sub := filepath.Join(tmp, "subdir")
|
||||
os.Mkdir(sub, 0755)
|
||||
subDir := openTestDir(t, sub)
|
||||
if err := RenameAt(d, "dst.txt", subDir, "moved.txt"); err != nil {
|
||||
t.Fatalf("RenameAt cross-dir: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(tmp, "dst.txt")); !os.IsNotExist(err) {
|
||||
t.Error("dst.txt should no longer exist after cross-dir rename")
|
||||
}
|
||||
if got := mustReadFile(t, filepath.Join(sub, "moved.txt")); got != "hello" {
|
||||
t.Errorf("moved.txt: got %q, want %q", got, "hello")
|
||||
}
|
||||
|
||||
// Renaming a non-existent file should return an error.
|
||||
if err := RenameAt(d, "nonexistent.txt", d, "out.txt"); err == nil {
|
||||
t.Error("expected error when renaming non-existent file")
|
||||
}
|
||||
|
||||
// Rename overwrites an existing destination file.
|
||||
os.WriteFile(filepath.Join(tmp, "a.txt"), []byte("aaa"), 0644)
|
||||
os.WriteFile(filepath.Join(tmp, "b.txt"), []byte("bbb"), 0644)
|
||||
if err := RenameAt(d, "a.txt", d, "b.txt"); err != nil {
|
||||
t.Fatalf("RenameAt overwrite: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(tmp, "a.txt")); !os.IsNotExist(err) {
|
||||
t.Error("a.txt should be gone after overwrite rename")
|
||||
}
|
||||
if got := mustReadFile(t, filepath.Join(tmp, "b.txt")); got != "aaa" {
|
||||
t.Errorf("b.txt after overwrite: got %q, want %q", got, "aaa")
|
||||
}
|
||||
|
||||
// Renaming a symlink should move the symlink itself, not the target.
|
||||
os.WriteFile(filepath.Join(tmp, "target.txt"), []byte("target"), 0644)
|
||||
os.Symlink("target.txt", filepath.Join(tmp, "link.txt"))
|
||||
if err := RenameAt(d, "link.txt", d, "renamedlink.txt"); err != nil {
|
||||
t.Fatalf("RenameAt symlink: %v", err)
|
||||
}
|
||||
linkTarget, err := os.Readlink(filepath.Join(tmp, "renamedlink.txt"))
|
||||
if err != nil {
|
||||
t.Fatalf("Readlink after rename: %v", err)
|
||||
}
|
||||
if linkTarget != "target.txt" {
|
||||
t.Errorf("renamed symlink target: got %q, want %q", linkTarget, "target.txt")
|
||||
}
|
||||
}
|
||||
|
||||
// --- RemoveChildren tests (pre-existing, kept for reference) ---
|
||||
|
||||
func TestRemoveChildren(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user