Repository: go-go-golems/glazed
Pull request #382 (2023-10-28 17:52:01)

Introduction of Helper Function for Temporary File Management
This pull request introduces a new helper function and associated tests. The key changes include:

- Addition of a function to manage temporary files. This function deletes the oldest files in a directory based on a given mask, leaving only a specified number of the newest files. It returns a list of the paths of the deleted files. The function requires read and write permissions on the directory and will stop if it encounters a file it cannot delete, returning the files it has deleted so far along with the error.

- Inclusion of comprehensive tests for the new function. These tests cover a variety of scenarios, including valid inputs, no matching files, fewer files than specified, exactly the specified number of files, directories, zero files specified, negative file count, non-existent directory, invalid mask, and unreadable directory.



Patch:
diff --git a/pkg/helpers/capture-output.go b/pkg/helpers/capture-output.go
index 852fcc2..deaf11e 100644
--- a/pkg/helpers/capture-output.go
+++ b/pkg/helpers/capture-output.go
@@ -9,7 +9,6 @@ import (
 // CaptureOutput executes a provided function and captures any data
 // the function writes to standard output.
 func CaptureOutput(fn func() error) (string, error) {
-	// Create a buffer to hold the captured output
 	var buf bytes.Buffer
 
 	// Save the original stdout to restore it later
diff --git a/pkg/helpers/files/temp-files.go b/pkg/helpers/files/temp-files.go
new file mode 100644
index 0000000..cf7e357
--- /dev/null
+++ b/pkg/helpers/files/temp-files.go
@@ -0,0 +1,66 @@
+package files
+
+import (
+	"os"
+	"path/filepath"
+	"sort"
+)
+
+type fileInfo struct {
+	os.FileInfo
+	path string // Full path to the file
+}
+
+type byModTime []fileInfo
+
+func (s byModTime) Len() int           { return len(s) }
+func (s byModTime) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }
+func (s byModTime) Less(i, j int) bool { return s[i].ModTime().Before(s[j].ModTime()) }
+
+// GarbageCollectTemporaryFiles deletes the oldest files in a directory based on a given mask,
+// leaving only 'n' newest files. It returns a slice of paths of the deleted files.
+// It requires read and write permissions on the directory. If it encounters a file that it cannot delete,
+// it will stop and return the files it has deleted so far along with the error.
+func GarbageCollectTemporaryFiles(tempDir string, mask string, n int) ([]string, error) {
+	var deletedFiles []string
+
+	matchingFiles, err := filepath.Glob(filepath.Join(tempDir, mask))
+	if err != nil {
+		return nil, err
+	}
+
+	d, err := os.Stat(tempDir)
+	if err != nil {
+		return nil, err
+	}
+	if d.Mode().Perm()&0444 == 0 || d.Mode().Perm()&0222 == 0 {
+		return nil, &os.PathError{Op: "GarbageCollectTemporaryFiles", Path: tempDir, Err: os.ErrPermission}
+	}
+
+	var fileInfos []fileInfo
+	for _, fileName := range matchingFiles {
+		fi, err := os.Stat(fileName)
+		if err != nil {
+			continue
+		}
+
+		if !fi.IsDir() {
+			fileInfos = append(fileInfos, fileInfo{FileInfo: fi, path: fileName})
+		}
+	}
+
+	sort.Sort(byModTime(fileInfos))
+
+	for i, fileInfo := range fileInfos {
+		if i >= len(fileInfos)-n {
+			break
+		}
+		err := os.Remove(fileInfo.path)
+		if err != nil {
+			return deletedFiles, err
+		}
+		deletedFiles = append(deletedFiles, fileInfo.path)
+	}
+
+	return deletedFiles, nil
+}


Test patch:
diff --git a/pkg/helpers/files/temp-files_test.go b/pkg/helpers/files/temp-files_test.go
new file mode 100644
index 0000000..ba9518f
--- /dev/null
+++ b/pkg/helpers/files/temp-files_test.go
@@ -0,0 +1,171 @@
+package files
+
+import (
+	"github.com/pkg/errors"
+	"log"
+	"os"
+	"testing"
+)
+
+func TestGarbageCollectTemporaryFiles(t *testing.T) {
+	tests := []struct {
+		name     string
+		numFiles int
+		n        int
+		want     int
+		err      error
+	}{
+		{
+			name:     "WithValidInputs",
+			numFiles: 5,
+			n:        2,
+			want:     3,
+			err:      nil,
+		},
+		{
+			name:     "WithNoMatchingFiles",
+			numFiles: 0,
+			n:        2,
+			want:     0,
+			err:      nil,
+		},
+		{
+			name:     "WithLessThanNFiles",
+			numFiles: 1,
+			n:        2,
+			want:     0,
+			err:      nil,
+		},
+		{
+			name:     "WithExactlyNFiles",
+			numFiles: 2,
+			n:        2,
+			want:     0,
+			err:      nil,
+		},
+		{
+			name:     "WithDirectories",
+			numFiles: 5,
+			n:        2,
+			want:     3,
+			err:      nil,
+		},
+		{
+			name:     "WithZeroN",
+			numFiles: 5,
+			n:        0,
+			want:     5,
+			err:      nil,
+		},
+		{
+			name:     "WithNegativeN",
+			numFiles: 5,
+			n:        -1,
+			want:     5,
+			err:      nil,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			tempDir, _, err := initTest(t, "", "temp", tt.numFiles, "file*")
+			if err != nil {
+				log.Fatalf("Failed to create temp directory: %v", err)
+			}
+
+			defer func(path string) {
+				err := os.RemoveAll(path)
+				if err != nil {
+					log.Fatalf("Failed to remove temp directory: %v", err)
+				}
+			}(tempDir)
+			// Call GarbageCollectTemporaryFiles
+			deletedFiles, err := GarbageCollectTemporaryFiles(tempDir, "file*", tt.n)
+			if err != tt.err {
+				t.Fatalf("GarbageCollectTemporaryFiles failed: %v, want: %v", err, tt.err)
+			}
+
+			// Check the number of deleted files
+			if got := len(deletedFiles); got != tt.want {
+				t.Errorf("GarbageCollectTemporaryFiles = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func initTest(t *testing.T, dir string, pattern string, numFiles int, filePattern string) (string, []string, error) {
+	// Create a temporary directory
+	tempDir, err := os.MkdirTemp(dir, pattern)
+	if err != nil {
+		t.Fatalf("Failed to create temp directory: %v", err)
+	}
+
+	fileNames := []string{}
+	// Create temporary files
+	for i := 0; i < numFiles; i++ {
+		tempFile, err := os.CreateTemp(tempDir, filePattern)
+		if err != nil {
+			t.Fatalf("Failed to create temp file: %v", err)
+		}
+		_ = tempFile.Close()
+		fileNames = append(fileNames, tempFile.Name())
+	}
+	return tempDir, fileNames, err
+}
+
+func TestGarbageCollectTemporaryFilesWithNonExistentDirectory(t *testing.T) {
+	tempDir := "/nonexistent"
+	n := 2
+	_, err := GarbageCollectTemporaryFiles(tempDir, "file*", n)
+	if err == nil {
+		t.Fatalf("GarbageCollectTemporaryFiles with invalid directory should fail")
+	}
+}
+
+func TestGarbageCollectTemporaryFilesWithInvalidMask(t *testing.T) {
+	tempDir, _, err := initTest(t, "", "temp", 5, "file*")
+	if err != nil {
+		t.Fatalf("Failed to create temp directory: %v", err)
+	}
+	defer func(path string) {
+		err := os.RemoveAll(path)
+		if err != nil {
+			log.Fatalf("Failed to remove temp directory: %v", err)
+		}
+	}(tempDir)
+
+	fs, err := GarbageCollectTemporaryFiles(tempDir, "\x00", 2)
+	if err != nil {
+		t.Fatalf("GarbageCollectTemporaryFiles failed: %v, want: %v", err, os.ErrInvalid)
+	}
+	if len(fs) != 0 {
+		t.Fatalf("GarbageCollectTemporaryFiles failed: %v, want: %v", len(fs), 0)
+	}
+}
+
+func TestGarbageCollectTemporaryFilesWithUnreadableDirError(t *testing.T) {
+	tempDir, _, err := initTest(t, "", "temp", 5, "file*")
+	if err != nil {
+		t.Fatalf("Failed to create temp directory: %v", err)
+	}
+	defer func(path string) {
+		err := os.RemoveAll(path)
+		if err != nil {
+			log.Fatalf("Failed to remove temp directory: %v", err)
+		}
+	}(tempDir)
+
+	n := 2
+	err = os.Chmod(tempDir, 0000)
+	if err != nil {
+		log.Fatalf("Failed to change permissions on temp directory: %v", err)
+	} // remove all permissions
+	_, err = GarbageCollectTemporaryFiles(tempDir, "file*", n)
+	if !errors.Is(err, os.ErrPermission) {
+		t.Fatalf("GarbageCollectTemporaryFiles failed: %v, want: %v", err, os.ErrPermission)
+	}
+	err = os.Chmod(tempDir, 0755)
+	if err != nil {
+		log.Fatalf("Failed to change permissions on temp directory: %v", err)
+	} // restore permissions
+}