mirror of
https://github.com/ivuorinen/gibidify.git
synced 2026-01-26 03:24:05 +00:00
* build: update Go 1.25, CI workflows, and build tooling - Upgrade to Go 1.25 - Add benchmark targets to Makefile - Implement parallel gosec execution - Lock tool versions for reproducibility - Add shellcheck directives to scripts - Update CI workflows with improved caching * refactor: migrate from golangci-lint to revive - Replace golangci-lint with revive for linting - Configure comprehensive revive rules - Fix all EditorConfig violations - Add yamllint and yamlfmt support - Remove deprecated .golangci.yml * refactor: rename utils to shared and deduplicate code - Rename utils package to shared - Add shared constants package - Deduplicate constants across packages - Address CodeRabbit review feedback * fix: resolve SonarQube issues and add safety guards - Fix all 73 SonarQube OPEN issues - Add nil guards for resourceMonitor, backpressure, metricsCollector - Implement io.Closer for headerFileReader - Propagate errors from processing helpers - Add metrics and templates packages - Improve error handling across codebase * test: improve test infrastructure and coverage - Add benchmarks for cli, fileproc, metrics - Improve test coverage for cli, fileproc, config - Refactor tests with helper functions - Add shared test constants - Fix test function naming conventions - Reduce cognitive complexity in benchmark tests * docs: update documentation and configuration examples - Update CLAUDE.md with current project state - Refresh README with new features - Add usage and configuration examples - Add SonarQube project configuration - Consolidate config.example.yaml * fix: resolve shellcheck warnings in scripts - Use ./*.go instead of *.go to prevent dash-prefixed filenames from being interpreted as options (SC2035) - Remove unreachable return statement after exit (SC2317) - Remove obsolete gibidiutils/ directory reference * chore(deps): upgrade go dependencies * chore(lint): megalinter fixes * fix: improve test coverage and fix file descriptor leaks - Add defer r.Close() to fix pipe file descriptor leaks in benchmark tests - Refactor TestProcessorConfigureFileTypes with helper functions and assertions - Refactor TestProcessorLogFinalStats with output capture and keyword verification - Use shared constants instead of literal strings (TestFilePNG, FormatMarkdown, etc.) - Reduce cognitive complexity by extracting helper functions * fix: align test comments with function names Remove underscores from test comments to match actual function names: - benchmark/benchmark_test.go (2 fixes) - fileproc/filetypes_config_test.go (4 fixes) - fileproc/filetypes_registry_test.go (6 fixes) - fileproc/processor_test.go (6 fixes) - fileproc/resource_monitor_types_test.go (4 fixes) - fileproc/writer_test.go (3 fixes) * fix: various test improvements and bug fixes - Remove duplicate maxCacheSize check in filetypes_registry_test.go - Shorten long comment in processor_test.go to stay under 120 chars - Remove flaky time.Sleep in collector_test.go, use >= 0 assertion - Close pipe reader in benchmark_test.go to fix file descriptor leak - Use ContinueOnError in flags_test.go to match ResetFlags behavior - Add nil check for p.ui in processor_workers.go before UpdateProgress - Fix resource_monitor_validation_test.go by setting hardMemoryLimitBytes directly * chore(yaml): add missing document start markers Add --- document start to YAML files to satisfy yamllint: - .github/workflows/codeql.yml - .github/workflows/build-test-publish.yml - .github/workflows/security.yml - .github/actions/setup/action.yml * fix: guard nil resourceMonitor and fix test deadlock - Guard resourceMonitor before CreateFileProcessingContext call - Add ui.UpdateProgress on emergency stop and path error returns - Fix potential deadlock in TestProcessFile using wg.Go with defer close
1039 lines
25 KiB
Go
1039 lines
25 KiB
Go
package shared
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// Mock test objects - local to avoid import cycles.
|
|
|
|
// mockCloser implements io.ReadCloser with configurable close error.
|
|
type mockCloser struct {
|
|
closeError error
|
|
closed bool
|
|
}
|
|
|
|
func (m *mockCloser) Read(_ []byte) (n int, err error) {
|
|
return 0, io.EOF
|
|
}
|
|
|
|
func (m *mockCloser) Close() error {
|
|
m.closed = true
|
|
|
|
return m.closeError
|
|
}
|
|
|
|
// mockReader implements io.Reader that returns EOF.
|
|
type mockReader struct{}
|
|
|
|
func (m *mockReader) Read(_ []byte) (n int, err error) {
|
|
return 0, io.EOF
|
|
}
|
|
|
|
// mockWriter implements io.Writer with configurable write error.
|
|
type mockWriter struct {
|
|
writeError error
|
|
written []byte
|
|
}
|
|
|
|
func (m *mockWriter) Write(p []byte) (n int, err error) {
|
|
if m.writeError != nil {
|
|
return 0, m.writeError
|
|
}
|
|
m.written = append(m.written, p...)
|
|
|
|
return len(p), nil
|
|
}
|
|
|
|
func TestSafeCloseReader(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
reader io.Reader
|
|
path string
|
|
expectClosed bool
|
|
expectError bool
|
|
closeError error
|
|
}{
|
|
{
|
|
name: "closer reader success",
|
|
reader: &mockCloser{},
|
|
path: TestPathBase,
|
|
expectClosed: true,
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "closer reader with error",
|
|
reader: &mockCloser{closeError: errors.New("close failed")},
|
|
path: TestPathBase,
|
|
expectClosed: true,
|
|
expectError: true,
|
|
closeError: errors.New("close failed"),
|
|
},
|
|
{
|
|
name: "non-closer reader",
|
|
reader: &mockReader{},
|
|
path: TestPathBase,
|
|
expectClosed: false,
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "closer reader with empty path",
|
|
reader: &mockCloser{},
|
|
path: "",
|
|
expectClosed: true,
|
|
expectError: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(
|
|
tt.name, func(t *testing.T) {
|
|
// Capture the reader if it's a mockCloser
|
|
var closerMock *mockCloser
|
|
if closer, ok := tt.reader.(*mockCloser); ok {
|
|
closerMock = closer
|
|
}
|
|
|
|
// Call SafeCloseReader (should not panic)
|
|
SafeCloseReader(tt.reader, tt.path)
|
|
|
|
// Verify expectations
|
|
if closerMock != nil {
|
|
if closerMock.closed != tt.expectClosed {
|
|
t.Errorf("Expected closed=%v, got %v", tt.expectClosed, closerMock.closed)
|
|
}
|
|
}
|
|
// Note: Error logging is tested indirectly through no panic
|
|
},
|
|
)
|
|
}
|
|
}
|
|
|
|
// validateWriteError validates error expectations for write operations.
|
|
func validateWriteError(t *testing.T, err error, errContains, filePath string) {
|
|
t.Helper()
|
|
|
|
if err == nil {
|
|
t.Error("Expected error, got nil")
|
|
|
|
return
|
|
}
|
|
|
|
if errContains != "" && !strings.Contains(err.Error(), errContains) {
|
|
t.Errorf("Error should contain %q, got: %v", errContains, err.Error())
|
|
}
|
|
|
|
var structErr *StructuredError
|
|
if !errors.As(err, &structErr) {
|
|
t.Error("Expected StructuredError")
|
|
|
|
return
|
|
}
|
|
|
|
if structErr.Type != ErrorTypeIO {
|
|
t.Errorf(TestFmtExpectedErrorTypeIO, structErr.Type)
|
|
}
|
|
if structErr.Code != CodeIOWrite {
|
|
t.Errorf("Expected CodeIOWrite, got %v", structErr.Code)
|
|
}
|
|
if filePath != "" && structErr.FilePath != filePath {
|
|
t.Errorf(TestFmtExpectedFilePath, filePath, structErr.FilePath)
|
|
}
|
|
}
|
|
|
|
func TestWriteWithErrorWrap(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
content string
|
|
errorMsg string
|
|
filePath string
|
|
writeError error
|
|
wantErr bool
|
|
errContains string
|
|
}{
|
|
{
|
|
name: "successful write",
|
|
content: TestContentTest,
|
|
errorMsg: "write failed",
|
|
filePath: TestPathTestFileTXT,
|
|
writeError: nil,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "write error with file path",
|
|
content: TestContentTest,
|
|
errorMsg: "custom error message",
|
|
filePath: TestPathTestFileTXT,
|
|
writeError: errors.New(TestErrDiskFull),
|
|
wantErr: true,
|
|
errContains: "custom error message",
|
|
},
|
|
{
|
|
name: "write error without file path",
|
|
content: TestContentTest,
|
|
errorMsg: "write operation failed",
|
|
filePath: "",
|
|
writeError: errors.New("network error"),
|
|
wantErr: true,
|
|
errContains: "write operation failed",
|
|
},
|
|
{
|
|
name: TestContentEmpty,
|
|
content: "",
|
|
errorMsg: "empty write",
|
|
filePath: TestPathTestEmptyTXT,
|
|
writeError: nil,
|
|
wantErr: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(
|
|
tt.name, func(t *testing.T) {
|
|
writer := &mockWriter{writeError: tt.writeError}
|
|
err := WriteWithErrorWrap(writer, tt.content, tt.errorMsg, tt.filePath)
|
|
|
|
if tt.wantErr {
|
|
validateWriteError(t, err, tt.errContains, tt.filePath)
|
|
|
|
return
|
|
}
|
|
|
|
if err != nil {
|
|
t.Errorf("WriteWithErrorWrap() unexpected error: %v", err)
|
|
}
|
|
if string(writer.written) != tt.content {
|
|
t.Errorf(TestFmtExpectedContent, tt.content, string(writer.written))
|
|
}
|
|
},
|
|
)
|
|
}
|
|
}
|
|
|
|
// validateStreamError validates error expectations for stream operations.
|
|
func validateStreamError(t *testing.T, err error, errContains, filePath string) {
|
|
t.Helper()
|
|
|
|
if err == nil {
|
|
t.Error("Expected error, got nil")
|
|
|
|
return
|
|
}
|
|
|
|
if errContains != "" && !strings.Contains(err.Error(), errContains) {
|
|
t.Errorf("Error should contain %q, got: %v", errContains, err.Error())
|
|
}
|
|
|
|
var structErr *StructuredError
|
|
if !errors.As(err, &structErr) {
|
|
return
|
|
}
|
|
|
|
if structErr.Type != ErrorTypeIO {
|
|
t.Errorf(TestFmtExpectedErrorTypeIO, structErr.Type)
|
|
}
|
|
if filePath != "" && structErr.FilePath != filePath {
|
|
t.Errorf(TestFmtExpectedFilePath, filePath, structErr.FilePath)
|
|
}
|
|
}
|
|
|
|
func TestStreamContent(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
content string
|
|
chunkSize int
|
|
filePath string
|
|
writeError error
|
|
processChunk func([]byte) []byte
|
|
wantErr bool
|
|
expectedContent string
|
|
errContains string
|
|
}{
|
|
{
|
|
name: "successful streaming",
|
|
content: "hello world test content",
|
|
chunkSize: 8,
|
|
filePath: TestPathTestFileTXT,
|
|
expectedContent: "hello world test content",
|
|
},
|
|
{
|
|
name: "streaming with chunk processor",
|
|
content: "abc def ghi",
|
|
chunkSize: 4,
|
|
filePath: TestPathTestFileTXT,
|
|
processChunk: bytes.ToUpper,
|
|
expectedContent: "ABC DEF GHI",
|
|
},
|
|
{
|
|
name: "write error during streaming",
|
|
content: TestContentTest,
|
|
chunkSize: 4,
|
|
filePath: TestPathTestFileTXT,
|
|
writeError: errors.New(TestErrDiskFull),
|
|
wantErr: true,
|
|
errContains: "failed to write content chunk",
|
|
},
|
|
{
|
|
name: TestContentEmpty,
|
|
content: "",
|
|
chunkSize: 1024,
|
|
filePath: TestPathTestEmptyTXT,
|
|
expectedContent: "",
|
|
},
|
|
{
|
|
name: "large chunk size",
|
|
content: "small content",
|
|
chunkSize: 1024,
|
|
filePath: TestPathTestFileTXT,
|
|
expectedContent: "small content",
|
|
},
|
|
{
|
|
name: "nil processor function",
|
|
content: "unchanged content",
|
|
chunkSize: 8,
|
|
filePath: TestPathTestFileTXT,
|
|
processChunk: nil,
|
|
expectedContent: "unchanged content",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(
|
|
tt.name, func(t *testing.T) {
|
|
reader := strings.NewReader(tt.content)
|
|
writer := &mockWriter{writeError: tt.writeError}
|
|
err := StreamContent(reader, writer, tt.chunkSize, tt.filePath, tt.processChunk)
|
|
|
|
if tt.wantErr {
|
|
validateStreamError(t, err, tt.errContains, tt.filePath)
|
|
|
|
return
|
|
}
|
|
|
|
if err != nil {
|
|
t.Errorf("StreamContent() unexpected error: %v", err)
|
|
}
|
|
if string(writer.written) != tt.expectedContent {
|
|
t.Errorf(TestFmtExpectedContent, tt.expectedContent, string(writer.written))
|
|
}
|
|
},
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestEscapeForJSON(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
expected string
|
|
}{
|
|
{
|
|
name: "simple string",
|
|
input: TestContentHelloWorld,
|
|
expected: TestContentHelloWorld,
|
|
},
|
|
{
|
|
name: "string with quotes",
|
|
input: `hello "quoted" world`,
|
|
expected: `hello \"quoted\" world`,
|
|
},
|
|
{
|
|
name: "string with newlines",
|
|
input: "line 1\nline 2\nline 3",
|
|
expected: "line 1\\nline 2\\nline 3",
|
|
},
|
|
{
|
|
name: "string with tabs",
|
|
input: "col1\tcol2\tcol3",
|
|
expected: "col1\\tcol2\\tcol3",
|
|
},
|
|
{
|
|
name: "string with backslashes",
|
|
input: `path\to\file`,
|
|
expected: `path\\to\\file`,
|
|
},
|
|
{
|
|
name: "string with unicode",
|
|
input: "Hello 世界 🌍",
|
|
expected: "Hello 世界 🌍",
|
|
},
|
|
{
|
|
name: "empty string",
|
|
input: "",
|
|
expected: "",
|
|
},
|
|
{
|
|
name: "control characters",
|
|
input: "\x00\x01\x1f",
|
|
expected: "\\u0000\\u0001\\u001f",
|
|
},
|
|
{
|
|
name: "mixed special characters",
|
|
input: "Line 1\n\t\"Quoted\"\r\nLine 2\\",
|
|
expected: "Line 1\\n\\t\\\"Quoted\\\"\\r\\nLine 2\\\\",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(
|
|
tt.name, func(t *testing.T) {
|
|
result := EscapeForJSON(tt.input)
|
|
if result != tt.expected {
|
|
t.Errorf("EscapeForJSON() = %q, want %q", result, tt.expected)
|
|
}
|
|
},
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestEscapeForYAML(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
expected string
|
|
}{
|
|
{
|
|
name: "simple string no quotes needed",
|
|
input: "hello",
|
|
expected: "hello",
|
|
},
|
|
{
|
|
name: "string with spaces needs quotes",
|
|
input: TestContentHelloWorld,
|
|
expected: `"hello world"`,
|
|
},
|
|
{
|
|
name: "string with colon needs quotes",
|
|
input: "key:value",
|
|
expected: `"key:value"`,
|
|
},
|
|
{
|
|
name: "string starting with dash",
|
|
input: "-value",
|
|
expected: `"-value"`,
|
|
},
|
|
{
|
|
name: "string starting with question mark",
|
|
input: "?value",
|
|
expected: `"?value"`,
|
|
},
|
|
{
|
|
name: "string starting with colon",
|
|
input: ":value",
|
|
expected: `":value"`,
|
|
},
|
|
{
|
|
name: "boolean true",
|
|
input: "true",
|
|
expected: `"true"`,
|
|
},
|
|
{
|
|
name: "boolean false",
|
|
input: "false",
|
|
expected: `"false"`,
|
|
},
|
|
{
|
|
name: "null value",
|
|
input: "null",
|
|
expected: `"null"`,
|
|
},
|
|
{
|
|
name: "tilde null",
|
|
input: "~",
|
|
expected: `"~"`,
|
|
},
|
|
{
|
|
name: "empty string",
|
|
input: "",
|
|
expected: `""`,
|
|
},
|
|
{
|
|
name: "string with newlines",
|
|
input: "line1\nline2",
|
|
expected: "\"line1\nline2\"",
|
|
},
|
|
{
|
|
name: "string with tabs",
|
|
input: "col1\tcol2",
|
|
expected: "\"col1\tcol2\"",
|
|
},
|
|
{
|
|
name: "string with brackets",
|
|
input: "[list]",
|
|
expected: `"[list]"`,
|
|
},
|
|
{
|
|
name: "string with braces",
|
|
input: "{object}",
|
|
expected: `"{object}"`,
|
|
},
|
|
{
|
|
name: "string with pipe",
|
|
input: "value|other",
|
|
expected: `"value|other"`,
|
|
},
|
|
{
|
|
name: "string with greater than",
|
|
input: "value>other",
|
|
expected: `"value>other"`,
|
|
},
|
|
{
|
|
name: "string with quotes and backslashes",
|
|
input: `path\to"file"`,
|
|
expected: `"path\\to\"file\""`,
|
|
},
|
|
{
|
|
name: "normal identifier",
|
|
input: "normalValue123",
|
|
expected: "normalValue123",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(
|
|
tt.name, func(t *testing.T) {
|
|
result := EscapeForYAML(tt.input)
|
|
if result != tt.expected {
|
|
t.Errorf("EscapeForYAML() = %q, want %q", result, tt.expected)
|
|
}
|
|
},
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestStreamLines(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
content string
|
|
filePath string
|
|
readError bool
|
|
writeError error
|
|
lineProcessor func(string) string
|
|
wantErr bool
|
|
expectedContent string
|
|
errContains string
|
|
}{
|
|
{
|
|
name: "successful line streaming",
|
|
content: "line1\nline2\nline3",
|
|
filePath: TestPathTestFileTXT,
|
|
expectedContent: "line1\nline2\nline3\n",
|
|
},
|
|
{
|
|
name: "line streaming with processor",
|
|
content: "hello\nworld",
|
|
filePath: TestPathTestFileTXT,
|
|
lineProcessor: strings.ToUpper,
|
|
expectedContent: "HELLO\nWORLD\n",
|
|
},
|
|
{
|
|
name: TestContentEmpty,
|
|
content: "",
|
|
filePath: TestPathTestEmptyTXT,
|
|
expectedContent: "",
|
|
},
|
|
{
|
|
name: "single line no newline",
|
|
content: "single line",
|
|
filePath: TestPathTestFileTXT,
|
|
expectedContent: "single line\n",
|
|
},
|
|
{
|
|
name: "content ending with newline",
|
|
content: "line1\nline2\n",
|
|
filePath: TestPathTestFileTXT,
|
|
expectedContent: "line1\nline2\n",
|
|
},
|
|
{
|
|
name: "write error during processing",
|
|
content: "line1\nline2",
|
|
filePath: TestPathTestFileTXT,
|
|
writeError: errors.New(TestErrDiskFull),
|
|
wantErr: true,
|
|
errContains: "failed to write processed line",
|
|
},
|
|
{
|
|
name: "nil line processor",
|
|
content: "unchanged\ncontent",
|
|
filePath: TestPathTestFileTXT,
|
|
lineProcessor: nil,
|
|
expectedContent: "unchanged\ncontent\n",
|
|
},
|
|
{
|
|
name: "multiple empty lines",
|
|
content: "\n\n\n",
|
|
filePath: TestPathTestFileTXT,
|
|
expectedContent: "\n\n\n",
|
|
},
|
|
{
|
|
name: "line processor with special characters",
|
|
content: "hello\t world\ntest\rline",
|
|
filePath: TestPathTestFileTXT,
|
|
lineProcessor: func(line string) string {
|
|
return strings.ReplaceAll(line, "\t", " ")
|
|
},
|
|
expectedContent: "hello world\ntest\rline\n",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(
|
|
tt.name, func(t *testing.T) {
|
|
reader := strings.NewReader(tt.content)
|
|
writer := &mockWriter{writeError: tt.writeError}
|
|
err := StreamLines(reader, writer, tt.filePath, tt.lineProcessor)
|
|
|
|
if tt.wantErr {
|
|
validateStreamError(t, err, tt.errContains, tt.filePath)
|
|
|
|
return
|
|
}
|
|
|
|
if err != nil {
|
|
t.Errorf("StreamLines() unexpected error: %v", err)
|
|
}
|
|
if string(writer.written) != tt.expectedContent {
|
|
t.Errorf(TestFmtExpectedContent, tt.expectedContent, string(writer.written))
|
|
}
|
|
},
|
|
)
|
|
}
|
|
}
|
|
|
|
// validateWriteProcessedChunkResult validates the result of writeProcessedChunk operation.
|
|
func validateWriteProcessedChunkResult(t *testing.T, writer *mockWriter, err error, wantErr bool, expected string) {
|
|
t.Helper()
|
|
|
|
if wantErr {
|
|
if err == nil {
|
|
t.Error("writeProcessedChunk() expected error, got nil")
|
|
}
|
|
return
|
|
}
|
|
|
|
if err != nil {
|
|
t.Errorf("writeProcessedChunk() unexpected error: %v", err)
|
|
return
|
|
}
|
|
|
|
if string(writer.written) != expected {
|
|
t.Errorf("Expected %q, got %q", expected, string(writer.written))
|
|
}
|
|
}
|
|
|
|
// Test helper functions indirectly through their usage.
|
|
func TestWriteProcessedChunk(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
chunk []byte
|
|
filePath string
|
|
processChunk func([]byte) []byte
|
|
writeError error
|
|
wantErr bool
|
|
expected string
|
|
}{
|
|
{
|
|
name: "successful chunk processing",
|
|
chunk: []byte("hello"),
|
|
filePath: TestPathTestFileTXT,
|
|
processChunk: bytes.ToUpper,
|
|
expected: "HELLO",
|
|
},
|
|
{
|
|
name: "no processor",
|
|
chunk: []byte("unchanged"),
|
|
filePath: TestPathTestFileTXT,
|
|
processChunk: nil,
|
|
expected: "unchanged",
|
|
},
|
|
{
|
|
name: "write error",
|
|
chunk: []byte("test"),
|
|
filePath: TestPathTestFileTXT,
|
|
writeError: errors.New("write failed"),
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(
|
|
tt.name, func(t *testing.T) {
|
|
writer := &mockWriter{writeError: tt.writeError}
|
|
err := writeProcessedChunk(writer, tt.chunk, tt.filePath, tt.processChunk)
|
|
validateWriteProcessedChunkResult(t, writer, err, tt.wantErr, tt.expected)
|
|
},
|
|
)
|
|
}
|
|
}
|
|
|
|
// testWrapErrorFunc is a helper function to test error wrapping functions without duplication.
|
|
func testWrapErrorFunc(
|
|
t *testing.T,
|
|
wrapFunc func(error, string) error,
|
|
expectedCode string,
|
|
expectedMessage string,
|
|
testName string,
|
|
) {
|
|
t.Helper()
|
|
|
|
originalErr := errors.New("original " + testName + " error")
|
|
filePath := TestPathTestFileTXT
|
|
|
|
wrappedErr := wrapFunc(originalErr, filePath)
|
|
|
|
// Should return a StructuredError
|
|
var structErr *StructuredError
|
|
if !errors.As(wrappedErr, &structErr) {
|
|
t.Fatal("Expected StructuredError")
|
|
}
|
|
|
|
// Verify error properties
|
|
if structErr.Type != ErrorTypeIO {
|
|
t.Errorf(TestFmtExpectedErrorTypeIO, structErr.Type)
|
|
}
|
|
if structErr.Code != expectedCode {
|
|
t.Errorf("Expected %v, got %v", expectedCode, structErr.Code)
|
|
}
|
|
if structErr.FilePath != filePath {
|
|
t.Errorf(TestFmtExpectedFilePath, filePath, structErr.FilePath)
|
|
}
|
|
if structErr.Message != expectedMessage {
|
|
t.Errorf("Expected message %q, got %q", expectedMessage, structErr.Message)
|
|
}
|
|
|
|
// Test with empty file path
|
|
wrappedErrEmpty := wrapFunc(originalErr, "")
|
|
var structErrEmpty *StructuredError
|
|
if errors.As(wrappedErrEmpty, &structErrEmpty) && structErrEmpty.FilePath != "" {
|
|
t.Errorf("Expected empty FilePath, got %q", structErrEmpty.FilePath)
|
|
}
|
|
}
|
|
|
|
func TestWrapWriteError(t *testing.T) {
|
|
testWrapErrorFunc(t, wrapWriteError, CodeIOWrite, "failed to write content chunk", "write")
|
|
}
|
|
|
|
func TestWrapReadError(t *testing.T) {
|
|
testWrapErrorFunc(t, wrapReadError, CodeIORead, "failed to read content chunk", "read")
|
|
}
|
|
|
|
// Benchmark tests for performance-critical functions.
|
|
func BenchmarkEscapeForJSON(b *testing.B) {
|
|
testString := `This is a "test string" with various characters: \n\t\r and some unicode: 世界`
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
_ = EscapeForJSON(testString)
|
|
}
|
|
}
|
|
|
|
func BenchmarkEscapeForYAML(b *testing.B) {
|
|
testString := `This is a test string with: spaces, "quotes", and special chars -?:`
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
_ = EscapeForYAML(testString)
|
|
}
|
|
}
|
|
|
|
func BenchmarkStreamContent(b *testing.B) {
|
|
content := strings.Repeat("This is test content that will be streamed in chunks.\n", 1000)
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
reader := strings.NewReader(content)
|
|
writer := &bytes.Buffer{}
|
|
_ = StreamContent(reader, writer, 1024, TestPathTestFileTXT, nil) // nolint:errcheck // benchmark test
|
|
}
|
|
}
|
|
|
|
// Integration test.
|
|
func TestWriterIntegration(t *testing.T) {
|
|
// Test a complete workflow using multiple writer utilities
|
|
content := `Line 1 with "quotes"
|
|
Line 2 with special chars: {}[]
|
|
Line 3 with unicode: 世界`
|
|
|
|
// Test JSON escaping in content
|
|
var jsonBuf bytes.Buffer
|
|
processedContent := EscapeForJSON(content)
|
|
err := WriteWithErrorWrap(
|
|
&jsonBuf,
|
|
fmt.Sprintf(`{"content":"%s"}`, processedContent),
|
|
"JSON write failed",
|
|
"/test/file.json",
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("JSON integration failed: %v", err)
|
|
}
|
|
|
|
// Test YAML escaping and line streaming
|
|
var yamlBuf bytes.Buffer
|
|
reader := strings.NewReader(content)
|
|
err = StreamLines(
|
|
reader, &yamlBuf, "/test/file.yaml", func(line string) string {
|
|
return "content: " + EscapeForYAML(line)
|
|
},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("YAML integration failed: %v", err)
|
|
}
|
|
|
|
// Verify both outputs contain expected content
|
|
jsonOutput := jsonBuf.String()
|
|
yamlOutput := yamlBuf.String()
|
|
|
|
if !strings.Contains(jsonOutput, `\"quotes\"`) {
|
|
t.Error("JSON output should contain escaped quotes")
|
|
}
|
|
if !strings.Contains(yamlOutput, `"Line 2 with special chars: {}[]"`) {
|
|
t.Error("YAML output should contain quoted special characters line")
|
|
}
|
|
}
|
|
|
|
// TestCheckContextCancellation tests the CheckContextCancellation function.
|
|
func TestCheckContextCancellation(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setupContext func() context.Context
|
|
operation string
|
|
expectError bool
|
|
errorContains string
|
|
}{
|
|
{
|
|
name: "active context",
|
|
setupContext: func() context.Context {
|
|
return context.Background()
|
|
},
|
|
operation: "test operation",
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "canceled context",
|
|
setupContext: func() context.Context {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel() // Cancel immediately
|
|
return ctx
|
|
},
|
|
operation: "test operation",
|
|
expectError: true,
|
|
errorContains: "test operation canceled",
|
|
},
|
|
{
|
|
name: "timeout context",
|
|
setupContext: func() context.Context {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Nanosecond)
|
|
defer cancel()
|
|
// Wait for timeout
|
|
time.Sleep(1 * time.Millisecond)
|
|
return ctx
|
|
},
|
|
operation: "timeout operation",
|
|
expectError: true,
|
|
errorContains: "timeout operation canceled",
|
|
},
|
|
{
|
|
name: "context with deadline exceeded",
|
|
setupContext: func() context.Context {
|
|
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(-1*time.Hour))
|
|
defer cancel()
|
|
return ctx
|
|
},
|
|
operation: "deadline operation",
|
|
expectError: true,
|
|
errorContains: "deadline operation canceled",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ctx := tt.setupContext()
|
|
err := CheckContextCancellation(ctx, tt.operation)
|
|
|
|
if tt.expectError {
|
|
if err == nil {
|
|
t.Errorf("Expected error for %s, but got none", tt.name)
|
|
} else if !strings.Contains(err.Error(), tt.errorContains) {
|
|
t.Errorf("Error %q should contain %q", err.Error(), tt.errorContains)
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Errorf("Unexpected error for %s: %v", tt.name, err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestWithContextCheck tests the WithContextCheck function.
|
|
func TestWithContextCheck(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setupContext func() context.Context
|
|
operation string
|
|
fn func() error
|
|
expectError bool
|
|
errorContains string
|
|
}{
|
|
{
|
|
name: "active context with successful operation",
|
|
setupContext: func() context.Context {
|
|
return context.Background()
|
|
},
|
|
operation: "successful operation",
|
|
fn: func() error {
|
|
return nil
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "active context with failing operation",
|
|
setupContext: func() context.Context {
|
|
return context.Background()
|
|
},
|
|
operation: "failing operation",
|
|
fn: func() error {
|
|
return errors.New("operation failed")
|
|
},
|
|
expectError: true,
|
|
errorContains: "operation failed",
|
|
},
|
|
{
|
|
name: "canceled context before operation",
|
|
setupContext: func() context.Context {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel() // Cancel immediately
|
|
return ctx
|
|
},
|
|
operation: "canceled operation",
|
|
fn: func() error {
|
|
t.Error("Function should not be called with canceled context")
|
|
return nil
|
|
},
|
|
expectError: true,
|
|
errorContains: "canceled operation canceled",
|
|
},
|
|
{
|
|
name: "timeout context before operation",
|
|
setupContext: func() context.Context {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Nanosecond)
|
|
defer cancel()
|
|
// Wait for timeout
|
|
time.Sleep(1 * time.Millisecond)
|
|
return ctx
|
|
},
|
|
operation: "timeout operation",
|
|
fn: func() error {
|
|
t.Error("Function should not be called with timed out context")
|
|
return nil
|
|
},
|
|
expectError: true,
|
|
errorContains: "timeout operation canceled",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ctx := tt.setupContext()
|
|
err := WithContextCheck(ctx, tt.operation, tt.fn)
|
|
|
|
if tt.expectError {
|
|
if err == nil {
|
|
t.Errorf("Expected error for %s, but got none", tt.name)
|
|
} else if !strings.Contains(err.Error(), tt.errorContains) {
|
|
t.Errorf("Error %q should contain %q", err.Error(), tt.errorContains)
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Errorf("Unexpected error for %s: %v", tt.name, err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// assertNoError is a helper that fails the test if err is not nil.
|
|
func assertNoError(t *testing.T, err error, msg string) {
|
|
t.Helper()
|
|
if err != nil {
|
|
t.Errorf("%s: %v", msg, err)
|
|
}
|
|
}
|
|
|
|
// assertError is a helper that fails the test if err is nil.
|
|
func assertError(t *testing.T, err error, msg string) {
|
|
t.Helper()
|
|
if err == nil {
|
|
t.Error(msg)
|
|
}
|
|
}
|
|
|
|
// TestContextCancellationIntegration tests integration scenarios.
|
|
func TestContextCancellationIntegration(t *testing.T) {
|
|
t.Run("multiple operations with context check", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
// First operation should succeed
|
|
err := WithContextCheck(ctx, "operation 1", func() error {
|
|
return nil
|
|
})
|
|
assertNoError(t, err, "First operation failed")
|
|
|
|
// Second operation should also succeed
|
|
err = WithContextCheck(ctx, "operation 2", func() error {
|
|
return nil
|
|
})
|
|
assertNoError(t, err, "Second operation failed")
|
|
})
|
|
|
|
t.Run("chained context checks", func(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
// First check should pass
|
|
err := CheckContextCancellation(ctx, "first check")
|
|
assertNoError(t, err, "First check should pass")
|
|
|
|
// Cancel context
|
|
cancel()
|
|
|
|
// Second check should fail
|
|
err = CheckContextCancellation(ctx, "second check")
|
|
assertError(t, err, "Second check should fail after cancellation")
|
|
|
|
// Third operation should also fail
|
|
err = WithContextCheck(ctx, "third operation", func() error {
|
|
t.Error("Function should not be called")
|
|
return nil
|
|
})
|
|
assertError(t, err, "Third operation should fail after cancellation")
|
|
})
|
|
|
|
t.Run("context cancellation propagation", func(t *testing.T) {
|
|
// Test that context cancellation properly propagates through nested calls
|
|
parentCtx, parentCancel := context.WithCancel(context.Background())
|
|
childCtx, childCancel := context.WithCancel(parentCtx)
|
|
|
|
defer parentCancel()
|
|
defer childCancel()
|
|
|
|
// Both contexts should be active initially
|
|
err := CheckContextCancellation(parentCtx, "parent")
|
|
assertNoError(t, err, "Parent context should be active")
|
|
|
|
err = CheckContextCancellation(childCtx, "child")
|
|
assertNoError(t, err, "Child context should be active")
|
|
|
|
// Cancel parent - child should also be canceled
|
|
parentCancel()
|
|
|
|
err = CheckContextCancellation(childCtx, "child after parent cancel")
|
|
assertError(t, err, "Child context should be canceled when parent is canceled")
|
|
})
|
|
}
|