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.
This commit is contained in:
2026-04-07 01:22:56 +01:00
parent c2eab8576e
commit cc15888df8
7 changed files with 86 additions and 89 deletions

View File

@@ -10,40 +10,47 @@ func TestRunAddCommand(t *testing.T) {
name string
existing string
args []string
expectedOutput 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",
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",
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",
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())
}
})
}

View File

@@ -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 {
if err := f.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 {
if _, err := f.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 := 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)
}
}

View File

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

View File

@@ -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")

View File

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

18
cmd/testhelpers_test.go Normal file
View File

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

View File

@@ -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