summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-06-20 09:34:26 +0300
committerPaul Buetow <paul@buetow.org>2025-06-20 09:34:26 +0300
commitecc66fcf9241c429cf2378f09793ecef3a00b098 (patch)
tree4e671982bec11a6d12a1374b56bae2b89f0f4287
parentc24e18b68b29384d2f63d44bfcbc9c02423edf78 (diff)
Fix line ending issue in dcat and add integration tests
- Fixed missing line endings in dcat output when not using --plain mode - Scanner.Bytes() strips newlines, so added logic to restore them - Only CatProcessor needs newlines added (GrepProcessor already adds them) - Added comprehensive integration tests for both dcat and dgrep line endings - Tests cover: basic usage, plain mode, multiple files, empty files, CRLF handling 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
-rw-r--r--integrationtests/dcat_line_endings_test.go147
-rw-r--r--integrationtests/dgrep_line_endings_test.go155
-rw-r--r--internal/io/fs/directprocessor.go10
3 files changed, 312 insertions, 0 deletions
diff --git a/integrationtests/dcat_line_endings_test.go b/integrationtests/dcat_line_endings_test.go
new file mode 100644
index 0000000..5484929
--- /dev/null
+++ b/integrationtests/dcat_line_endings_test.go
@@ -0,0 +1,147 @@
+package integrationtests
+
+import (
+ "bytes"
+ "os"
+ "os/exec"
+ "testing"
+
+ "github.com/mimecast/dtail/internal/config"
+)
+
+// TestDCatLineEndings verifies that dcat preserves line endings correctly
+func TestDCatLineEndings(t *testing.T) {
+ if !config.Env("DTAIL_INTEGRATION_TEST_RUN_MODE") {
+ t.Log("Skipping")
+ return
+ }
+
+ // Create a test file with various line endings
+ testContent := "Line 1\nLine 2\nLine 3 with no ending"
+ testFile := "test_line_endings.txt"
+
+ if err := os.WriteFile(testFile, []byte(testContent), 0644); err != nil {
+ t.Fatalf("Failed to create test file: %v", err)
+ }
+ defer os.Remove(testFile)
+
+ t.Run("Serverless", func(t *testing.T) {
+ // Run dcat in serverless mode
+ cmd := exec.Command("../dcat", "--cfg", "none", testFile)
+ output, err := cmd.Output()
+ if err != nil {
+ t.Fatalf("dcat command failed: %v", err)
+ }
+
+ // Expected output should have line endings preserved
+ // Note: dcat adds a newline after the last line too
+ expected := "Line 1\nLine 2\nLine 3 with no ending\n"
+
+ if string(output) != expected {
+ t.Errorf("Line endings not preserved correctly.\nExpected:\n%q\nGot:\n%q",
+ expected, string(output))
+ }
+ })
+
+ t.Run("PlainMode", func(t *testing.T) {
+ // Test with --plain flag which should preserve exact file content
+ cmd := exec.Command("../dcat", "--plain", "--cfg", "none", testFile)
+ output, err := cmd.Output()
+ if err != nil {
+ t.Fatalf("dcat command failed: %v", err)
+ }
+
+ // In plain mode, content should match exactly
+ if string(output) != testContent {
+ t.Errorf("Plain mode should preserve exact content.\nExpected:\n%q\nGot:\n%q",
+ testContent, string(output))
+ }
+ })
+
+ t.Run("MultipleFiles", func(t *testing.T) {
+ // Create additional test files
+ testFile2 := "test_line_endings2.txt"
+ testFile3 := "test_line_endings3.txt"
+
+ if err := os.WriteFile(testFile2, []byte("File 2 Line 1\nFile 2 Line 2\n"), 0644); err != nil {
+ t.Fatalf("Failed to create test file 2: %v", err)
+ }
+ defer os.Remove(testFile2)
+
+ if err := os.WriteFile(testFile3, []byte("File 3 Line 1\n"), 0644); err != nil {
+ t.Fatalf("Failed to create test file 3: %v", err)
+ }
+ defer os.Remove(testFile3)
+
+ // Run dcat with multiple files
+ cmd := exec.Command("../dcat", "--cfg", "none", testFile, testFile2, testFile3)
+ output, err := cmd.Output()
+ if err != nil {
+ t.Fatalf("dcat command failed: %v", err)
+ }
+
+ // Verify that lines from all files are separated properly
+ lines := bytes.Split(output, []byte{'\n'})
+ // We expect 7 lines total (3 + 2 + 1 + empty line at end)
+ if len(lines) != 7 {
+ t.Errorf("Expected 7 lines, got %d. Output:\n%s", len(lines), string(output))
+ }
+ })
+
+ t.Run("EmptyFile", func(t *testing.T) {
+ // Test with empty file
+ emptyFile := "empty.txt"
+ if err := os.WriteFile(emptyFile, []byte(""), 0644); err != nil {
+ t.Fatalf("Failed to create empty file: %v", err)
+ }
+ defer os.Remove(emptyFile)
+
+ cmd := exec.Command("../dcat", "--cfg", "none", emptyFile)
+ output, err := cmd.Output()
+ if err != nil {
+ t.Fatalf("dcat command failed: %v", err)
+ }
+
+ // Empty file should produce empty output
+ if len(output) != 0 {
+ t.Errorf("Expected empty output for empty file, got: %q", string(output))
+ }
+ })
+
+ t.Run("CRLFLineEndings", func(t *testing.T) {
+ // Test with Windows-style CRLF line endings
+ crlfFile := "crlf_test.txt"
+ crlfContent := "Line 1\r\nLine 2\r\nLine 3\r\n"
+
+ if err := os.WriteFile(crlfFile, []byte(crlfContent), 0644); err != nil {
+ t.Fatalf("Failed to create CRLF test file: %v", err)
+ }
+ defer os.Remove(crlfFile)
+
+ // Run dcat in regular mode
+ cmd := exec.Command("../dcat", "--cfg", "none", crlfFile)
+ output, err := cmd.Output()
+ if err != nil {
+ t.Fatalf("dcat command failed: %v", err)
+ }
+
+ // In regular mode, CRLF should be normalized to LF
+ expected := "Line 1\nLine 2\nLine 3\n"
+ if string(output) != expected {
+ t.Errorf("CRLF not handled correctly.\nExpected:\n%q\nGot:\n%q",
+ expected, string(output))
+ }
+
+ // In plain mode, CRLF should be preserved
+ cmd = exec.Command("../dcat", "--plain", "--cfg", "none", crlfFile)
+ output, err = cmd.Output()
+ if err != nil {
+ t.Fatalf("dcat --plain command failed: %v", err)
+ }
+
+ if string(output) != crlfContent {
+ t.Errorf("Plain mode should preserve CRLF.\nExpected:\n%q\nGot:\n%q",
+ crlfContent, string(output))
+ }
+ })
+} \ No newline at end of file
diff --git a/integrationtests/dgrep_line_endings_test.go b/integrationtests/dgrep_line_endings_test.go
new file mode 100644
index 0000000..3d28e5c
--- /dev/null
+++ b/integrationtests/dgrep_line_endings_test.go
@@ -0,0 +1,155 @@
+package integrationtests
+
+import (
+ "os"
+ "os/exec"
+ "strings"
+ "testing"
+
+ "github.com/mimecast/dtail/internal/config"
+)
+
+// TestDGrepLineEndings verifies that dgrep preserves line endings correctly
+func TestDGrepLineEndings(t *testing.T) {
+ if !config.Env("DTAIL_INTEGRATION_TEST_RUN_MODE") {
+ t.Log("Skipping")
+ return
+ }
+
+ // Create a test file with various content
+ testContent := `Line 1 with pattern
+Line 2 no test
+Line 3 with pattern again
+Line 4 no test
+Line 5 final pattern`
+
+ testFile := "test_grep_line_endings.txt"
+ if err := os.WriteFile(testFile, []byte(testContent), 0644); err != nil {
+ t.Fatalf("Failed to create test file: %v", err)
+ }
+ defer os.Remove(testFile)
+
+ t.Run("BasicGrep", func(t *testing.T) {
+ // Run dgrep searching for "pattern"
+ cmd := exec.Command("../dgrep", "--cfg", "none", "--grep", "pattern", testFile)
+ output, err := cmd.Output()
+ if err != nil {
+ t.Fatalf("dgrep command failed: %v", err)
+ }
+
+ // Should get 3 matching lines, each with proper line ending
+ outputStr := string(output)
+ lines := strings.Split(strings.TrimRight(outputStr, "\n"), "\n")
+ if len(lines) != 3 {
+ t.Errorf("Expected 3 matching lines, got %d lines. Output:\n%s",
+ len(lines), outputStr)
+ }
+
+ // Verify each line contains "pattern"
+ for i, line := range lines {
+ if !strings.Contains(line, "pattern") {
+ t.Errorf("Line %d should contain 'pattern': %s", i, line)
+ }
+ }
+ })
+
+ t.Run("WithContext", func(t *testing.T) {
+ // Test with before and after context
+ cmd := exec.Command("../dgrep", "--cfg", "none",
+ "--before", "1", "--after", "1", "--grep", "Line 3", testFile)
+ output, err := cmd.Output()
+ if err != nil {
+ t.Fatalf("dgrep command failed: %v", err)
+ }
+
+ // Should get Line 2 (before), Line 3 (match), Line 4 (after)
+ lines := strings.Split(strings.TrimRight(string(output), "\n"), "\n")
+ if len(lines) != 3 {
+ t.Errorf("Expected 3 lines with context, got %d. Output:\n%s",
+ len(lines), string(output))
+ }
+
+ // Verify the lines
+ if !strings.Contains(lines[0], "Line 2") {
+ t.Errorf("First line should be before context (Line 2): %s", lines[0])
+ }
+ if !strings.Contains(lines[1], "Line 3") {
+ t.Errorf("Second line should be the match (Line 3): %s", lines[1])
+ }
+ if !strings.Contains(lines[2], "Line 4") {
+ t.Errorf("Third line should be after context (Line 4): %s", lines[2])
+ }
+ })
+
+ t.Run("NoMatches", func(t *testing.T) {
+ // Test with pattern that doesn't match
+ cmd := exec.Command("../dgrep", "--cfg", "none", "--grep", "nonexistent", testFile)
+ output, err := cmd.Output()
+ if err != nil {
+ t.Fatalf("dgrep command failed: %v", err)
+ }
+
+ // Should get empty output
+ if len(output) != 0 {
+ t.Errorf("Expected empty output for no matches, got: %q", string(output))
+ }
+ })
+
+ t.Run("PlainMode", func(t *testing.T) {
+ // Test with --plain flag
+ cmd := exec.Command("../dgrep", "--plain", "--cfg", "none", "--grep", "pattern", testFile)
+ output, err := cmd.Output()
+ if err != nil {
+ t.Fatalf("dgrep command failed: %v", err)
+ }
+
+ // In plain mode, should still have proper line endings
+ outputStr := string(output)
+ lines := strings.Split(strings.TrimRight(outputStr, "\n"), "\n")
+ if len(lines) != 3 {
+ t.Errorf("Plain mode: Expected 3 matching lines, got %d lines",
+ len(lines))
+ }
+ })
+
+ t.Run("MultipleFiles", func(t *testing.T) {
+ // Create additional test files
+ testFile2 := "test_grep_line_endings2.txt"
+ testFile3 := "test_grep_line_endings3.txt"
+
+ content2 := "File 2 with pattern\nFile 2 no test\n"
+ content3 := "File 3 no test\nFile 3 with pattern\n"
+
+ if err := os.WriteFile(testFile2, []byte(content2), 0644); err != nil {
+ t.Fatalf("Failed to create test file 2: %v", err)
+ }
+ defer os.Remove(testFile2)
+
+ if err := os.WriteFile(testFile3, []byte(content3), 0644); err != nil {
+ t.Fatalf("Failed to create test file 3: %v", err)
+ }
+ defer os.Remove(testFile3)
+
+ // Run dgrep with multiple files
+ cmd := exec.Command("../dgrep", "--cfg", "none", "--grep", "pattern",
+ testFile, testFile2, testFile3)
+ output, err := cmd.Output()
+ if err != nil {
+ t.Fatalf("dgrep command failed: %v", err)
+ }
+
+ // Should get 5 matching lines total (3 + 1 + 1)
+ lines := strings.Split(strings.TrimRight(string(output), "\n"), "\n")
+ if len(lines) != 5 {
+ t.Errorf("Expected 5 matching lines from multiple files, got %d. Output:\n%s",
+ len(lines), string(output))
+ }
+
+ // Verify all lines contain "pattern"
+ for i, line := range lines {
+ if !strings.Contains(line, "pattern") {
+ t.Errorf("Line %d should contain 'pattern': %s", i, line)
+ }
+ }
+ })
+} \ No newline at end of file
diff --git a/internal/io/fs/directprocessor.go b/internal/io/fs/directprocessor.go
index dd259b6..e02d4c2 100644
--- a/internal/io/fs/directprocessor.go
+++ b/internal/io/fs/directprocessor.go
@@ -114,9 +114,19 @@ func (dp *DirectProcessor) ProcessReader(ctx context.Context, reader io.Reader,
return err
}
} else {
+ // Regular write path (e.g., stdout in serverless mode)
if _, err := dp.output.Write(result); err != nil {
return err
}
+
+ // Only add newline if the processor doesn't already handle it
+ // CatProcessor doesn't add newlines, but GrepProcessor does
+ if _, isCat := dp.processor.(*CatProcessor); isCat {
+ // Scanner strips newlines, so we need to add them back for cat
+ if _, err := dp.output.Write([]byte{'\n'}); err != nil {
+ return err
+ }
+ }
}
// Update transmission stats