diff options
| author | Paul Buetow <paul@buetow.org> | 2025-06-20 09:34:26 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2025-06-20 09:34:26 +0300 |
| commit | ecc66fcf9241c429cf2378f09793ecef3a00b098 (patch) | |
| tree | 4e671982bec11a6d12a1374b56bae2b89f0f4287 | |
| parent | c24e18b68b29384d2f63d44bfcbc9c02423edf78 (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.go | 147 | ||||
| -rw-r--r-- | integrationtests/dgrep_line_endings_test.go | 155 | ||||
| -rw-r--r-- | internal/io/fs/directprocessor.go | 10 |
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 |
