From 086ed8406ea62dc401784c0a862de6f54dc8c466 Mon Sep 17 00:00:00 2001 From: "Eduardo M. Santos" <160991364+supitsdu@users.noreply.github.com> Date: Wed, 26 Jun 2024 11:14:25 -0300 Subject: [PATCH 01/14] refactor: remove main_test.go Removed main_test.go as its functionality is now integrated into more specific test files. --- main_test.go | 234 --------------------------------------------------- 1 file changed, 234 deletions(-) delete mode 100644 main_test.go diff --git a/main_test.go b/main_test.go deleted file mode 100644 index f4d953c..0000000 --- a/main_test.go +++ /dev/null @@ -1,234 +0,0 @@ -package main - -import ( - "os" - "testing" - "github.com/xyproto/randomstring" -) - -// TestParseContent_MultipleFiles verifies parseContent correctly concatenates contents of multiple files. -func TestParseContent_MultipleFiles(t *testing.T) { - // Prepare test data with content from two files - content1 := "Content from file 1" - content2 := "Content from file 2" - file1 := createTempFile(t, content1) - file2 := createTempFile(t, content2) - - // Expected result after parsing multiple files - expected := "Content from file 1\nContent from file 2\n" - - // Define directText as nil since we are testing file paths only - var directText *string - - // Execute the function under test - actual, err := parseContent(directText, []string{file1.Name(), file2.Name()}) - if err != nil { - t.Fatalf("TestParseContent_MultipleFiles: Expected no error, got %v", err) - } - - // Verify the actual output matches the expected output - if actual != expected { - t.Errorf("TestParseContent_MultipleFiles: Expected %s but got %s", expected, actual) - } -} - -// TestParseContent_EmptyFiles verifies parseContent handles empty files gracefully. -func TestParseContent_EmptyFiles(t *testing.T) { - // Create an empty temporary file - emptyFile := createTempFile(t, "") - - // Expected result when parsing an empty file - expected := "\n" - - // Define directText as nil since we are testing file paths only - var directText *string - - // Execute the function under test with the empty file - actual, err := parseContent(directText, []string{emptyFile.Name()}) - if err != nil { - t.Fatalf("TestParseContent_EmptyFiles: Expected no error, got %v", err) - } - - // Verify the actual output matches the expected output - if actual != expected { - t.Errorf("TestParseContent_EmptyFiles: Expected %s but got %s", expected, actual) - } -} - -// TestParseContent_EmptyInput ensures parseContent handles empty input gracefully. -func TestParseContent_EmptyInput(t *testing.T) { - // Expected result for empty input - expected := "" - - // Define directText as nil since we are testing empty inputs only - var directText *string - - // Execute the function under test with no input arguments - actual, err := parseContent(directText, []string{}) - if err != nil { - t.Fatalf("TestParseContent_EmptyInput: Expected no error, got %v", err) - } - - // Verify the actual output matches the expected output - if actual != expected { - t.Errorf("TestParseContent_EmptyInput: Expected %s but got %s", expected, actual) - } -} - -// TestParseContent_InvalidFilePath checks how parseContent behaves with an invalid file path. -func TestParseContent_InvalidFilePath(t *testing.T) { - // Invalid file path that doesn't exist - invalidPath := "/invalid/path/to/file.txt" - - // Define directText as nil since we are testing file paths only - var directText *string - - // Execute the function under test with the invalid file path - _, err := parseContent(directText, []string{invalidPath}) - if err == nil { - t.Fatalf("TestParseContent_InvalidFilePath: Expected an error, but got none") - } - // Further assertions can be added based on specific error expectations - // For example, checking the error type or message. -} - -// TestParseContent_LargeFile verifies parseContent handles large files correctly -func TestParseContent_LargeFile(t *testing.T) { - // Create a large file (e.g., 10MB) - largeContent := randomstring.String(10 * 1024 * 1024) // 10MB of random data - largeFile := createTempFile(t, largeContent) - expected := largeContent + "\n" - - // Define directText as nil since we are testing file paths only - var directText *string - - // Execute the function under test with the large file - actual, err := parseContent(directText, []string{largeFile.Name()}) - if err != nil { - t.Fatalf("TestParseContent_LargeFile: Expected no error, got %v", err) - } - - // Verify the actual output matches the expected output - if actual != expected { - t.Errorf("TestParseContent_LargeFile: Output differs") - } -} - -// TestParseContent_DirectText verifies parseContent handles direct text input correctly. -func TestParseContent_DirectText(t *testing.T) { - // Direct text input to parse - content := "Direct text input" - expected := "Direct text input" - - // Execute the function under test with direct text input - actual, err := parseContent(stringPtr(content), []string{}) - if err != nil { - t.Fatalf("TestParseContent_DirectText: Expected no error, got %v", err) - } - - // Verify the actual output matches the expected output - if actual != expected { - t.Errorf("TestParseContent_DirectText: Expected %s but got %s", expected, actual) - } -} - -// TestParseContent_Stdin verifies parseContent handles stdin input correctly. -func TestParseContent_Stdin(t *testing.T) { - // Stdin input content - content := "Stdin input\n" - expected := "Stdin input\n" - // Define directText as nil since we are testing stdin only - var directText *string - - // Replace stdin with a pipe for testing - _, w := replaceStdin(t) - - // Write content to the pipe and close it - go func() { - _, err := w.Write([]byte(content)) - if err != nil { - t.Fatalf("Failed to write to stdin pipe: %v", err) - } - w.Close() - }() - - // Execute the function under test with stdin input - actual, err := parseContent(directText, []string{}) - if err != nil { - t.Fatalf("TestParseContent_Stdin: Expected no error, got %v", err) - } - - // Verify the actual output matches the expected output - if actual != expected { - t.Errorf("TestParseContent_Stdin: Expected %s but got %s", expected, actual) - } -} - -// TestParseContent_File verifies parseContent handles file input correctly. -func TestParseContent_File(t *testing.T) { - // Content to write to the temporary file - content := "Content from file" - file := createTempFile(t, content) - - // Expected content after parsing the file - expected := "Content from file\n" - - // Execute the function under test with the temporary file as input - actual, err := parseContent(nil, []string{file.Name()}) - if err != nil { - t.Fatalf("TestParseContent_File: Expected no error, got %v", err) - } - - // Verify the actual output matches the expected output - if actual != expected { - t.Errorf("TestParseContent_File: Expected %s but got %s", expected, actual) - } -} - -// stringPtr creates a pointer to a string. -func stringPtr(s string) *string { - return &s -} - -// createTempFile creates a temporary file for testing purposes and writes the given content to it. -func createTempFile(t *testing.T, content string) *os.File { -t.Helper () - // Create a temporary file - file, err := os.CreateTemp(t.TempDir(), "testfile") - if err != nil { - t.Fatalf("createTempFile: Failed to create temp file: %v", err) - } - - // Write the provided content to the temporary file - _, err = file.WriteString(content) - if err != nil { - t.Fatalf("createTempFile: Failed to write to temp file: %v", err) - } - - return file -} - -// replaceStdin replaces os.Stdin with a pipe for testing purposes. -func replaceStdin(t *testing.T) (*os.File, *os.File) { -t.Helper () - // Create a pipe - r, w, err := os.Pipe() - if err != nil { - t.Fatalf("replaceStdin: Failed to create pipe: %v", err) - } - - // Save the original stdin - originalStdin := os.Stdin - - // Replace stdin with the read end of the pipe - os.Stdin = r - - // Cleanup function to restore the original stdin and close the pipe - t.Cleanup(func() { - os.Stdin = originalStdin - r.Close() - w.Close() - }) - - return r, w -} From f4daa9bc612f4bfd3f00668224cb7eecfbdd7b37 Mon Sep 17 00:00:00 2001 From: "Eduardo M. Santos" <160991364+supitsdu@users.noreply.github.com> Date: Wed, 26 Jun 2024 11:15:36 -0300 Subject: [PATCH 02/14] refactor: remove main.go Removed main.go file to migrate functionality to the more appropriately named cli/main.go. --- main.go | 103 -------------------------------------------------------- 1 file changed, 103 deletions(-) delete mode 100644 main.go diff --git a/main.go b/main.go deleted file mode 100644 index 0762817..0000000 --- a/main.go +++ /dev/null @@ -1,103 +0,0 @@ -package main - -import ( - "flag" - "fmt" - "io" - "os" - "strings" - - "github.com/atotto/clipboard" -) - -const version = "1.4.0" - -// copyToClipboard writes the content to the clipboard -func copyToClipboard(contentStr string) error { - return clipboard.WriteAll(contentStr) -} - -// readFromStdin reads content from stdin -func readFromStdin() (string, error) { - input, err := io.ReadAll(os.Stdin) - if err != nil { - return "", fmt.Errorf("error reading from stdin: %v", err) - } - return string(input), nil -} - -// readFromFile reads content from the specified file -func readFromFile(filePath string) (string, error) { - content, err := os.ReadFile(filePath) - if err != nil { - return "", fmt.Errorf("error reading file '%s': %v", filePath, err) - } - return string(content), nil -} - -// parseContent determines the content string based on the flags and arguments -func parseContent(directText *string, args []string) (string, error) { - if directText != nil && *directText != "" { - // Use the provided direct text - return *directText, nil - } - - if len(args) == 0 { - // Read from stdin - return readFromStdin() - } - - // Read the content from all provided file paths - var sb strings.Builder - for _, filePath := range args { - content, err := readFromFile(filePath) - if err != nil { - return "", err - } - sb.WriteString(content + "\n") - } - - return sb.String(), nil -} - -func main() { - // Define flags - directText := flag.String("c", "", "Copy text directly from command line argument") - showVersion := flag.Bool("v", false, "Show the current version of the clipper tool") - - // Custom usage message - flag.Usage = func() { - fmt.Fprintf(flag.CommandLine.Output(), "Clipper is a lightweight command-line tool for copying contents to the clipboard.\n") - fmt.Fprintf(flag.CommandLine.Output(), "\nUsage:\n") - fmt.Fprintf(flag.CommandLine.Output(), " clipper [arguments] [file ...]\n") - fmt.Fprintf(flag.CommandLine.Output(), "\nArguments:\n") - fmt.Fprintf(flag.CommandLine.Output(), " -c Copy text directly from command line argument\n") - fmt.Fprintf(flag.CommandLine.Output(), " -v Show the current version of the clipper tool\n") - fmt.Fprintf(flag.CommandLine.Output(), "\nIf no file or text is provided, reads from standard input.\n") - } - - flag.Parse() - - // Check if the version flag is set - if *showVersion { - fmt.Printf("Clipper %s\n", version) - return - } - - // Refactor the code to call parseContent and validate flag args - contentStr, err := parseContent(directText, flag.Args()) - if err != nil { - fmt.Printf("Error parsing content: %v\n", err) - os.Exit(1) - } - - // Write the content to the clipboard - err = copyToClipboard(contentStr) - if err != nil { - fmt.Printf("Error copying content to clipboard: %v\n", err) - os.Exit(1) - } - - // Print success message - fmt.Println("Clipboard updated successfully. Ready to paste!") -} From 7977f0a57d5edb84958e10809be3aee696511d46 Mon Sep 17 00:00:00 2001 From: "Eduardo M. Santos" <160991364+supitsdu@users.noreply.github.com> Date: Wed, 26 Jun 2024 11:16:20 -0300 Subject: [PATCH 03/14] feat: define options configuration struct Define a configuration struct for command line options to manage and parse command line arguments effectively. --- cli/options/options.go | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 cli/options/options.go diff --git a/cli/options/options.go b/cli/options/options.go new file mode 100644 index 0000000..6faba20 --- /dev/null +++ b/cli/options/options.go @@ -0,0 +1,38 @@ +package options + +import ( + "flag" + "fmt" +) + +type Config struct { + DirectText *string + ShowVersion *bool + Args []string +} + +const Version = "1.4.0" + +// ParseFlags parses the command-line flags and arguments. +func ParseFlags() *Config { + directText := flag.String("c", "", "Copy text directly from command line argument") + showVersion := flag.Bool("v", false, "Show the current version of the clipper tool") + + flag.Usage = func() { + fmt.Fprintf(flag.CommandLine.Output(), "Clipper is a lightweight command-line tool for copying contents to the clipboard.\n") + fmt.Fprintf(flag.CommandLine.Output(), "\nUsage:\n") + fmt.Fprintf(flag.CommandLine.Output(), " clipper [arguments] [file ...]\n") + fmt.Fprintf(flag.CommandLine.Output(), "\nArguments:\n") + fmt.Fprintf(flag.CommandLine.Output(), " -c Copy text directly from command line argument\n") + fmt.Fprintf(flag.CommandLine.Output(), " -v Show the current version of the clipper tool\n") + fmt.Fprintf(flag.CommandLine.Output(), "\nIf no file or text is provided, reads from standard input.\n") + } + + flag.Parse() + + return &Config{ + DirectText: directText, + ShowVersion: showVersion, + Args: flag.Args(), + } +} From 5b10fc5391047505b3a42c22deadc45d2a398910 Mon Sep 17 00:00:00 2001 From: "Eduardo M. Santos" <160991364+supitsdu@users.noreply.github.com> Date: Wed, 26 Jun 2024 11:17:17 -0300 Subject: [PATCH 04/14] feat: introduce content and clipboard interfaces Introduce interfaces for content reading and clipboard writing to enhance flexibility and testability. --- cli/clipper/clipper.go | 114 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 cli/clipper/clipper.go diff --git a/cli/clipper/clipper.go b/cli/clipper/clipper.go new file mode 100644 index 0000000..1fe87f1 --- /dev/null +++ b/cli/clipper/clipper.go @@ -0,0 +1,114 @@ +package clipper + +import ( + "fmt" + "io" + "os" + "strings" + + "github.com/supitsdu/clipper/cli/options" + "github.com/atotto/clipboard" +) + +// ContentReader defines an interface for reading content from various sources. +type ContentReader interface { + Read() (string, error) +} + +// ClipboardWriter defines an interface for writing content to the clipboard. +type ClipboardWriter interface { + Write(content string) error +} + +// FileContentReader reads content from a specified file path. +type FileContentReader struct { + FilePath string +} + +// Read reads the content from the file specified in FileContentReader. +func (f FileContentReader) Read() (string, error) { + content, err := os.ReadFile(f.FilePath) + if err != nil { + return "", fmt.Errorf("error reading file '%s': %v", f.FilePath, err) + } + return string(content), nil +} + +// StdinContentReader reads content from the standard input (stdin). +type StdinContentReader struct{} + +// Read reads the content from stdin. +func (s StdinContentReader) Read() (string, error) { + input, err := io.ReadAll(os.Stdin) + if err != nil { + return "", fmt.Errorf("error reading from stdin: %v", err) + } + return string(input), nil +} + +// DefaultClipboardWriter writes content to the clipboard using the default clipboard implementation. +type DefaultClipboardWriter struct{} + +// Write writes the given content to the clipboard. +func (c DefaultClipboardWriter) Write(content string) error { + return clipboard.WriteAll(content) +} + +// ParseContent aggregates content from the provided readers, or returns the direct text if provided. +func ParseContent(directText *string, readers ...ContentReader) (string, error) { + if directText != nil && *directText != "" { + return *directText, nil + } + + if len(readers) == 0 { + return "", fmt.Errorf("no content readers provided") + } + + var sb strings.Builder + for _, reader := range readers { + content, err := reader.Read() + if err != nil { + return "", err + } + sb.WriteString(content + "\n") + } + + return sb.String(), nil +} + +// Run executes the clipper tool logic based on the provided configuration. +func Run(config *options.Config) { + // Display the version if the flag is set. + if *config.ShowVersion { + fmt.Printf("Clipper %s\n", options.Version) + return + } + + var readers []ContentReader + if len(config.Args) > 0 { + // If file paths are provided as arguments, create FileContentReader instances for each. + for _, filePath := range config.Args { + readers = append(readers, FileContentReader{FilePath: filePath}) + } + } else { + // If no file paths are provided, use StdinContentReader to read from stdin. + readers = append(readers, StdinContentReader{}) + } + + // Aggregate the content from the provided sources. + content, err := ParseContent(config.DirectText, readers...) + if err != nil { + fmt.Printf("Error parsing content: %v\n", err) + os.Exit(1) + } + + // Write the parsed content to the clipboard. + writer := DefaultClipboardWriter{} + if err = writer.Write(content); err != nil { + fmt.Printf("Error copying content to clipboard: %v\n", err) + os.Exit(1) + } + + // Print success message. + fmt.Println("Clipboard updated successfully. Ready to paste!") +} From 7e798312e2b2961ad3aa0fc310867247eba97d5d Mon Sep 17 00:00:00 2001 From: "Eduardo M. Santos" <160991364+supitsdu@users.noreply.github.com> Date: Wed, 26 Jun 2024 11:17:54 -0300 Subject: [PATCH 05/14] refactor: restructure command line argument handling Refactor command line argument handling into a dedicated CLI package for improved organization and clarity. --- cli/main.go | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 cli/main.go diff --git a/cli/main.go b/cli/main.go new file mode 100644 index 0000000..0618cce --- /dev/null +++ b/cli/main.go @@ -0,0 +1,11 @@ +package main + +import ( + "github.com/supitsdu/clipper/cli/clipper" + "github.com/supitsdu/clipper/cli/options" +) + +func main() { + config := options.ParseFlags() + clipper.Run(config) +} From 1d9e5ed5c7e243244618fbf7534a6684c4c85db2 Mon Sep 17 00:00:00 2001 From: "Eduardo M. Santos" <160991364+supitsdu@users.noreply.github.com> Date: Thu, 27 Jun 2024 12:24:15 -0300 Subject: [PATCH 06/14] feat: add ClipboardWriter as a parameter to Run - Modify the Run function to accept a ClipboardWriter as a parameter instead of creating a DefaultClipboardWriter directly. This would enable easier mocking of the clipboard for testing. --- cli/clipper/clipper.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/cli/clipper/clipper.go b/cli/clipper/clipper.go index 1fe87f1..32a222a 100644 --- a/cli/clipper/clipper.go +++ b/cli/clipper/clipper.go @@ -6,8 +6,8 @@ import ( "os" "strings" - "github.com/supitsdu/clipper/cli/options" "github.com/atotto/clipboard" + "github.com/supitsdu/clipper/cli/options" ) // ContentReader defines an interface for reading content from various sources. @@ -77,7 +77,7 @@ func ParseContent(directText *string, readers ...ContentReader) (string, error) } // Run executes the clipper tool logic based on the provided configuration. -func Run(config *options.Config) { +func Run(config *options.Config, writer ClipboardWriter) { // Display the version if the flag is set. if *config.ShowVersion { fmt.Printf("Clipper %s\n", options.Version) @@ -102,8 +102,7 @@ func Run(config *options.Config) { os.Exit(1) } - // Write the parsed content to the clipboard. - writer := DefaultClipboardWriter{} + // Write the parsed content to the provided clipboard. if err = writer.Write(content); err != nil { fmt.Printf("Error copying content to clipboard: %v\n", err) os.Exit(1) From 24170582b97938b6c90250781d0d61fa7a629247 Mon Sep 17 00:00:00 2001 From: "Eduardo M. Santos" <160991364+supitsdu@users.noreply.github.com> Date: Thu, 27 Jun 2024 12:24:48 -0300 Subject: [PATCH 07/14] feat: update clipper.Run() with the default clipboard writer --- cli/main.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cli/main.go b/cli/main.go index 0618cce..6c04362 100644 --- a/cli/main.go +++ b/cli/main.go @@ -7,5 +7,6 @@ import ( func main() { config := options.ParseFlags() - clipper.Run(config) + writer := clipper.DefaultClipboardWriter{} + clipper.Run(config, writer) } From f559780df2dd6cd6d7ac581ed34997424c92f46a Mon Sep 17 00:00:00 2001 From: "Eduardo M. Santos" <160991364+supitsdu@users.noreply.github.com> Date: Thu, 27 Jun 2024 12:30:04 -0300 Subject: [PATCH 08/14] test: add comprehensive unit tests for clipper package This commit introduces a new `tests/clipper_test.go` file, which includes comprehensive unit tests for the clipper package. The tests cover various components including: - FileContentReader: Reads content from a file. - StdinContentReader: Reads content from standard input. - DefaultClipboardWriter: Writes content to the clipboard. - ParseContent: Parses content from multiple sources, including direct text, files, and invalid inputs. Additionally, the tests use helper functions for creating temporary files and replacing stdin, improving readability and maintainability. --- tests/clipper_test.go | 272 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 272 insertions(+) create mode 100644 tests/clipper_test.go diff --git a/tests/clipper_test.go b/tests/clipper_test.go new file mode 100644 index 0000000..f4137a0 --- /dev/null +++ b/tests/clipper_test.go @@ -0,0 +1,272 @@ +package clipper + +import ( + "os" + "testing" + + "github.com/atotto/clipboard" + "github.com/supitsdu/clipper/cli/clipper" + + "github.com/xyproto/randomstring" +) + +const mockTextContent = "Mocking Bird! Just A Sample Text." + +func TestContentReaders(t *testing.T) { + t.Run("FileContentReader", func(t *testing.T) { + // Create a temporary file + file, err := createTempFile(t, mockTextContent) + if err != nil { + t.Fatalf("Failed to create temp file: %v", err) + } + + // Create a FileContentReader + reader := clipper.FileContentReader{FilePath: file.Name()} + + // Read the content + readContent, err := reader.Read() + if err != nil { + t.Fatalf("Error reading file: %v", err) + } + + // Check the content + if readContent != mockTextContent { + t.Errorf("Expected '%s', got '%s'", mockTextContent, readContent) + } + }) + + t.Run("StdinContentReader", func(t *testing.T) { + // Create a StdinContentReader + reader := clipper.StdinContentReader{} + + // Replace stdin with a pipe + _, w := replaceStdin(t) + + // Write some content to the pipe + _, err := w.WriteString(mockTextContent) + if err != nil { + t.Fatalf("Error writing to pipe: %v", err) + } + + // Close the write end of the pipe + err = w.Close() + if err != nil { + t.Fatalf("Error closing pipe: %v", err) + } + + // Read the content + readContent, err := reader.Read() + if err != nil { + t.Fatalf("Error reading from stdin: %v", err) + } + + // Check the content + if readContent != mockTextContent { + t.Errorf("Expected '%s', got '%s'", mockTextContent, readContent) + } + }) +} + +func TestClipboardWriter(t *testing.T) { + t.Run("DefaultClipboardWriter", func(t *testing.T) { + // Create a DefaultClipboardWriter + writer := clipper.DefaultClipboardWriter{} + + // Write some content to the clipboard + err := writer.Write(mockTextContent) + if err != nil { + t.Errorf("Error writing to clipboard: %v", err) + } + + // Check the clipboard content + clipboardContent, err := clipboard.ReadAll() + if err != nil { + t.Errorf("Error reading from clipboard: %v", err) + } + + // Check the content + if clipboardContent != mockTextContent { + t.Errorf("Expected '%s', got '%s'", mockTextContent, clipboardContent) + } + }) +} + +func TestParseContent(t *testing.T) { + t.Run("MultipleFiles", func(t *testing.T) { + // Prepare test data with content from two files + content1 := "Content from file 1" + content2 := "Content from file 2" + file1, err := createTempFile(t, content1) + if err != nil { + t.Fatalf("Failed to create temp file: %v", err) + } + file2, err := createTempFile(t, content2) + if err != nil { + t.Fatalf("Failed to create temp file: %v", err) + } + + // Expected result after parsing multiple files + expected := "Content from file 1\nContent from file 2\n" + + // Create FileContentReaders for the files + reader1 := clipper.FileContentReader{FilePath: file1.Name()} + reader2 := clipper.FileContentReader{FilePath: file2.Name()} + + // Execute the function under test + actual, err := clipper.ParseContent(nil, reader1, reader2) + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + // Verify the actual output matches the expected output + if actual != expected { + t.Errorf("Expected %s but got %s", expected, actual) + } + }) + + t.Run("EmptyFiles", func(t *testing.T) { + // Create an empty temporary file + emptyFile, err := createTempFile(t, "") + if err != nil { + t.Fatalf("Failed to create temp file: %v", err) + } + + // Expected result when parsing an empty file + expected := "\n" + + // Create a FileContentReader for the empty file + reader := clipper.FileContentReader{FilePath: emptyFile.Name()} + + // Execute the function under test with the empty file + actual, err := clipper.ParseContent(nil, reader) + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + // Verify the actual output matches the expected output + if actual != expected { + t.Errorf("Expected %s but got %s", expected, actual) + } + }) + + t.Run("InvalidNilInput", func(t *testing.T) { + // Execute the function under test with no input arguments + _, err := clipper.ParseContent(nil) + if err == nil { + t.Fatalf("Expected error, got %v", err) + } + }) + + t.Run("InvalidFilePath", func(t *testing.T) { + // Invalid file path that doesn't exist + invalidPath := "/invalid/path/to/file.txt" + + // Create a FileContentReader with the invalid file path + reader := clipper.FileContentReader{FilePath: invalidPath} + + // Execute the function under test with the invalid file path + _, err := clipper.ParseContent(nil, reader) + if err == nil { + t.Fatalf("Expected an error, but got none") + } + // Further assertions can be added based on specific error expectations + // For example, checking the error type or message. + }) + + t.Run("LargeFile", func(t *testing.T) { + // Create a large file (e.g., 10MB) + largeContent := randomstring.String(10 * 1024 * 1024) // 10MB of random data + largeFile, err := createTempFile(t, largeContent) + if err != nil { + t.Fatalf("Failed to create temp file: %v", err) + } + expected := largeContent + "\n" + + // Create a FileContentReader for the large file + reader := clipper.FileContentReader{FilePath: largeFile.Name()} + + // Execute the function under test with the large file + actual, err := clipper.ParseContent(nil, reader) + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + // Verify the actual output matches the expected output + if actual != expected { + t.Errorf("Output differs") + } + }) + + t.Run("DirectText", func(t *testing.T) { + // Test with direct text + directText := mockTextContent + content, err := clipper.ParseContent(&directText) + if err != nil { + t.Errorf("Error parsing content: %v", err) + } + if content != directText { + t.Errorf("Expected '%s', got '%s'", directText, content) + } + }) + + t.Run("FileContentReader", func(t *testing.T) { + // Test with a FileContentReader + tmpFile, err := createTempFile(t, mockTextContent) + if err != nil { + t.Fatalf("Failed to create temp file: %v", err) + } + + reader := clipper.FileContentReader{FilePath: tmpFile.Name()} + + content, err := clipper.ParseContent(nil, reader) + if err != nil { + t.Errorf("Error parsing content: %v", err) + } + if content != (mockTextContent + "\n") { + t.Errorf("Expected '%s', got '%s'", mockTextContent, content) + } + }) +} + +// replaceStdin replaces os.Stdin with a pipe for testing purposes. +func replaceStdin(t *testing.T) (*os.File, *os.File) { + t.Helper() + // Create a pipe + r, w, err := os.Pipe() + if err != nil { + t.Fatalf("replaceStdin: Failed to create pipe: %v", err) + } + + // Save the original stdin + originalStdin := os.Stdin + + // Replace stdin with the read end of the pipe + os.Stdin = r + + // Cleanup function to restore the original stdin and close the pipe + t.Cleanup(func() { + os.Stdin = originalStdin + r.Close() + w.Close() + }) + + return r, w +} + +// createTempFile creates a temporary file for testing purposes and writes the given content to it. +func createTempFile(t *testing.T, content string) (*os.File, error) { + t.Helper() + // Create a temporary file + file, err := os.CreateTemp(t.TempDir(), "testfile") + if err != nil { + return nil, err + } + + // Write the provided content to the temporary file + _, err = file.WriteString(content) + if err != nil { + return nil, err + } + + return file, nil +} From 27d426385676c9bed1854b3718c0a7b8d3278ab5 Mon Sep 17 00:00:00 2001 From: "Eduardo M. Santos" <160991364+supitsdu@users.noreply.github.com> Date: Thu, 27 Jun 2024 13:37:15 -0300 Subject: [PATCH 09/14] chore: bump release to 1.5.0 --- cli/options/options.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/options/options.go b/cli/options/options.go index 6faba20..49f8c02 100644 --- a/cli/options/options.go +++ b/cli/options/options.go @@ -11,7 +11,7 @@ type Config struct { Args []string } -const Version = "1.4.0" +const Version = "1.5.0" // ParseFlags parses the command-line flags and arguments. func ParseFlags() *Config { From 45c6bcfaef5ac5098df5b325c79b518446634032 Mon Sep 17 00:00:00 2001 From: "Eduardo M. Santos" <160991364+supitsdu@users.noreply.github.com> Date: Fri, 28 Jun 2024 00:27:52 -0300 Subject: [PATCH 10/14] build: update build target to the cli folder --- makefile | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/makefile b/makefile index fec142c..32c7a17 100644 --- a/makefile +++ b/makefile @@ -20,27 +20,27 @@ all: windows linux linux_arm linux_arm64 darwin darwin_arm64 # Build binary for Windows windows: $(OUT_DIR) - GOOS=windows GOARCH=amd64 go build -ldflags="-X main.version=$(VERSION)" -o $(OUT_DIR)/$(WINDOWS_BIN) + GOOS=windows GOARCH=amd64 go build -ldflags="-X main.version=$(VERSION)" -o $(OUT_DIR)/$(WINDOWS_BIN) ./cli # Build binary for Linux (amd64) linux: $(OUT_DIR) - GOOS=linux GOARCH=amd64 go build -ldflags="-X main.version=$(VERSION)" -o $(OUT_DIR)/$(LINUX_BIN) + GOOS=linux GOARCH=amd64 go build -ldflags="-X main.version=$(VERSION)" -o $(OUT_DIR)/$(LINUX_BIN) ./cli # Build binary for Linux (arm) linux_arm: $(OUT_DIR) - GOOS=linux GOARCH=arm go build -ldflags="-X main.version=$(VERSION)" -o $(OUT_DIR)/$(LINUX_ARM_BIN) + GOOS=linux GOARCH=arm go build -ldflags="-X main.version=$(VERSION)" -o $(OUT_DIR)/$(LINUX_ARM_BIN) ./cli # Build binary for Linux (arm64) linux_arm64: $(OUT_DIR) - GOOS=linux GOARCH=arm64 go build -ldflags="-X main.version=$(VERSION)" -o $(OUT_DIR)/$(LINUX_ARM64_BIN) + GOOS=linux GOARCH=arm64 go build -ldflags="-X main.version=$(VERSION)" -o $(OUT_DIR)/$(LINUX_ARM64_BIN) ./cli # Build binary for macOS (amd64) darwin: $(OUT_DIR) - GOOS=darwin GOARCH=amd64 go build -ldflags="-X main.version=$(VERSION)" -o $(OUT_DIR)/$(DARWIN_BIN) + GOOS=darwin GOARCH=amd64 go build -ldflags="-X main.version=$(VERSION)" -o $(OUT_DIR)/$(DARWIN_BIN) ./cli # Build binary for macOS (arm64) darwin_arm64: $(OUT_DIR) - GOOS=darwin GOARCH=arm64 go build -ldflags="-X main.version=$(VERSION)" -o $(OUT_DIR)/$(DARWIN_ARM64_BIN) + GOOS=darwin GOARCH=arm64 go build -ldflags="-X main.version=$(VERSION)" -o $(OUT_DIR)/$(DARWIN_ARM64_BIN) ./cli # Generate SHA256 checksums for each binary checksums: $(OUT_DIR) @@ -80,4 +80,4 @@ help: # Show the latest git tag version version: - @echo $(VERSION) \ No newline at end of file + @echo $(VERSION) From ea807f2bde363696da3e793ca7f138ff7b8a66ff Mon Sep 17 00:00:00 2001 From: "Eduardo M. Santos" <160991364+supitsdu@users.noreply.github.com> Date: Fri, 28 Jun 2024 02:16:19 -0300 Subject: [PATCH 11/14] tests: skips DefaultClipboardWriter test on CI Skipping clipboard test in short mode. Helps avoid errors when on CI environments. --- tests/clipper_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/clipper_test.go b/tests/clipper_test.go index f4137a0..c6573c4 100644 --- a/tests/clipper_test.go +++ b/tests/clipper_test.go @@ -69,6 +69,10 @@ func TestContentReaders(t *testing.T) { func TestClipboardWriter(t *testing.T) { t.Run("DefaultClipboardWriter", func(t *testing.T) { + if testing.Short() == true { + t.Skip("Skipping clipboard test in short mode. Helps avoid errors when on CI environments.") + } + // Create a DefaultClipboardWriter writer := clipper.DefaultClipboardWriter{} From 732bb2bba5e9b6cb166578779fee39197fa1061b Mon Sep 17 00:00:00 2001 From: "Eduardo M. Santos" <160991364+supitsdu@users.noreply.github.com> Date: Fri, 28 Jun 2024 02:24:06 -0300 Subject: [PATCH 12/14] refactor: modify Run() to return (string, error) instead of printing and exiting - Refactored the `Run` function to return a `string` (representing the success message or version information) and an `error`. - This change allows the caller of `Run` to handle the output and errors gracefully, rather than relying on `Run` to print messages and exit directly. - Updated the error handling to use `fmt.Errorf` with error wrapping (%w) for better context. - This improvement enhances the flexibility and testability of the `clipper` tool. --- cli/clipper/clipper.go | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/cli/clipper/clipper.go b/cli/clipper/clipper.go index 32a222a..470030f 100644 --- a/cli/clipper/clipper.go +++ b/cli/clipper/clipper.go @@ -29,7 +29,7 @@ type FileContentReader struct { func (f FileContentReader) Read() (string, error) { content, err := os.ReadFile(f.FilePath) if err != nil { - return "", fmt.Errorf("error reading file '%s': %v", f.FilePath, err) + return "", fmt.Errorf("error reading file '%s': %w", f.FilePath, err) } return string(content), nil } @@ -41,7 +41,7 @@ type StdinContentReader struct{} func (s StdinContentReader) Read() (string, error) { input, err := io.ReadAll(os.Stdin) if err != nil { - return "", fmt.Errorf("error reading from stdin: %v", err) + return "", fmt.Errorf("error reading from stdin: %w", err) } return string(input), nil } @@ -77,11 +77,9 @@ func ParseContent(directText *string, readers ...ContentReader) (string, error) } // Run executes the clipper tool logic based on the provided configuration. -func Run(config *options.Config, writer ClipboardWriter) { - // Display the version if the flag is set. +func Run(config *options.Config, writer ClipboardWriter) (string, error) { if *config.ShowVersion { - fmt.Printf("Clipper %s\n", options.Version) - return + return options.Version, nil } var readers []ContentReader @@ -98,16 +96,13 @@ func Run(config *options.Config, writer ClipboardWriter) { // Aggregate the content from the provided sources. content, err := ParseContent(config.DirectText, readers...) if err != nil { - fmt.Printf("Error parsing content: %v\n", err) - os.Exit(1) + return "", fmt.Errorf("parsing content: %w", err) } // Write the parsed content to the provided clipboard. if err = writer.Write(content); err != nil { - fmt.Printf("Error copying content to clipboard: %v\n", err) - os.Exit(1) + return "", fmt.Errorf("copying content to clipboard: %w", err) } - // Print success message. - fmt.Println("Clipboard updated successfully. Ready to paste!") + return "updated clipboard successfully. Ready to paste!", nil } From eb3a0a88ab782b23b34f27dc322bf3674d7335af Mon Sep 17 00:00:00 2001 From: "Eduardo M. Santos" <160991364+supitsdu@users.noreply.github.com> Date: Fri, 28 Jun 2024 02:25:39 -0300 Subject: [PATCH 13/14] refactor: update main function to handle Run's (string, error) return values - Modified the `main` function to call `clipper.Run` and handle both the returned message and error. - Prints the success message if no error occurred, or an error message if an error was encountered. - Exits with a status code of 0 for success or 1 for errors. - This change ensures that the `main` function gracefully handles the output and errors generated by the `Run` function. --- cli/main.go | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/cli/main.go b/cli/main.go index 6c04362..c47b8ca 100644 --- a/cli/main.go +++ b/cli/main.go @@ -1,12 +1,25 @@ package main import ( + "fmt" + "os" + "github.com/supitsdu/clipper/cli/clipper" "github.com/supitsdu/clipper/cli/options" ) func main() { config := options.ParseFlags() - writer := clipper.DefaultClipboardWriter{} - clipper.Run(config, writer) + writer := clipper.DefaultClipboardWriter{} + + msg, err := clipper.Run(config, writer) + if err != nil { + fmt.Printf("Error %s\n", err) + os.Exit(1) + } + + if msg != "" { + fmt.Printf("Clipper %s\n", msg) + os.Exit(0) + } } From b3be15039e0093c7dce6a09e4cb64512ddf74792 Mon Sep 17 00:00:00 2001 From: "Eduardo M. Santos" <160991364+supitsdu@users.noreply.github.com> Date: Fri, 28 Jun 2024 02:34:27 -0300 Subject: [PATCH 14/14] refactor: extract GetReaders function to improve code organization - Introduced a new function `GetReaders` in `clipper.go` to handle the logic of creating `ContentReader` instances based on command-line arguments. - This refactoring improves the readability and maintainability of the `Run` function by separating out a specific task. - The `Run` function now calls `GetReaders` to obtain the necessary readers, making its logic more focused and concise. --- cli/clipper/clipper.go | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/cli/clipper/clipper.go b/cli/clipper/clipper.go index 470030f..5e63ee7 100644 --- a/cli/clipper/clipper.go +++ b/cli/clipper/clipper.go @@ -76,22 +76,27 @@ func ParseContent(directText *string, readers ...ContentReader) (string, error) return sb.String(), nil } +func GetReaders(targets []string) []ContentReader { + if len(targets) == 0 { + // If no file paths are provided, use StdinContentReader to read from stdin. + return []ContentReader{StdinContentReader{}} + } else { + // If file paths are provided as arguments, create FileContentReader instances for each. + var readers []ContentReader + for _, filePath := range targets { + readers = append(readers, FileContentReader{FilePath: filePath}) + } + return readers + } +} + // Run executes the clipper tool logic based on the provided configuration. func Run(config *options.Config, writer ClipboardWriter) (string, error) { if *config.ShowVersion { return options.Version, nil } - var readers []ContentReader - if len(config.Args) > 0 { - // If file paths are provided as arguments, create FileContentReader instances for each. - for _, filePath := range config.Args { - readers = append(readers, FileContentReader{FilePath: filePath}) - } - } else { - // If no file paths are provided, use StdinContentReader to read from stdin. - readers = append(readers, StdinContentReader{}) - } + readers := GetReaders(config.Args) // Aggregate the content from the provided sources. content, err := ParseContent(config.DirectText, readers...)