package buffer import ( "errors" "os" "path/filepath" "testing" ) func TestOpenReadsTheFile(t *testing.T) { path := filepath.Join(t.TempDir(), "main.go") writeTestFile(t, path, "package main\n\nfunc main() {}\n") b, err := Open(path) if err != nil { t.Fatalf("Open() error = %v", err) } if got := b.LineCount(); got != 3 { t.Errorf("LineCount() = %d, want 3", got) } if got := b.Path(); got != path { t.Errorf("Path() = %q, want %q", got, path) } if b.Modified() { t.Error("a freshly opened buffer must not be modified") } if b.CanUndo() { t.Error("opening a file must not leave anything to undo") } } func TestOpenAMissingFileGivesAnEmptyBuffer(t *testing.T) { path := filepath.Join(t.TempDir(), "new.go") b, err := Open(path) if err != nil { t.Fatalf("Open() error = %v, want nil for a file that does not exist yet", err) } if got := b.LineCount(); got != 1 { t.Errorf("LineCount() = %d, want 1", got) } if got := b.Path(); got != path { t.Errorf("Path() = %q, want the requested path", got) } if b.Modified() { t.Error("an empty new buffer must not be modified") } } func TestOpenADirectoryFails(t *testing.T) { if _, err := Open(t.TempDir()); err == nil { t.Fatal("Open() error = nil, want a failure when the path is a directory") } } func TestSaveWritesTheTextBack(t *testing.T) { path := filepath.Join(t.TempDir(), "main.go") writeTestFile(t, path, "package main\n") b, err := Open(path) if err != nil { t.Fatalf("Open() error = %v", err) } b.MoveBufferEnd() b.Insert("\nfunc main() {}") if err := b.Save(); err != nil { t.Fatalf("Save() error = %v", err) } if b.Modified() { t.Error("Save must clear the modified flag") } want := "package main\nfunc main() {}\n" if got := readTestFile(t, path); got != want { t.Errorf("file content = %q, want %q", got, want) } } func TestSaveRoundTripsAnUnmodifiedFileByteForByte(t *testing.T) { tests := []struct { name string content string }{ {"unix endings", "a\nb\n"}, {"windows endings", "a\r\nb\r\n"}, {"no trailing newline", "a\nb"}, {"empty file", ""}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { path := filepath.Join(t.TempDir(), "f.txt") writeTestFile(t, path, tc.content) b, err := Open(path) if err != nil { t.Fatalf("Open() error = %v", err) } if err := b.Save(); err != nil { t.Fatalf("Save() error = %v", err) } if got := readTestFile(t, path); got != tc.content { t.Errorf("file content = %q, want %q", got, tc.content) } }) } } func TestSaveWithoutAPathReportsErrNoPath(t *testing.T) { b := NewFromString("orphan") err := b.Save() if !errors.Is(err, ErrNoPath) { t.Errorf("Save() error = %v, want ErrNoPath", err) } } func TestSaveAsAdoptsTheNewPath(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "copy.go") b := NewFromString("package main\n") if err := b.SaveAs(path); err != nil { t.Fatalf("SaveAs() error = %v", err) } if got := b.Path(); got != path { t.Errorf("Path() = %q, want %q", got, path) } if got := readTestFile(t, path); got != "package main\n" { t.Errorf("file content = %q", got) } if entries, _ := os.ReadDir(dir); len(entries) != 1 { t.Errorf("the directory holds %d entries, want 1 — the temporary file must be gone", len(entries)) } } func TestSaveKeepsTheExistingFilePermissions(t *testing.T) { path := filepath.Join(t.TempDir(), "script.go") writeTestFile(t, path, "package main\n") if err := os.Chmod(path, 0o640); err != nil { t.Fatalf("Chmod() error = %v", err) } b, err := Open(path) if err != nil { t.Fatalf("Open() error = %v", err) } if err := b.Save(); err != nil { t.Fatalf("Save() error = %v", err) } info, err := os.Stat(path) if err != nil { t.Fatalf("Stat() error = %v", err) } if got := info.Mode().Perm(); got != 0o640 { t.Errorf("permissions = %o, want 640 — the atomic rename must not tighten them", got) } } func TestSaveIntoAMissingDirectoryFails(t *testing.T) { b := NewFromString("x") err := b.SaveAs(filepath.Join(t.TempDir(), "nope", "f.go")) if err == nil { t.Fatal("SaveAs() error = nil, want a failure when the directory does not exist") } if b.Path() != "" { t.Errorf("Path() = %q, want it left alone after a failed save", b.Path()) } } func TestModeOfFallsBackForAMissingFile(t *testing.T) { if got := modeOf(filepath.Join(t.TempDir(), "absent")); got != defaultFileMode { t.Errorf("modeOf(missing) = %o, want %o", got, defaultFileMode) } } // writeTestFile creates a file with the given content, failing the test if it // cannot. func writeTestFile(t *testing.T, path, content string) { t.Helper() if err := os.WriteFile(path, []byte(content), 0o644); err != nil { t.Fatalf("writing %s: %v", path, err) } } // readTestFile returns the content of a file, failing the test if it cannot. func readTestFile(t *testing.T, path string) string { t.Helper() data, err := os.ReadFile(path) if err != nil { t.Fatalf("reading %s: %v", path, err) } return string(data) } func TestReloadPicksUpAChangeMadeOnDisk(t *testing.T) { // What a formatter run from the Go menu leaves behind. path := filepath.Join(t.TempDir(), "main.go") writeTestFile(t, path, "package main\n") b, err := Open(path) if err != nil { t.Fatalf("Open() error = %v", err) } writeTestFile(t, path, "package main\n\nfunc main() {}\n") changed, err := b.Reload() if err != nil { t.Fatalf("Reload() error = %v", err) } if !changed { t.Error("Reload() reported no change for a file that was rewritten") } if got := b.Text(); got != "package main\n\nfunc main() {}\n" { t.Errorf("the buffer holds %q", got) } if b.Modified() { t.Error("a reloaded buffer reports itself modified") } } func TestReloadReportsNoChangeWhenTheFileIsTheSame(t *testing.T) { // A caller uses this to tell a file something rewrote from one it did not. path := filepath.Join(t.TempDir(), "main.go") writeTestFile(t, path, "package main\n") b, _ := Open(path) changed, err := b.Reload() if err != nil { t.Fatalf("Reload() error = %v", err) } if changed { t.Error("Reload() reported a change for a file nobody touched") } } func TestReloadRefusesToThrowAwayUnsavedWork(t *testing.T) { path := filepath.Join(t.TempDir(), "main.go") writeTestFile(t, path, "package main\n") b, _ := Open(path) b.Insert("// mine") before := b.Text() writeTestFile(t, path, "something else\n") _, err := b.Reload() if !errors.Is(err, ErrModified) { t.Fatalf("Reload() error = %v, want ErrModified", err) } if got := b.Text(); got != before { t.Errorf("the buffer was reloaded anyway: %q", got) } } func TestReloadKeepsTheCursorWhereItWas(t *testing.T) { // A formatter moves lines about; putting the cursor back at the top would // lose the reader's place for no reason. path := filepath.Join(t.TempDir(), "main.go") writeTestFile(t, path, "one\ntwo\nthree\n") b, _ := Open(path) b.SetCursor(Position{Line: 1, Col: 2}) if _, err := b.Reload(); err != nil { t.Fatalf("Reload() error = %v", err) } writeTestFile(t, path, "one\nTWO!\nthree\n") if _, err := b.Reload(); err != nil { t.Fatalf("Reload() error = %v", err) } if got := b.Cursor(); got.Line != 1 || got.Col != 2 { t.Errorf("Cursor() = %+v, want line 1 column 2", got) } } func TestReloadClampsACursorPastTheEndOfTheNewFile(t *testing.T) { path := filepath.Join(t.TempDir(), "main.go") writeTestFile(t, path, "one\ntwo\nthree\nfour\n") b, _ := Open(path) b.SetCursor(Position{Line: 3, Col: 4}) writeTestFile(t, path, "one\n") if _, err := b.Reload(); err != nil { t.Fatalf("Reload() error = %v", err) } cursor := b.Cursor() if cursor.Line >= b.LineCount() { t.Errorf("Cursor() = %+v, past the end of a %d-line file", cursor, b.LineCount()) } } func TestReloadForgetsTheUndoHistory(t *testing.T) { // Undoing back past a reload would restore text the file no longer has. path := filepath.Join(t.TempDir(), "main.go") writeTestFile(t, path, "one\n") b, _ := Open(path) b.Insert("x") if err := b.Save(); err != nil { t.Fatalf("Save() error = %v", err) } writeTestFile(t, path, "reformatted\n") if _, err := b.Reload(); err != nil { t.Fatalf("Reload() error = %v", err) } b.Undo() if got := b.Text(); got != "reformatted\n" { t.Errorf("undo after a reload gave %q", got) } } func TestReloadWithNoPathSaysSo(t *testing.T) { if _, err := New().Reload(); !errors.Is(err, ErrNoPath) { t.Errorf("Reload() error = %v, want ErrNoPath", err) } } func TestReloadOfAFileThatHasGoneIsAnError(t *testing.T) { path := filepath.Join(t.TempDir(), "main.go") writeTestFile(t, path, "package main\n") b, _ := Open(path) if err := os.Remove(path); err != nil { t.Fatalf("removing the file: %v", err) } if _, err := b.Reload(); err == nil { t.Error("Reload() accepted a file that no longer exists") } }