primeiro commit

This commit is contained in:
2026-08-31 15:08:33 -03:00
commit bf46c56b36
13 changed files with 2947 additions and 0 deletions
+299
View File
@@ -0,0 +1,299 @@
package files_test
import (
"context"
"errors"
"os"
"path/filepath"
"slices"
"strings"
"testing"
"aehenamer/internal/files"
)
// makeDir creates a temporary directory containing the given files.
func makeDir(t *testing.T, names ...string) string {
t.Helper()
dir := t.TempDir()
for _, name := range names {
if err := os.WriteFile(filepath.Join(dir, name), []byte(name), 0o600); err != nil {
t.Fatalf("creating %s: %v", name, err)
}
}
return dir
}
// namesOn returns the sorted file names currently in dir.
func namesOn(t *testing.T, dir string) []string {
t.Helper()
got, err := files.List(context.Background(), dir)
if err != nil {
t.Fatalf("listing %s: %v", dir, err)
}
return got
}
func TestList(t *testing.T) {
dir := makeDir(t, "b.txt", "a.txt", ".hidden")
if err := os.Mkdir(filepath.Join(dir, "sub"), 0o750); err != nil {
t.Fatalf("creating subdirectory: %v", err)
}
if err := os.WriteFile(filepath.Join(dir, "sub", "deep.txt"), nil, 0o600); err != nil {
t.Fatalf("creating nested file: %v", err)
}
got := namesOn(t, dir)
want := []string{".hidden", "a.txt", "b.txt"}
if !slices.Equal(got, want) {
t.Errorf("List() = %v, want %v (directories and nested files must be skipped)", got, want)
}
}
func TestListErrors(t *testing.T) {
t.Run("missing directory", func(t *testing.T) {
_, err := files.List(context.Background(), filepath.Join(t.TempDir(), "nope"))
if err == nil {
t.Fatal("List() of a missing directory returned no error")
}
if !errors.Is(err, os.ErrNotExist) {
t.Errorf("error %v does not wrap os.ErrNotExist", err)
}
})
t.Run("cancelled context", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
if _, err := files.List(ctx, t.TempDir()); !errors.Is(err, context.Canceled) {
t.Errorf("List() = %v, want context.Canceled", err)
}
})
}
func TestConflicts(t *testing.T) {
tests := []struct {
name string
onDisk []string
directories []string
entries []files.Entry
want map[int]string
}{
{
name: "clean plan",
onDisk: []string{"a.txt", "b.txt"},
entries: []files.Entry{
{Original: "a.txt", Proposed: "1.txt"},
{Original: "b.txt", Proposed: "2.txt"},
},
want: map[int]string{},
},
{
name: "swapping names is allowed",
onDisk: []string{"a.txt", "b.txt"},
entries: []files.Entry{
{Original: "a.txt", Proposed: "b.txt"},
{Original: "b.txt", Proposed: "a.txt"},
},
want: map[int]string{},
},
{
name: "duplicate targets",
onDisk: []string{"a.txt", "b.txt"},
entries: []files.Entry{
{Original: "a.txt", Proposed: "same.txt"},
{Original: "b.txt", Proposed: "same.txt"},
},
want: map[int]string{0: "several files would get this name", 1: "several files would get this name"},
},
{
name: "empty name",
onDisk: []string{"a.txt"},
entries: []files.Entry{
{Original: "a.txt", Proposed: " "},
},
want: map[int]string{0: "new name is empty"},
},
{
name: "path separator",
onDisk: []string{"a.txt"},
entries: []files.Entry{
{Original: "a.txt", Proposed: "sub/a.txt"},
},
want: map[int]string{0: "new name contains a path separator"},
},
{
name: "directory reference",
onDisk: []string{"a.txt"},
entries: []files.Entry{
{Original: "a.txt", Proposed: ".."},
},
want: map[int]string{0: "new name is a directory reference"},
},
{
name: "would overwrite a file that is not part of the plan",
onDisk: []string{"a.txt", "keep.txt"},
entries: []files.Entry{
{Original: "a.txt", Proposed: "keep.txt"},
},
want: map[int]string{0: "a filesystem entry with this name already exists"},
},
{
name: "would collide with an existing directory",
onDisk: []string{"a.txt"},
directories: []string{"archive"},
entries: []files.Entry{
{Original: "a.txt", Proposed: "archive"},
},
want: map[int]string{0: "a filesystem entry with this name already exists"},
},
{
name: "unchanged names never conflict with themselves",
onDisk: []string{"a.txt"},
entries: []files.Entry{
{Original: "a.txt", Proposed: "a.txt"},
},
want: map[int]string{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dir := makeDir(t, tt.onDisk...)
for _, name := range tt.directories {
if err := os.Mkdir(filepath.Join(dir, name), 0o750); err != nil {
t.Fatalf("creating directory %s: %v", name, err)
}
}
got := files.Conflicts(context.Background(), dir, tt.entries)
if len(got) != len(tt.want) {
t.Fatalf("Conflicts() = %v, want %v", got, tt.want)
}
for index, reason := range tt.want {
if got[index] != reason {
t.Errorf("entry %d: got %q, want %q", index, got[index], reason)
}
}
})
}
}
func TestRename(t *testing.T) {
t.Run("renames and skips unchanged files", func(t *testing.T) {
dir := makeDir(t, "a.txt", "b.txt")
renamed, err := files.Rename(context.Background(), dir, []files.Entry{
{Original: "a.txt", Proposed: "1.txt"},
{Original: "b.txt", Proposed: "b.txt"},
})
if err != nil {
t.Fatalf("Rename() error: %v", err)
}
if renamed != 1 {
t.Errorf("Rename() = %d, want 1", renamed)
}
if got, want := namesOn(t, dir), []string{"1.txt", "b.txt"}; !slices.Equal(got, want) {
t.Errorf("directory holds %v, want %v", got, want)
}
})
t.Run("swaps names", func(t *testing.T) {
dir := makeDir(t, "a.txt", "b.txt")
if _, err := files.Rename(context.Background(), dir, []files.Entry{
{Original: "a.txt", Proposed: "b.txt"},
{Original: "b.txt", Proposed: "a.txt"},
}); err != nil {
t.Fatalf("Rename() error: %v", err)
}
content, err := os.ReadFile(filepath.Join(dir, "a.txt"))
if err != nil {
t.Fatalf("reading a.txt: %v", err)
}
if string(content) != "b.txt" {
t.Errorf("a.txt holds %q, want the contents of the old b.txt", content)
}
})
t.Run("rotates names", func(t *testing.T) {
dir := makeDir(t, "a.txt", "b.txt", "c.txt")
if _, err := files.Rename(context.Background(), dir, []files.Entry{
{Original: "a.txt", Proposed: "b.txt"},
{Original: "b.txt", Proposed: "c.txt"},
{Original: "c.txt", Proposed: "a.txt"},
}); err != nil {
t.Fatalf("Rename() error: %v", err)
}
for name, want := range map[string]string{"b.txt": "a.txt", "c.txt": "b.txt", "a.txt": "c.txt"} {
content, err := os.ReadFile(filepath.Join(dir, name))
if err != nil {
t.Fatalf("reading %s: %v", name, err)
}
if string(content) != want {
t.Errorf("%s holds %q, want %q", name, content, want)
}
}
})
t.Run("refuses a conflicting plan", func(t *testing.T) {
dir := makeDir(t, "a.txt", "b.txt")
_, err := files.Rename(context.Background(), dir, []files.Entry{
{Original: "a.txt", Proposed: "same.txt"},
{Original: "b.txt", Proposed: "same.txt"},
})
if err == nil {
t.Fatal("Rename() accepted a conflicting plan")
}
if !strings.Contains(err.Error(), "a.txt") {
t.Errorf("error %v does not name the offending file", err)
}
if got, want := namesOn(t, dir), []string{"a.txt", "b.txt"}; !slices.Equal(got, want) {
t.Errorf("directory holds %v, want the untouched %v", got, want)
}
})
t.Run("rolls back when a file disappears", func(t *testing.T) {
dir := makeDir(t, "a.txt", "b.txt")
if err := os.Remove(filepath.Join(dir, "b.txt")); err != nil {
t.Fatalf("removing b.txt: %v", err)
}
_, err := files.Rename(context.Background(), dir, []files.Entry{
{Original: "a.txt", Proposed: "1.txt"},
{Original: "b.txt", Proposed: "2.txt"},
})
if err == nil {
t.Fatal("Rename() succeeded although a file was missing")
}
if got, want := namesOn(t, dir), []string{"a.txt"}; !slices.Equal(got, want) {
t.Errorf("directory holds %v, want the rolled back %v", got, want)
}
})
t.Run("rolls back when the final pass fails", func(t *testing.T) {
dir := makeDir(t, "a.txt", "b.txt")
tooLong := strings.Repeat("x", 256)
renamed, err := files.Rename(context.Background(), dir, []files.Entry{
{Original: "a.txt", Proposed: "1.txt"},
{Original: "b.txt", Proposed: tooLong},
})
if err == nil {
t.Fatal("Rename() succeeded with an overlong destination name")
}
if renamed != 1 {
t.Errorf("Rename() = %d completed renames before rollback, want 1", renamed)
}
if got, want := namesOn(t, dir), []string{"a.txt", "b.txt"}; !slices.Equal(got, want) {
t.Errorf("directory holds files %v, want the rolled back %v", got, want)
}
})
t.Run("cancelled context stops before touching anything", func(t *testing.T) {
dir := makeDir(t, "a.txt")
ctx, cancel := context.WithCancel(context.Background())
cancel()
if _, err := files.Rename(ctx, dir, []files.Entry{{Original: "a.txt", Proposed: "1.txt"}}); !errors.Is(err, context.Canceled) {
t.Errorf("Rename() = %v, want context.Canceled", err)
}
if got, want := namesOn(t, dir), []string{"a.txt"}; !slices.Equal(got, want) {
t.Errorf("directory holds %v, want %v", got, want)
}
})
}