Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 34 additions & 5 deletions internal/postprocessing/fileops.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@ import (
"bytes"
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"regexp"
Comment thread
yangyzs marked this conversation as resolved.
"slices"
"strings"

"github.com/googleapis/librarian/internal/filesystem"
Expand All @@ -44,11 +47,6 @@ func CopyFile(src, dst string) error {
return filesystem.CopyFile(src, dst)
}

// RemoveFile removes the file at the specified path.
func RemoveFile(path string) error {
return os.Remove(path)
}

// Replace finds and replaces exact text in a file.
// It returns an error if the target file does not exist or if the text is not found.
func Replace(path, original, replacement string) error {
Expand Down Expand Up @@ -91,3 +89,34 @@ func ReplaceRegex(path, pattern, replacement string) error {
newContent := re.ReplaceAll(content, []byte(replacement))
return os.WriteFile(path, newContent, 0644)
}

// RemoveFiles removes all files in outDir matching the given patterns (exact paths or globs).
func RemoveFiles(outDir string, removePatterns []string) error {
for _, rem := range removePatterns {
if err := applyToFiles(outDir, rem, os.Remove); err != nil {
return err
}
}
return nil
}

// applyToFiles executes action on files matching pathPattern under outDir.
// Note: Uses [filepath.Glob] (* only, ** is not supported).
func applyToFiles(outDir string, pathPattern string, action func(string) error) error {
files, err := filepath.Glob(filepath.Join(outDir, pathPattern))
if err != nil {
return fmt.Errorf("failed to resolve glob for %s: %w", pathPattern, err)
}
if len(files) == 0 {
return fmt.Errorf("no files match pattern %q in %s: %w", pathPattern, outDir, fs.ErrNotExist)
}
// Reverse sort so children are processed before parent directories.
slices.Sort(files)
slices.Reverse(files)
for _, file := range files {
Comment thread
yangyzs marked this conversation as resolved.
if err := action(file); err != nil {
return err
}
}
return nil
}
234 changes: 194 additions & 40 deletions internal/postprocessing/fileops_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"io/fs"
"os"
"path/filepath"
"strings"
"syscall"
"testing"

Expand Down Expand Up @@ -56,44 +57,6 @@ func TestCopyFile_Error(t *testing.T) {
}
}

func TestRemoveFile(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "test.txt")
if err := os.WriteFile(path, []byte("content"), 0644); err != nil {
t.Fatal(err)
}
if err := RemoveFile(path); err != nil {
t.Fatal(err)
}
if _, err := os.Stat(path); err == nil {
t.Error("RemoveFile() expected file to be removed, but it still exists")
}
}

func TestRemoveFile_NonExistent(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "nonexistent.txt")
err := RemoveFile(path)
if !errors.Is(err, fs.ErrNotExist) {
t.Errorf("RemoveFile() returned unexpected error: got %v, want %v", err, fs.ErrNotExist)
}
}

func TestRemoveFile_Error(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "target")
if err := os.Mkdir(path, 0755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(path, "sub.txt"), []byte("data"), 0644); err != nil {
t.Fatal(err)
}
err := RemoveFile(path)
if !errors.Is(err, syscall.ENOTEMPTY) {
t.Errorf("RemoveFile() error = %v, wantErr %v", err, syscall.ENOTEMPTY)
}
}

func TestReplace(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "test.txt")
Expand All @@ -118,7 +81,6 @@ func TestReplace(t *testing.T) {
}

func TestReplaceRegex(t *testing.T) {
t.Parallel()
for _, test := range []struct {
name string
content string
Expand All @@ -142,7 +104,6 @@ func TestReplaceRegex(t *testing.T) {
},
} {
t.Run(test.name, func(t *testing.T) {
t.Parallel()
dir := t.TempDir()
path := filepath.Join(dir, "test.txt")
if err := os.WriteFile(path, []byte(test.content), 0644); err != nil {
Expand Down Expand Up @@ -254,3 +215,196 @@ func TestReplaceRegex_Error(t *testing.T) {
})
}
}

func TestApplyToFiles(t *testing.T) {
t.Parallel()
for _, test := range []struct {
name string
files map[string]string
pattern string
}{
{
name: "exact file success",
files: map[string]string{"foo.txt": "hello"},
pattern: "foo.txt",
},
{
name: "glob pattern success",
files: map[string]string{"a.java": "match", "b.java": "match"},
pattern: "*.java",
},
} {
t.Run(test.name, func(t *testing.T) {
t.Parallel()
dir := t.TempDir()
Comment thread
yangyzs marked this conversation as resolved.
createFiles(t, dir, test.files)
if err := applyToFiles(dir, test.pattern, func(string) error { return nil }); err != nil {
t.Fatal(err)
}
})
}
}

func TestApplyToFiles_Error(t *testing.T) {
for _, test := range []struct {
name string
files map[string]string
pattern string
action func(string) error
wantErr error
}{
{
name: "action fails on glob match",
files: map[string]string{"a.java": "match", "b.java": "nomatch"},
pattern: "*.java",
action: func(p string) error {
if strings.HasSuffix(p, "b.java") {
return errTextNotFound
}
return nil
},
wantErr: errTextNotFound,
},
{
name: "action fails on exact file",
files: map[string]string{"foo.txt": "nomatch"},
pattern: "foo.txt",
action: func(string) error { return errTextNotFound },
wantErr: errTextNotFound,
},
{
name: "zero files match pattern",
files: map[string]string{"other.txt": "hello"},
pattern: "*.java",
action: func(string) error { return nil },
wantErr: fs.ErrNotExist,
},
} {
t.Run(test.name, func(t *testing.T) {
dir := t.TempDir()
createFiles(t, dir, test.files)
err := applyToFiles(dir, test.pattern, test.action)
if !errors.Is(err, test.wantErr) {
t.Errorf("applyToFiles() error = %v, want %v", err, test.wantErr)
}
})
}
}

func TestRemoveFiles(t *testing.T) {
for _, test := range []struct {
name string
files map[string]string
patterns []string
wantFiles map[string]string
}{
{
name: "single glob pattern",
files: map[string]string{"A.java": "java content", "B.txt": "txt content"},
patterns: []string{"*.java"},
wantFiles: map[string]string{"B.txt": "txt content"},
},
{
name: "exact filename",
files: map[string]string{"A.java": "java content", "B.txt": "txt content"},
patterns: []string{"A.java"},
wantFiles: map[string]string{"B.txt": "txt content"},
},
{
name: "multiple glob patterns",
files: map[string]string{"A.java": "java content", "B.txt": "txt content", "C.md": "md content"},
patterns: []string{"*.java", "*.txt"},
wantFiles: map[string]string{"C.md": "md content"},
},
{
name: "nested directory file removal",
files: map[string]string{"src/A.java": "java", "src/B.txt": "txt", "docs/C.html": "html"},
patterns: []string{"src/*.java"},
wantFiles: map[string]string{"src/B.txt": "txt", "docs/C.html": "html"},
},
Comment thread
yangyzs marked this conversation as resolved.
{
name: "directory file deletion",
files: map[string]string{"src/A.java": "java", "src/B.txt": "txt"},
patterns: []string{"src/*"},
wantFiles: map[string]string{},
},
} {
t.Run(test.name, func(t *testing.T) {
dir := t.TempDir()
createFiles(t, dir, test.files)
if err := RemoveFiles(dir, test.patterns); err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(test.wantFiles, readDirFiles(t, dir)); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
})
}
}

func TestRemoveFiles_Error(t *testing.T) {
for _, test := range []struct {
name string
files map[string]string
patterns []string
wantErr error
}{
{
name: "zero files match pattern",
patterns: []string{"nonexistent/*.java"},
wantErr: fs.ErrNotExist,
},
{
name: "remove non-empty directory",
files: map[string]string{"targetDir/file.txt": "data"},
patterns: []string{"targetDir"},
wantErr: syscall.ENOTEMPTY,
},
} {
t.Run(test.name, func(t *testing.T) {
dir := t.TempDir()
createFiles(t, dir, test.files)
err := RemoveFiles(dir, test.patterns)
if !errors.Is(err, test.wantErr) {
t.Errorf("RemoveFiles() error = %v, want %v", err, test.wantErr)
}
})
}
}

func createFiles(t *testing.T, dir string, files map[string]string) {
t.Helper()
for relPath, content := range files {
absPath := filepath.Join(dir, relPath)
if err := os.MkdirAll(filepath.Dir(absPath), 0755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(absPath, []byte(content), 0644); err != nil {
t.Fatal(err)
}
}
}

func readDirFiles(t *testing.T, dir string) map[string]string {
t.Helper()
gotFiles := make(map[string]string)
err := filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error {
if err != nil || d.IsDir() {
return err
}
rel, err := filepath.Rel(dir, path)
if err != nil {
t.Fatal(err)
}
b, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
gotFiles[filepath.ToSlash(rel)] = string(b)
return nil
})
if err != nil {
t.Fatal(err)
}
return gotFiles
}
Loading