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) } } }