228 lines
4.9 KiB
Go
228 lines
4.9 KiB
Go
package fileops
|
|
|
|
import (
|
|
"os"
|
|
"testing"
|
|
)
|
|
|
|
func writeTempFile(t *testing.T, content string) string {
|
|
t.Helper()
|
|
f, err := os.CreateTemp(t.TempDir(), "ts-test-*.txt")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := f.WriteString(content); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f.Close()
|
|
return f.Name()
|
|
}
|
|
|
|
func readTempFile(t *testing.T, path string) string {
|
|
t.Helper()
|
|
b, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return string(b)
|
|
}
|
|
|
|
func equalIntSlice(a, b []int) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
for i := range a {
|
|
if a[i] != b[i] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func TestReplaceInFile(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
content string
|
|
from, to string
|
|
dryRun bool
|
|
wantLines []int
|
|
wantContent string
|
|
}{
|
|
{
|
|
name: "single occurrence",
|
|
content: "foo bar\nbaz\n",
|
|
from: "foo",
|
|
to: "qux",
|
|
wantLines: []int{1},
|
|
wantContent: "qux bar\nbaz\n",
|
|
},
|
|
{
|
|
name: "multiple occurrences on same line",
|
|
content: "foo foo foo\n",
|
|
from: "foo",
|
|
to: "x",
|
|
wantLines: []int{1},
|
|
wantContent: "x x x\n",
|
|
},
|
|
{
|
|
name: "occurrences across multiple lines",
|
|
content: "keep\nfoo here\nalso foo\n",
|
|
from: "foo",
|
|
to: "bar",
|
|
wantLines: []int{2, 3},
|
|
wantContent: "keep\nbar here\nalso bar\n",
|
|
},
|
|
{
|
|
name: "no match leaves file unchanged",
|
|
content: "hello world\n",
|
|
from: "xyz",
|
|
to: "abc",
|
|
wantLines: nil,
|
|
wantContent: "hello world\n",
|
|
},
|
|
{
|
|
name: "dry run reports lines but does not write",
|
|
content: "foo bar\n",
|
|
from: "foo",
|
|
to: "baz",
|
|
dryRun: true,
|
|
wantLines: []int{1},
|
|
wantContent: "foo bar\n",
|
|
},
|
|
{
|
|
name: "empty file",
|
|
content: "",
|
|
from: "foo",
|
|
to: "bar",
|
|
wantLines: nil,
|
|
wantContent: "",
|
|
},
|
|
{
|
|
name: "replacement on last line without trailing newline",
|
|
content: "line1\nfoo",
|
|
from: "foo",
|
|
to: "bar",
|
|
wantLines: []int{2},
|
|
wantContent: "line1\nbar\n",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
path := writeTempFile(t, tt.content)
|
|
res, err := ReplaceInFile(path, tt.from, tt.to, tt.dryRun)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if !equalIntSlice(res.ChangedLines, tt.wantLines) {
|
|
t.Errorf("ChangedLines = %v, want %v", res.ChangedLines, tt.wantLines)
|
|
}
|
|
got := readTempFile(t, path)
|
|
if got != tt.wantContent {
|
|
t.Errorf("file content = %q, want %q", got, tt.wantContent)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestReplaceInFile_MissingFile(t *testing.T) {
|
|
_, err := ReplaceInFile("/no/such/file.txt", "x", "y", false)
|
|
if err == nil {
|
|
t.Error("expected error for missing file, got nil")
|
|
}
|
|
}
|
|
|
|
func TestReplaceInFileRegex(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
content string
|
|
pattern string
|
|
repl string
|
|
dryRun bool
|
|
wantLines []int
|
|
wantContent string
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "basic regex replacement",
|
|
content: "foo123\nbar\nfoo456\n",
|
|
pattern: `foo\d+`,
|
|
repl: "replaced",
|
|
wantLines: []int{1, 3},
|
|
wantContent: "replaced\nbar\nreplaced\n",
|
|
},
|
|
{
|
|
name: "capture group substitution",
|
|
content: "hello world\n",
|
|
pattern: `(hello) (world)`,
|
|
repl: "$2 $1",
|
|
wantLines: []int{1},
|
|
wantContent: "world hello\n",
|
|
},
|
|
{
|
|
name: "no match",
|
|
content: "no match here\n",
|
|
pattern: `\d+`,
|
|
repl: "NUM",
|
|
wantLines: nil,
|
|
wantContent: "no match here\n",
|
|
},
|
|
{
|
|
name: "invalid regex returns error",
|
|
content: "foo\n",
|
|
pattern: `[invalid`,
|
|
repl: "x",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "dry run with regex",
|
|
content: "abc123\n",
|
|
pattern: `\d+`,
|
|
repl: "NUM",
|
|
dryRun: true,
|
|
wantLines: []int{1},
|
|
wantContent: "abc123\n",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
path := writeTempFile(t, tt.content)
|
|
res, err := ReplaceInFileRegex(path, tt.pattern, tt.repl, tt.dryRun)
|
|
if tt.wantErr {
|
|
if err == nil {
|
|
t.Error("expected error, got nil")
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if !equalIntSlice(res.ChangedLines, tt.wantLines) {
|
|
t.Errorf("ChangedLines = %v, want %v", res.ChangedLines, tt.wantLines)
|
|
}
|
|
got := readTempFile(t, path)
|
|
if got != tt.wantContent {
|
|
t.Errorf("file content = %q, want %q", got, tt.wantContent)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFormatLineNums(t *testing.T) {
|
|
tests := []struct {
|
|
nums []int
|
|
want string
|
|
}{
|
|
{[]int{1}, "1"},
|
|
{[]int{1, 2, 3}, "1, 2, 3"},
|
|
{[]int{10, 47}, "10, 47"},
|
|
}
|
|
for _, tt := range tests {
|
|
got := formatLineNums(tt.nums)
|
|
if got != tt.want {
|
|
t.Errorf("formatLineNums(%v) = %q, want %q", tt.nums, got, tt.want)
|
|
}
|
|
}
|
|
}
|