From a149ce347b70b18b760d6db8e0c487757500fe6d Mon Sep 17 00:00:00 2001 From: onyx-and-iris Date: Tue, 7 Apr 2026 01:22:56 +0100 Subject: [PATCH] simplify del/reset commands by using readWriteTruncater interface. remove the type assertions update the tests: the tests now check output as well as file contents separately. --- cmd/add_test.go | 47 +++++++++++++++++++++++------------------ cmd/del.go | 34 ++++++++--------------------- cmd/del_test.go | 27 ++++++++++++----------- cmd/reset.go | 26 ++++------------------- cmd/reset_test.go | 16 ++++++-------- cmd/testhelpers_test.go | 18 ++++++++++++++++ cmd/util.go | 7 ++++++ 7 files changed, 86 insertions(+), 89 deletions(-) create mode 100644 cmd/testhelpers_test.go diff --git a/cmd/add_test.go b/cmd/add_test.go index e205e2c..37753bd 100644 --- a/cmd/add_test.go +++ b/cmd/add_test.go @@ -7,43 +7,50 @@ import ( func TestRunAddCommand(t *testing.T) { tests := []struct { - name string - existing string - args []string - expectedOutput string + name string + existing string + args []string + expectedOut string + expectedContent string }{ { - name: "Add new patterns", - existing: "", - args: []string{"*.log", "temp/"}, - expectedOutput: "Added pattern '*.log' to the exclude file.\nAdded pattern 'temp/' to the exclude file.\n", + name: "Add new patterns", + existing: "", + args: []string{"*.log", "temp/"}, + expectedOut: "Added pattern '*.log' to the exclude file.\nAdded pattern 'temp/' to the exclude file.\n", + expectedContent: "*.log\ntemp/\n", }, { - name: "Add duplicate patterns", - existing: "*.log\ntemp/\n", - args: []string{"*.log", "temp/"}, - expectedOutput: "Pattern '*.log' already exists in the exclude file. Skipping.\nPattern 'temp/' already exists in the exclude file. Skipping.\n", + name: "Add duplicate patterns", + existing: "*.log\ntemp/\n", + args: []string{"*.log", "temp/"}, + expectedOut: "Pattern '*.log' already exists in the exclude file. Skipping.\nPattern 'temp/' already exists in the exclude file. Skipping.\n", + expectedContent: "", }, { - name: "Add mix of new and duplicate patterns", - existing: "*.log\n", - args: []string{"*.log", "temp/"}, - expectedOutput: "Pattern '*.log' already exists in the exclude file. Skipping.\nAdded pattern 'temp/' to the exclude file.\n", + name: "Add mix of new and duplicate patterns", + existing: "*.log\n", + args: []string{"*.log", "temp/"}, + expectedOut: "Pattern '*.log' already exists in the exclude file. Skipping.\nAdded pattern 'temp/' to the exclude file.\n", + expectedContent: "temp/\n", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - var buf bytes.Buffer + var out bytes.Buffer f := bytes.NewBufferString(tt.existing) - err := runAddCommand(&buf, f, tt.args) + err := runAddCommand(&out, f, tt.args) if err != nil { t.Fatalf("runAddCommand returned an error: %v", err) } - if buf.String() != tt.expectedOutput { - t.Errorf("Expected output:\n%s\nGot:\n%s", tt.expectedOutput, buf.String()) + if out.String() != tt.expectedOut { + t.Errorf("Expected output:\n%s\nGot:\n%s", tt.expectedOut, out.String()) + } + if f.String() != tt.expectedContent { + t.Errorf("Expected file content:\n%s\nGot:\n%s", tt.expectedContent, f.String()) } }) } diff --git a/cmd/del.go b/cmd/del.go index a33a57d..7290787 100644 --- a/cmd/del.go +++ b/cmd/del.go @@ -32,13 +32,8 @@ func init() { } // runDelCommand deletes the specified pattern from the exclude file and writes the updated content back -// It handles both file and in-memory buffer cases for testing -func runDelCommand(out io.Writer, f any, pattern string) error { - r, ok := f.(io.Reader) - if !ok { - return fmt.Errorf("provided file does not support Reader") - } - existingPatterns, err := readExistingPatterns(r) +func runDelCommand(out io.Writer, f readWriteTruncater, pattern string) error { + existingPatterns, err := readExistingPatterns(f) if err != nil { return fmt.Errorf("error reading existing patterns: %v", err) } @@ -55,29 +50,18 @@ func runDelCommand(out io.Writer, f any, pattern string) error { } } - var w io.Writer - if t, ok := f.(truncater); ok { - if err := t.Truncate(0); err != nil { - return fmt.Errorf("error truncating exclude file: %w", err) - } - if s, ok := f.(io.Seeker); ok { - if _, err := s.Seek(0, 0); err != nil { - return fmt.Errorf("error seeking to the beginning of exclude file: %w", err) - } - } - w, _ = f.(io.Writer) - } else if buf, ok := f.(interface{ Reset() }); ok { - buf.Reset() - w, _ = f.(io.Writer) - } else { - return fmt.Errorf("provided file does not support writing") + if err := f.Truncate(0); err != nil { + return fmt.Errorf("error truncating exclude file: %w", err) + } + if _, err := f.Seek(0, 0); err != nil { + return fmt.Errorf("error seeking to the beginning of exclude file: %w", err) } - if err := writeDefaultExcludeContent(w); err != nil { + if err := writeDefaultExcludeContent(f); err != nil { return fmt.Errorf("error writing default exclude content: %w", err) } for _, p := range updatedPatterns { - if _, err := fmt.Fprintln(w, p); err != nil { + if _, err := fmt.Fprintln(f, p); err != nil { return fmt.Errorf("error writing updated patterns to exclude file: %v", err) } } diff --git a/cmd/del_test.go b/cmd/del_test.go index 8724376..030a3fd 100644 --- a/cmd/del_test.go +++ b/cmd/del_test.go @@ -10,40 +10,43 @@ func TestRunDelCommand(t *testing.T) { name string initialContent string patternToDelete string - expectedOutput string + expectedOut string expectedContent string }{ { name: "Delete existing pattern", initialContent: defaultExcludeFileContent + "node_modules\n.DS_Store\n", patternToDelete: "node_modules", - expectedOutput: defaultExcludeFileContent + ".DS_Store\n" + "Deleted pattern 'node_modules' from the exclude file.\n", + expectedOut: "Deleted pattern 'node_modules' from the exclude file.\n", + expectedContent: defaultExcludeFileContent + ".DS_Store\n", }, { name: "Delete non-existing pattern", initialContent: defaultExcludeFileContent + "node_modules\n.DS_Store\n", patternToDelete: "dist", - expectedOutput: "Pattern 'dist' not found in the exclude file. Nothing to delete.\n", + expectedOut: "Pattern 'dist' not found in the exclude file. Nothing to delete.\n", + expectedContent: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - var buf bytes.Buffer - buf.WriteString(tt.initialContent) + var out bytes.Buffer + var f seekBuffer + f.WriteString(tt.initialContent) - err := runDelCommand(&buf, &buf, tt.patternToDelete) + err := runDelCommand(&out, &f, tt.patternToDelete) if err != nil { t.Fatalf("runDelCommand returned an error: %v", err) } - if buf.String() != tt.expectedOutput { - t.Errorf( - "Expected output and content:\n%s\nGot:\n%s", - tt.expectedOutput, - buf.String(), - ) + if out.String() != tt.expectedOut { + t.Errorf("Expected output:\n%s\nGot:\n%s", tt.expectedOut, out.String()) + } + if f.String() != tt.expectedContent { + t.Errorf("Expected file content:\n%s\nGot:\n%s", tt.expectedContent, f.String()) } }) } } + diff --git a/cmd/reset.go b/cmd/reset.go index 19ff89d..a01e17b 100644 --- a/cmd/reset.go +++ b/cmd/reset.go @@ -27,33 +27,15 @@ func init() { RootCmd.AddCommand(resetCmd) } -// Truncate and seek to beginning -type truncater interface{ Truncate(size int64) error } - // resetAndWriteExcludeFile truncates and resets the file, then writes the default content -func resetAndWriteExcludeFile(out io.Writer, f any) error { - // Try to assert to io.ReadWriteSeeker for file operations - rws, ok := f.(io.ReadWriteSeeker) - if !ok { - // If not a file, try as io.Writer (for test buffers) - if w, ok := f.(io.Writer); ok { - return writeDefaultExcludeContent(w) - } - return fmt.Errorf("provided file does not support ReadWriteSeeker or Writer") - } - - t, ok := f.(truncater) - if !ok { - return fmt.Errorf("provided file does not support Truncate") - } - if err := t.Truncate(0); err != nil { +func resetAndWriteExcludeFile(out io.Writer, f readWriteTruncater) error { + if err := f.Truncate(0); err != nil { return fmt.Errorf("error truncating exclude file: %w", err) } - if _, err := rws.Seek(0, 0); err != nil { + if _, err := f.Seek(0, 0); err != nil { return fmt.Errorf("error seeking to the beginning of exclude file: %w", err) } - err := writeDefaultExcludeContent(rws) - if err != nil { + if err := writeDefaultExcludeContent(f); err != nil { return fmt.Errorf("error writing default exclude content: %w", err) } fmt.Fprintf(out, "Exclude file reset successfully.\n") diff --git a/cmd/reset_test.go b/cmd/reset_test.go index 52897cc..b146691 100644 --- a/cmd/reset_test.go +++ b/cmd/reset_test.go @@ -2,27 +2,23 @@ package cmd import ( "bytes" - "io" "testing" ) func TestRunResetCommand(t *testing.T) { - var buf bytes.Buffer + var out bytes.Buffer + var f seekBuffer - if err := resetAndWriteExcludeFile(&buf, &buf); err != nil { + if err := resetAndWriteExcludeFile(&out, &f); err != nil { t.Fatalf("resetAndWriteExcludeFile failed: %v", err) } - resetContent, err := io.ReadAll(&buf) - if err != nil { - t.Fatalf("failed to read from temp file: %v", err) - } - - if string(resetContent) != defaultExcludeFileContent { + if f.String() != defaultExcludeFileContent { t.Errorf( "unexpected content after reset:\nGot:\n%s\nExpected:\n%s", - string(resetContent), + f.String(), defaultExcludeFileContent, ) } } + diff --git a/cmd/testhelpers_test.go b/cmd/testhelpers_test.go new file mode 100644 index 0000000..a0896a0 --- /dev/null +++ b/cmd/testhelpers_test.go @@ -0,0 +1,18 @@ +package cmd + +import "bytes" + +// seekBuffer wraps bytes.Buffer to satisfy the readWriteTruncater interface, +// allowing it to be used in place of *os.File in tests. +type seekBuffer struct { + bytes.Buffer +} + +func (s *seekBuffer) Seek(offset int64, whence int) (int64, error) { + return 0, nil +} + +func (s *seekBuffer) Truncate(size int64) error { + s.Buffer.Reset() + return nil +} diff --git a/cmd/util.go b/cmd/util.go index 1a8a2ff..3b8fc19 100644 --- a/cmd/util.go +++ b/cmd/util.go @@ -7,6 +7,13 @@ import ( "strings" ) +// readWriteTruncater is the interface satisfied by *os.File that allows +// reading, writing, seeking, and truncating — the operations needed to rewrite the exclude file. +type readWriteTruncater interface { + io.ReadWriteSeeker + Truncate(size int64) error +} + // readExistingPatterns reads the existing patterns from the exclude file, ignoring comments and empty lines func readExistingPatterns(f io.Reader) ([]string, error) { var patterns []string