421 lines
12 KiB
Go
421 lines
12 KiB
Go
package ops_test
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"aehenamer/internal/ops"
|
|
)
|
|
|
|
// apply builds the operation and applies it to a single name.
|
|
func apply(t *testing.T, cfg ops.Config, name string, index int) string {
|
|
t.Helper()
|
|
op, err := ops.New(cfg)
|
|
if err != nil {
|
|
t.Fatalf("ops.New(%+v): %v", cfg, err)
|
|
}
|
|
return op.Apply(name, index)
|
|
}
|
|
|
|
func TestReplace(t *testing.T) {
|
|
base := func() ops.Config {
|
|
cfg := ops.DefaultConfig(ops.KindReplace)
|
|
cfg.Target = "foo"
|
|
cfg.Replacement = "baz"
|
|
return cfg
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
cfg func(c *ops.Config)
|
|
in string
|
|
want string
|
|
}{
|
|
{name: "all occurrences", in: "foo bar foo.txt", want: "baz bar baz.txt"},
|
|
{
|
|
name: "limit from start",
|
|
cfg: func(c *ops.Config) { c.Limit = "1" },
|
|
in: "foo bar foo.txt", want: "baz bar foo.txt",
|
|
},
|
|
{
|
|
name: "limit from end",
|
|
cfg: func(c *ops.Config) { c.Limit = "1"; c.FromStart = false },
|
|
in: "foo bar foo.txt", want: "foo bar baz.txt",
|
|
},
|
|
{
|
|
name: "limit larger than matches",
|
|
cfg: func(c *ops.Config) { c.Limit = "9" },
|
|
in: "foo.txt", want: "baz.txt",
|
|
},
|
|
{name: "case insensitive by default", in: "FoO.txt", want: "baz.txt"},
|
|
{
|
|
name: "case sensitive",
|
|
cfg: func(c *ops.Config) { c.CaseSensitive = true },
|
|
in: "FoO.txt", want: "FoO.txt",
|
|
},
|
|
{
|
|
name: "extension preserved",
|
|
cfg: func(c *ops.Config) { c.Target = "txt"; c.Replacement = "md" },
|
|
in: "txt.txt", want: "md.txt",
|
|
},
|
|
{
|
|
name: "extension included when not preserved",
|
|
cfg: func(c *ops.Config) { c.Target = "txt"; c.Replacement = "md"; c.PreserveExt = false },
|
|
in: "txt.txt", want: "md.md",
|
|
},
|
|
{
|
|
name: "regex with capture group",
|
|
cfg: func(c *ops.Config) { c.Regex = true; c.Target = `(\d+)`; c.Replacement = "[$1]" },
|
|
in: "episode 12.mkv", want: "episode [12].mkv",
|
|
},
|
|
{
|
|
name: "regex is quoted in literal mode",
|
|
cfg: func(c *ops.Config) { c.Target = `a.c`; c.Replacement = "x" },
|
|
in: "abc a.c.txt", want: "abc x.txt",
|
|
},
|
|
{
|
|
name: "empty target is a no-op",
|
|
cfg: func(c *ops.Config) { c.Target = "" },
|
|
in: "foo.txt", want: "foo.txt",
|
|
},
|
|
{
|
|
name: "no match leaves the name alone",
|
|
cfg: func(c *ops.Config) { c.Target = "zzz" },
|
|
in: "foo.txt", want: "foo.txt",
|
|
},
|
|
{
|
|
name: "dotfile has no extension to preserve",
|
|
cfg: func(c *ops.Config) { c.Target = "bashrc"; c.Replacement = "zshrc" },
|
|
in: ".bashrc", want: ".zshrc",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
cfg := base()
|
|
if tt.cfg != nil {
|
|
tt.cfg(&cfg)
|
|
}
|
|
if got := apply(t, cfg, tt.in, 0); got != tt.want {
|
|
t.Errorf("Apply(%q) = %q, want %q", tt.in, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestReplaceWithEmptyReplacementRemovesMatches(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
cfg func(c *ops.Config)
|
|
in string
|
|
want string
|
|
}{
|
|
{
|
|
name: "literal text",
|
|
cfg: func(c *ops.Config) { c.Target = " - copy" },
|
|
in: "report - copy.pdf", want: "report.pdf",
|
|
},
|
|
{
|
|
name: "regular expression",
|
|
cfg: func(c *ops.Config) { c.Target = `\s*\(\d+\)`; c.Regex = true },
|
|
in: "photo (1).jpg", want: "photo.jpg",
|
|
},
|
|
{
|
|
name: "last match only",
|
|
cfg: func(c *ops.Config) { c.Target = "x"; c.Limit = "1"; c.FromStart = false },
|
|
in: "xaxbx.txt", want: "xaxb.txt",
|
|
},
|
|
{
|
|
name: "extension untouched",
|
|
cfg: func(c *ops.Config) { c.Target = "a" },
|
|
in: "banana.aac", want: "bnn.aac",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
cfg := ops.DefaultConfig(ops.KindReplace)
|
|
tt.cfg(&cfg)
|
|
if got := apply(t, cfg, tt.in, 0); got != tt.want {
|
|
t.Errorf("Apply(%q) = %q, want %q", tt.in, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestInsert(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
text string
|
|
index string
|
|
ext bool
|
|
in string
|
|
want string
|
|
}{
|
|
{name: "at the beginning", text: "2024-", index: "0", ext: true, in: "trip.jpg", want: "2024-trip.jpg"},
|
|
{name: "in the middle", text: "-", index: "2", ext: true, in: "abcd.jpg", want: "ab-cd.jpg"},
|
|
{name: "to-last", text: "-end", index: ops.ToLast, ext: true, in: "abcd.jpg", want: "abcd-end.jpg"},
|
|
{name: "to-last without preserving extension", text: "!", index: ops.ToLast, ext: false, in: "abcd.jpg", want: "abcd.jpg!"},
|
|
{name: "index beyond the name is clamped", text: "!", index: "99", ext: true, in: "ab.jpg", want: "ab!.jpg"},
|
|
{name: "negative index is clamped", text: "!", index: "-3", ext: true, in: "ab.jpg", want: "!ab.jpg"},
|
|
{name: "empty index defaults to the front", text: "!", index: "", ext: true, in: "ab.jpg", want: "!ab.jpg"},
|
|
{name: "counts characters not bytes", text: "-", index: "2", ext: true, in: "héllo.txt", want: "hé-llo.txt"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
cfg := ops.DefaultConfig(ops.KindInsert)
|
|
cfg.Text, cfg.Index, cfg.PreserveExt = tt.text, tt.index, tt.ext
|
|
if got := apply(t, cfg, tt.in, 0); got != tt.want {
|
|
t.Errorf("Apply(%q) = %q, want %q", tt.in, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIncrement(t *testing.T) {
|
|
cfg := ops.DefaultConfig(ops.KindIncrement)
|
|
cfg.Prefix = "_"
|
|
cfg.Start = "1"
|
|
cfg.Step = "2"
|
|
|
|
names := []string{"a.txt", "b.txt", "c.txt"}
|
|
op, err := ops.New(cfg)
|
|
if err != nil {
|
|
t.Fatalf("ops.New: %v", err)
|
|
}
|
|
got := ops.Preview(names, []ops.Operation{op})
|
|
want := []string{"a_1.txt", "b_3.txt", "c_5.txt"}
|
|
for i := range want {
|
|
if got[i] != want[i] {
|
|
t.Errorf("file %d = %q, want %q", i, got[i], want[i])
|
|
}
|
|
}
|
|
|
|
t.Run("defaults", func(t *testing.T) {
|
|
cfg := ops.DefaultConfig(ops.KindIncrement)
|
|
if got := apply(t, cfg, "a.txt", 3); got != "a3.txt" {
|
|
t.Errorf("got %q, want %q", got, "a3.txt")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestCase(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
kind ops.Kind
|
|
ext bool
|
|
in string
|
|
want string
|
|
}{
|
|
{name: "lower keeps extension", kind: ops.KindLower, ext: true, in: "MyFile.TXT", want: "myfile.TXT"},
|
|
{name: "lower whole name", kind: ops.KindLower, ext: false, in: "MyFile.TXT", want: "myfile.txt"},
|
|
{name: "upper keeps extension", kind: ops.KindUpper, ext: true, in: "MyFile.txt", want: "MYFILE.txt"},
|
|
{name: "upper whole name", kind: ops.KindUpper, ext: false, in: "MyFile.txt", want: "MYFILE.TXT"},
|
|
{name: "dotfile", kind: ops.KindUpper, ext: true, in: ".bashrc", want: ".BASHRC"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
cfg := ops.DefaultConfig(tt.kind)
|
|
cfg.PreserveExt = tt.ext
|
|
if got := apply(t, cfg, tt.in, 0); got != tt.want {
|
|
t.Errorf("Apply(%q) = %q, want %q", tt.in, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTruncate(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
first, last string
|
|
keepBetween bool
|
|
ext bool
|
|
in string
|
|
want string
|
|
}{
|
|
{name: "remove range", first: "0", last: "2", ext: true, in: "abcdef.txt", want: "def.txt"},
|
|
{name: "keep range", first: "0", last: "2", keepBetween: true, ext: true, in: "abcdef.txt", want: "abc.txt"},
|
|
{name: "remove to the end", first: "2", last: ops.ToLast, ext: true, in: "abcdef.txt", want: "ab.txt"},
|
|
{name: "keep to the end", first: "2", last: ops.ToLast, keepBetween: true, ext: true, in: "abcdef.txt", want: "cdef.txt"},
|
|
{name: "single character", first: "1", last: "1", ext: true, in: "abc.txt", want: "ac.txt"},
|
|
{name: "swapped indexes", first: "4", last: "1", ext: true, in: "abcdef.txt", want: "af.txt"},
|
|
{name: "indexes beyond the name are clamped", first: "0", last: "99", ext: true, in: "abc.txt", want: ".txt"},
|
|
{name: "extension included", first: "0", last: "0", ext: false, in: "abc.txt", want: "bc.txt"},
|
|
{name: "counts characters not bytes", first: "0", last: "1", ext: true, in: "héllo.txt", want: "llo.txt"},
|
|
{name: "dotfile is truncated as a whole", first: "0", last: "0", ext: true, in: ".txt", want: "txt"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
cfg := ops.DefaultConfig(ops.KindTruncate)
|
|
cfg.First, cfg.Last = tt.first, tt.last
|
|
cfg.KeepBetween, cfg.PreserveExt = tt.keepBetween, tt.ext
|
|
if got := apply(t, cfg, tt.in, 0); got != tt.want {
|
|
t.Errorf("Apply(%q) = %q, want %q", tt.in, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewRejectsBadInput(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
cfg ops.Config
|
|
want string
|
|
}{
|
|
{
|
|
name: "unknown kind",
|
|
cfg: ops.Config{Kind: "Frobnicate"},
|
|
want: "unknown operation kind",
|
|
},
|
|
{
|
|
name: "invalid regular expression",
|
|
cfg: ops.Config{Kind: ops.KindReplace, Target: "([a-z", Regex: true},
|
|
want: "invalid regular expression",
|
|
},
|
|
{
|
|
name: "limit is not a number",
|
|
cfg: ops.Config{Kind: ops.KindReplace, Target: "a", Limit: "many"},
|
|
want: "limit",
|
|
},
|
|
{
|
|
name: "negative limit",
|
|
cfg: ops.Config{Kind: ops.KindReplace, Target: "a", Limit: "-2"},
|
|
want: "must not be negative",
|
|
},
|
|
{
|
|
name: "insert index is not a number",
|
|
cfg: ops.Config{Kind: ops.KindInsert, Index: "somewhere"},
|
|
want: "insert index",
|
|
},
|
|
{
|
|
name: "start is not a number",
|
|
cfg: ops.Config{Kind: ops.KindIncrement, Start: "one"},
|
|
want: "number to start",
|
|
},
|
|
{
|
|
name: "step is not a number",
|
|
cfg: ops.Config{Kind: ops.KindIncrement, Step: "two"},
|
|
want: "incremental step",
|
|
},
|
|
{
|
|
name: "truncate index is not a number",
|
|
cfg: ops.Config{Kind: ops.KindTruncate, First: "x"},
|
|
want: "first character index",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
op, err := ops.New(tt.cfg)
|
|
if err == nil {
|
|
t.Fatalf("ops.New(%+v) = %v, want an error", tt.cfg, op)
|
|
}
|
|
if !strings.Contains(err.Error(), tt.want) {
|
|
t.Errorf("error %q does not mention %q", err, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPreviewAppliesOperationsInOrder(t *testing.T) {
|
|
remove := ops.DefaultConfig(ops.KindReplace)
|
|
remove.Target = "IMG_"
|
|
lower := ops.DefaultConfig(ops.KindUpper)
|
|
increment := ops.DefaultConfig(ops.KindIncrement)
|
|
increment.Prefix = "-"
|
|
increment.Start = "10"
|
|
|
|
list := make([]ops.Operation, 0, 3)
|
|
for _, cfg := range []ops.Config{remove, lower, increment} {
|
|
op, err := ops.New(cfg)
|
|
if err != nil {
|
|
t.Fatalf("ops.New(%v): %v", cfg.Kind, err)
|
|
}
|
|
list = append(list, op)
|
|
}
|
|
|
|
names := []string{"IMG_one.jpg", "IMG_two.jpg"}
|
|
got := ops.Preview(names, list)
|
|
want := []string{"ONE-10.jpg", "TWO-11.jpg"}
|
|
for i := range want {
|
|
if got[i] != want[i] {
|
|
t.Errorf("file %d = %q, want %q", i, got[i], want[i])
|
|
}
|
|
}
|
|
if names[0] != "IMG_one.jpg" {
|
|
t.Errorf("Preview modified its input: %q", names[0])
|
|
}
|
|
}
|
|
|
|
func TestPreviewWithoutOperations(t *testing.T) {
|
|
names := []string{"a.txt", "b.txt"}
|
|
got := ops.Preview(names, nil)
|
|
for i := range names {
|
|
if got[i] != names[i] {
|
|
t.Errorf("file %d = %q, want %q", i, got[i], names[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSummaryMentionsTheSettings(t *testing.T) {
|
|
cfg := ops.DefaultConfig(ops.KindReplace)
|
|
cfg.Target = "a"
|
|
cfg.Replacement = "b"
|
|
cfg.Limit = "2"
|
|
cfg.Regex = true
|
|
|
|
op, err := ops.New(cfg)
|
|
if err != nil {
|
|
t.Fatalf("ops.New: %v", err)
|
|
}
|
|
summary := op.Summary()
|
|
for _, want := range []string{`"a"`, `"b"`, "first 2", "regex", "keep extension"} {
|
|
if !strings.Contains(summary, want) {
|
|
t.Errorf("summary %q does not mention %q", summary, want)
|
|
}
|
|
}
|
|
if op.Config().Target != "a" {
|
|
t.Errorf("Config() lost the entered values: %+v", op.Config())
|
|
}
|
|
}
|
|
|
|
func TestDefaultConfig(t *testing.T) {
|
|
tests := []struct {
|
|
kind ops.Kind
|
|
want ops.Config
|
|
}{
|
|
{kind: ops.KindIncrement, want: ops.Config{Kind: ops.KindIncrement, Start: "0", Step: "1", FromStart: true, PreserveExt: true}},
|
|
{kind: ops.KindTruncate, want: ops.Config{Kind: ops.KindTruncate, First: "0", Last: "0", FromStart: true, PreserveExt: true}},
|
|
{kind: ops.KindInsert, want: ops.Config{Kind: ops.KindInsert, Index: "0", FromStart: true, PreserveExt: true}},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(string(tt.kind), func(t *testing.T) {
|
|
if got := ops.DefaultConfig(tt.kind); got != tt.want {
|
|
t.Errorf("DefaultConfig(%q) = %+v, want %+v", tt.kind, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestKindsContainsOnlySupportedMenuOperations(t *testing.T) {
|
|
want := []ops.Kind{
|
|
ops.KindReplace,
|
|
ops.KindInsert,
|
|
ops.KindIncrement,
|
|
ops.KindLower,
|
|
ops.KindUpper,
|
|
ops.KindTruncate,
|
|
}
|
|
if len(ops.Kinds) != len(want) {
|
|
t.Fatalf("Kinds = %v, want %v", ops.Kinds, want)
|
|
}
|
|
for i := range want {
|
|
if ops.Kinds[i] != want[i] {
|
|
t.Errorf("Kinds[%d] = %q, want %q", i, ops.Kinds[i], want[i])
|
|
}
|
|
}
|
|
}
|