primeiro commit
This commit is contained in:
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user