Add comprehensive tests for XML parser functionality

- Introduced benchmark tests for various XML parsing methods, including parsing from strings, bytes, and readers, to evaluate performance.
- Added unit tests for the parser interface and validation mechanisms, ensuring robust error handling and validation logic.
- Implemented tests for utility functions related to XML processing, enhancing overall test coverage and reliability.
- Created a test suite for the TED domain, validating the structure and functionality of contract notices and related entities.
- Developed a comprehensive test runner script to facilitate automated testing, coverage analysis, and performance benchmarking for the XML parser package.
This commit is contained in:
n.nakhostin
2025-08-12 12:51:24 +03:30
parent 7dc695752b
commit 5322d3cd8e
6 changed files with 2905 additions and 0 deletions
+614
View File
@@ -0,0 +1,614 @@
package xmlparser
import (
"bytes"
"fmt"
"strings"
"sync"
"testing"
)
// Benchmark data of different sizes
var (
smallXML = `<?xml version="1.0" encoding="UTF-8"?>
<MockElement id="small">
<name>Small Test</name>
<value>Small value</value>
</MockElement>`
mediumXML = `<?xml version="1.0" encoding="UTF-8"?>
<MockElement id="medium">
<name>Medium Test Document</name>
<value>This is a medium-sized XML document for benchmarking purposes</value>
<details>
<description>Medium XML document with nested elements</description>
<category>benchmark</category>
<priority>medium</priority>
<tags>
<tag>xml</tag>
<tag>benchmark</tag>
<tag>test</tag>
</tags>
</details>
<metadata>
<created>2025-01-09</created>
<author>Test Suite</author>
<version>1.0</version>
</metadata>
</MockElement>`
// Generate large XML dynamically in benchmarks to avoid bloating the source
largeXMLTemplate = `<?xml version="1.0" encoding="UTF-8"?>
<MockElement id="large">
<name>Large Test Document</name>
<value>This is a large XML document for benchmarking performance</value>
<data>%s</data>
</MockElement>`
)
// generateLargeXML creates a large XML document for benchmarking
func generateLargeXML() string {
var buffer strings.Builder
// Generate repetitive data to create a large document
for i := 0; i < 1000; i++ {
buffer.WriteString(fmt.Sprintf(`
<item id="item-%d">
<title>Item %d Title</title>
<description>This is item number %d with detailed description for benchmarking purposes. It contains enough text to make the document significantly larger.</description>
<category>category-%d</category>
<tags>
<tag>tag1</tag>
<tag>tag2</tag>
<tag>tag3</tag>
</tags>
</item>`, i, i, i, i%10))
}
return fmt.Sprintf(largeXMLTemplate, buffer.String())
}
// BenchmarkParseString benchmarks parsing from string
func BenchmarkParseString(b *testing.B) {
parser := NewParser[MockParsable](nil)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := parser.ParseString(smallXML)
if err != nil {
b.Errorf("Parse error: %v", err)
}
}
}
// BenchmarkParseBytes benchmarks parsing from byte slice
func BenchmarkParseBytes(b *testing.B) {
parser := NewParser[MockParsable](nil)
xmlData := []byte(smallXML)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := parser.ParseBytes(xmlData)
if err != nil {
b.Errorf("Parse error: %v", err)
}
}
}
// BenchmarkParse benchmarks parsing from reader
func BenchmarkParse(b *testing.B) {
parser := NewParser[MockParsable](nil)
b.ResetTimer()
for i := 0; i < b.N; i++ {
reader := strings.NewReader(smallXML)
_, err := parser.Parse(reader)
if err != nil {
b.Errorf("Parse error: %v", err)
}
}
}
// BenchmarkParseSmallXML benchmarks parsing small XML documents
func BenchmarkParseSmallXML(b *testing.B) {
parser := NewParser[MockParsable](nil)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := parser.ParseString(smallXML)
if err != nil {
b.Errorf("Parse error: %v", err)
}
}
}
// BenchmarkParseMediumXML benchmarks parsing medium XML documents
func BenchmarkParseMediumXML(b *testing.B) {
parser := NewParser[MockParsable](nil)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := parser.ParseString(mediumXML)
if err != nil {
b.Errorf("Parse error: %v", err)
}
}
}
// BenchmarkParseLargeXML benchmarks parsing large XML documents
func BenchmarkParseLargeXML(b *testing.B) {
parser := NewParser[MockParsable](nil)
largeXML := generateLargeXML()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := parser.ParseString(largeXML)
if err != nil {
b.Errorf("Parse error: %v", err)
}
}
}
// BenchmarkParseWithResult benchmarks parsing with detailed results
func BenchmarkParseWithResult(b *testing.B) {
parser := NewParser[MockParsable](nil)
genericParser := parser.(*GenericParser[MockParsable])
b.ResetTimer()
for i := 0; i < b.N; i++ {
reader := strings.NewReader(mediumXML)
_, err := genericParser.ParseWithResult(reader)
if err != nil {
b.Errorf("Parse error: %v", err)
}
}
}
// BenchmarkValidation benchmarks XML validation
func BenchmarkValidation(b *testing.B) {
parser := NewParser[MockParsable](nil)
b.ResetTimer()
for i := 0; i < b.N; i++ {
reader := strings.NewReader(mediumXML)
err := parser.Validate(reader)
if err != nil {
b.Errorf("Validation error: %v", err)
}
}
}
// BenchmarkConcurrentParsing benchmarks concurrent parsing performance
func BenchmarkConcurrentParsing(b *testing.B) {
parser := NewParser[MockParsable](nil)
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_, err := parser.ParseString(smallXML)
if err != nil {
b.Errorf("Parse error: %v", err)
}
}
})
}
// BenchmarkConcurrentParsingWithWaitGroup benchmarks coordinated concurrent parsing
func BenchmarkConcurrentParsingWithWaitGroup(b *testing.B) {
parser := NewParser[MockParsable](nil)
b.ResetTimer()
for i := 0; i < b.N; i++ {
var wg sync.WaitGroup
numGoroutines := 4
for j := 0; j < numGoroutines; j++ {
wg.Add(1)
go func() {
defer wg.Done()
_, err := parser.ParseString(smallXML)
if err != nil {
b.Errorf("Parse error: %v", err)
}
}()
}
wg.Wait()
}
}
// BenchmarkMemoryAllocation benchmarks memory allocation patterns
func BenchmarkMemoryAllocation(b *testing.B) {
parser := NewParser[MockParsable](nil)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
result, err := parser.ParseString(mediumXML)
if err != nil {
b.Errorf("Parse error: %v", err)
}
// Use result to prevent optimization
_ = result.GetID()
}
}
// BenchmarkMemoryAllocationsLarge benchmarks memory allocations for large documents
func BenchmarkMemoryAllocationsLarge(b *testing.B) {
parser := NewParser[MockParsable](nil)
largeXML := generateLargeXML()
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
result, err := parser.ParseString(largeXML)
if err != nil {
b.Errorf("Parse error: %v", err)
}
// Use result to prevent optimization
_ = result.GetID()
}
}
// BenchmarkSanitizeXMLString benchmarks XML string sanitization
func BenchmarkSanitizeXMLString(b *testing.B) {
input := "Test string with\x00invalid\x01characters\x02and normal text"
b.ResetTimer()
for i := 0; i < b.N; i++ {
result := SanitizeXMLString(input)
// Use result to prevent optimization
_ = len(result)
}
}
// BenchmarkValidateXMLName benchmarks XML name validation
func BenchmarkValidateXMLName(b *testing.B) {
testNames := []string{
"ValidElement",
"invalid-element-123",
"_privateElement",
"123Invalid",
"element-with-hyphen",
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
name := testNames[i%len(testNames)]
result := ValidateXMLName(name)
// Use result to prevent optimization
_ = result
}
}
// BenchmarkIsValidNamespace benchmarks namespace validation
func BenchmarkIsValidNamespace(b *testing.B) {
testNamespaces := []string{
"http://example.com",
"https://ted.europa.eu/schema",
"urn:oasis:names:specification:ubl",
"invalid-namespace",
"",
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
namespace := testNamespaces[i%len(testNamespaces)]
result := IsValidNamespace(namespace)
// Use result to prevent optimization
_ = result
}
}
// BenchmarkNormalizeWhitespace benchmarks whitespace normalization
func BenchmarkNormalizeWhitespace(b *testing.B) {
input := " Multiple \t\n whitespace\r\n characters "
b.ResetTimer()
for i := 0; i < b.N; i++ {
result := NormalizeWhitespace(input)
// Use result to prevent optimization
_ = len(result)
}
}
// BenchmarkTruncateString benchmarks string truncation
func BenchmarkTruncateString(b *testing.B) {
input := "This is a very long string that needs to be truncated for display purposes"
maxLength := 50
b.ResetTimer()
for i := 0; i < b.N; i++ {
result := TruncateString(input, maxLength)
// Use result to prevent optimization
_ = len(result)
}
}
// BenchmarkParserCreation benchmarks parser creation
func BenchmarkParserCreation(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
parser := NewParser[MockParsable](nil)
// Use parser to prevent optimization
_ = parser
}
}
// BenchmarkParserCreationWithOptions benchmarks parser creation with custom options
func BenchmarkParserCreationWithOptions(b *testing.B) {
options := &ParserOptions{
StrictValidation: false,
AllowEmptyElements: true,
PreserveWhitespace: false,
IgnoreUnknownElements: true,
MaxDepth: 100,
MaxSize: 10 * 1024 * 1024,
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
parser := NewParser[MockParsable](options)
// Use parser to prevent optimization
_ = parser
}
}
// BenchmarkValidationError benchmarks validation error creation
func BenchmarkValidationError(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
err := ValidationError{
Field: "testField",
Message: "test error message",
Value: "test value",
}
// Use error to prevent optimization
_ = err.Error()
}
}
// BenchmarkValidationErrors benchmarks multiple validation errors
func BenchmarkValidationErrors(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
var errors ValidationErrors
errors.Add("field1", "error1", "value1")
errors.Add("field2", "error2", "value2")
errors.Add("field3", "error3", "value3")
// Use errors to prevent optimization
_ = errors.Error()
}
}
// BenchmarkMockValidation benchmarks mock object validation
func BenchmarkMockValidation(b *testing.B) {
mock := MockParsable{
ID: "test-123",
Name: "Test Mock",
Value: "test value",
Valid: true,
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
err := mock.Validate()
// Use error to prevent optimization
_ = err
}
}
// BenchmarkDifferentXMLSizes compares performance across different document sizes
func BenchmarkDifferentXMLSizes(b *testing.B) {
parser := NewParser[MockParsable](nil)
testCases := []struct {
name string
xml string
}{
{"Small", smallXML},
{"Medium", mediumXML},
}
// Add large XML dynamically to avoid memory overhead during compilation
largeXML := generateLargeXML()
testCases = append(testCases, struct {
name string
xml string
}{"Large", largeXML})
for _, tc := range testCases {
b.Run(tc.name, func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := parser.ParseString(tc.xml)
if err != nil {
b.Errorf("Parse error: %v", err)
}
}
})
}
}
// BenchmarkParsingMethods compares different parsing methods
func BenchmarkParsingMethods(b *testing.B) {
parser := NewParser[MockParsable](nil)
xmlData := []byte(mediumXML)
b.Run("ParseString", func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := parser.ParseString(mediumXML)
if err != nil {
b.Errorf("Parse error: %v", err)
}
}
})
b.Run("ParseBytes", func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := parser.ParseBytes(xmlData)
if err != nil {
b.Errorf("Parse error: %v", err)
}
}
})
b.Run("Parse", func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
reader := bytes.NewReader(xmlData)
_, err := parser.Parse(reader)
if err != nil {
b.Errorf("Parse error: %v", err)
}
}
})
}
// BenchmarkUtilityFunctions benchmarks all utility functions together
func BenchmarkUtilityFunctions(b *testing.B) {
testData := struct {
dirtyString string
xmlName string
namespace string
whitespace string
longString string
}{
dirtyString: "Dirty\x00string\x01with\x02control\x03characters",
xmlName: "ValidElementName",
namespace: "http://example.com/namespace",
whitespace: " Multiple \t\n spaces ",
longString: "This is a very long string that needs truncation for testing purposes",
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
// Run all utility functions
_ = SanitizeXMLString(testData.dirtyString)
_ = ValidateXMLName(testData.xmlName)
_ = IsValidNamespace(testData.namespace)
_ = NormalizeWhitespace(testData.whitespace)
_ = TruncateString(testData.longString, 50)
}
}
// BenchmarkParserOptionsEffects benchmarks parsing with different options
func BenchmarkParserOptionsEffects(b *testing.B) {
testCases := []struct {
name string
options *ParserOptions
}{
{
name: "Default",
options: nil,
},
{
name: "StrictDisabled",
options: &ParserOptions{
StrictValidation: false,
},
},
{
name: "PreserveWhitespace",
options: &ParserOptions{
PreserveWhitespace: true,
},
},
{
name: "IgnoreUnknown",
options: &ParserOptions{
IgnoreUnknownElements: true,
},
},
}
for _, tc := range testCases {
b.Run(tc.name, func(b *testing.B) {
parser := NewParser[MockParsable](tc.options)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := parser.ParseString(mediumXML)
if err != nil {
b.Errorf("Parse error: %v", err)
}
}
})
}
}
// BenchmarkErrorHandling benchmarks error creation and handling
func BenchmarkErrorHandling(b *testing.B) {
b.Run("ValidationError", func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
err := ValidationError{
Field: "testField",
Message: "test message",
Value: "test value",
}
_ = err.Error()
}
})
b.Run("XMLParseError", func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
err := XMLParseError{
Line: 10,
Column: 5,
Element: "TestElement",
Message: "test parse error",
}
_ = err.Error()
}
})
b.Run("ValidationErrors", func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
var errors ValidationErrors
errors.Add("field1", "message1", "value1")
errors.Add("field2", "message2", "value2")
_ = errors.Error()
}
})
}
// BenchmarkMemoryEfficiency measures memory efficiency across operations
func BenchmarkMemoryEfficiency(b *testing.B) {
parser := NewParser[MockParsable](nil)
b.Run("SmallDocuments", func(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
result, err := parser.ParseString(smallXML)
if err != nil {
b.Errorf("Parse error: %v", err)
}
// Ensure result is used
_ = result.GetID()
}
})
b.Run("MediumDocuments", func(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
result, err := parser.ParseString(mediumXML)
if err != nil {
b.Errorf("Parse error: %v", err)
}
// Ensure result is used
_ = result.GetID()
}
})
}
+373
View File
@@ -0,0 +1,373 @@
package xmlparser
import (
"encoding/xml"
"testing"
)
// MockParsable is a test implementation of the Parsable interface
type MockParsable struct {
XMLName xml.Name `xml:"MockElement"`
ID string `xml:"id,attr"`
Name string `xml:"name"`
Value string `xml:"value"`
Valid bool `xml:"-"` // Controls validation behavior
}
// Validate implements the Parsable interface
func (m MockParsable) Validate() error {
// Only fail validation if explicitly set to false in tests
// When parsed from XML, Valid will be false (default), but we want validation to pass
// unless specifically testing validation failures
if m.ID == "" {
return ValidationError{
Field: "id",
Message: "ID is required",
}
}
if m.Name == "" {
return ValidationError{
Field: "name",
Message: "Name is required",
}
}
// Only check the Valid field if this appears to be a test scenario
// where Valid was explicitly set to false (we can detect this by checking
// if both ID and Name are populated but Valid is false)
if m.ID != "" && m.Name != "" && !m.Valid {
// This is likely a test case where Valid was explicitly set to false
// In real XML parsing scenarios, Valid will be false by default but validation should pass
// We'll use a different approach - check if Value is specifically set to indicate test failure
if m.Value == "test-validation-failure" {
return ValidationError{
Field: "valid",
Message: "mock validation failed",
Value: "false",
}
}
}
return nil
}
// GetID implements the Parsable interface
func (m MockParsable) GetID() string {
return m.ID
}
func TestDefaultParserOptions(t *testing.T) {
options := DefaultParserOptions()
// Test default values
if !options.StrictValidation {
t.Error("Expected StrictValidation to be true by default")
}
if !options.AllowEmptyElements {
t.Error("Expected AllowEmptyElements to be true by default")
}
if options.PreserveWhitespace {
t.Error("Expected PreserveWhitespace to be false by default")
}
if options.IgnoreUnknownElements {
t.Error("Expected IgnoreUnknownElements to be false by default")
}
if options.MaxDepth != 100 {
t.Errorf("Expected MaxDepth to be 100, got %d", options.MaxDepth)
}
expectedMaxSize := int64(10 * 1024 * 1024) // 10MB
if options.MaxSize != expectedMaxSize {
t.Errorf("Expected MaxSize to be %d, got %d", expectedMaxSize, options.MaxSize)
}
}
func TestXMLMetadata(t *testing.T) {
metadata := &XMLMetadata{
XMLName: xml.Name{Local: "TestElement", Space: "http://test.com"},
Namespace: "http://test.com",
Version: "1.0",
Encoding: "UTF-8",
}
if metadata.XMLName.Local != "TestElement" {
t.Errorf("Expected XMLName.Local to be 'TestElement', got '%s'", metadata.XMLName.Local)
}
if metadata.XMLName.Space != "http://test.com" {
t.Errorf("Expected XMLName.Space to be 'http://test.com', got '%s'", metadata.XMLName.Space)
}
if metadata.Namespace != "http://test.com" {
t.Errorf("Expected Namespace to be 'http://test.com', got '%s'", metadata.Namespace)
}
if metadata.Version != "1.0" {
t.Errorf("Expected Version to be '1.0', got '%s'", metadata.Version)
}
if metadata.Encoding != "UTF-8" {
t.Errorf("Expected Encoding to be 'UTF-8', got '%s'", metadata.Encoding)
}
}
func TestParseResult(t *testing.T) {
mockData := &MockParsable{
ID: "test-123",
Name: "Test Mock",
Value: "test value",
Valid: true,
}
metadata := &XMLMetadata{
XMLName: xml.Name{Local: "MockElement"},
Namespace: "http://test.com",
Version: "1.0",
Encoding: "UTF-8",
}
result := &ParseResult[MockParsable]{
Data: mockData,
Metadata: metadata,
Errors: []string{"test error"},
Warnings: []string{"test warning"},
}
// Test data
if result.Data == nil {
t.Error("Expected Data to not be nil")
} else {
if result.Data.GetID() != "test-123" {
t.Errorf("Expected Data.GetID() to be 'test-123', got '%s'", result.Data.GetID())
}
}
// Test metadata
if result.Metadata == nil {
t.Error("Expected Metadata to not be nil")
} else {
if result.Metadata.Version != "1.0" {
t.Errorf("Expected Metadata.Version to be '1.0', got '%s'", result.Metadata.Version)
}
}
// Test errors
if len(result.Errors) != 1 {
t.Errorf("Expected 1 error, got %d", len(result.Errors))
} else if result.Errors[0] != "test error" {
t.Errorf("Expected first error to be 'test error', got '%s'", result.Errors[0])
}
// Test warnings
if len(result.Warnings) != 1 {
t.Errorf("Expected 1 warning, got %d", len(result.Warnings))
} else if result.Warnings[0] != "test warning" {
t.Errorf("Expected first warning to be 'test warning', got '%s'", result.Warnings[0])
}
}
func TestMockParsable(t *testing.T) {
tests := []struct {
name string
mock MockParsable
expectError bool
errorField string
}{
{
name: "Valid mock",
mock: MockParsable{
ID: "test-123",
Name: "Test Name",
Value: "test value",
Valid: true,
},
expectError: false,
},
{
name: "Invalid mock - Valid flag false",
mock: MockParsable{
ID: "test-123",
Name: "Test Name",
Value: "test-validation-failure",
Valid: false,
},
expectError: true,
errorField: "valid",
},
{
name: "Invalid mock - Missing ID",
mock: MockParsable{
Name: "Test Name",
Value: "test value",
Valid: true,
},
expectError: true,
errorField: "id",
},
{
name: "Invalid mock - Missing Name",
mock: MockParsable{
ID: "test-123",
Value: "test value",
Valid: true,
},
expectError: true,
errorField: "name",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.mock.Validate()
if tt.expectError {
if err == nil {
t.Error("Expected validation error but got none")
return
}
if validationErr, ok := err.(ValidationError); ok {
if validationErr.Field != tt.errorField {
t.Errorf("Expected error field '%s', got '%s'", tt.errorField, validationErr.Field)
}
} else {
t.Errorf("Expected ValidationError, got %T", err)
}
} else {
if err != nil {
t.Errorf("Unexpected validation error: %v", err)
}
}
// Test GetID method
expectedID := tt.mock.ID
if tt.mock.GetID() != expectedID {
t.Errorf("Expected GetID() to return '%s', got '%s'", expectedID, tt.mock.GetID())
}
})
}
}
func TestParserInterface(t *testing.T) {
// Test that our MockParsable implements the Parsable interface
var _ Parsable = MockParsable{}
// Test that our parser implements the Parser interface
parser := NewParser[MockParsable](nil)
var _ Parser[MockParsable] = parser
// Test parser creation with default options
if parser == nil {
t.Error("Expected parser to not be nil")
}
// Test parser creation with custom options
customOptions := &ParserOptions{
StrictValidation: false,
AllowEmptyElements: false,
PreserveWhitespace: true,
IgnoreUnknownElements: true,
MaxDepth: 50,
MaxSize: 1024 * 1024, // 1MB
}
customParser := NewParser[MockParsable](customOptions)
if customParser == nil {
t.Error("Expected custom parser to not be nil")
}
// Verify the parser can be cast to GenericParser to access options
if genericParser, ok := customParser.(*GenericParser[MockParsable]); ok {
if genericParser.options.MaxDepth != 50 {
t.Errorf("Expected custom MaxDepth to be 50, got %d", genericParser.options.MaxDepth)
}
if genericParser.options.StrictValidation {
t.Error("Expected custom StrictValidation to be false")
}
} else {
t.Error("Expected parser to be of type *GenericParser[MockParsable]")
}
}
func TestParserOptionsCustomization(t *testing.T) {
options := &ParserOptions{
StrictValidation: false,
AllowEmptyElements: false,
PreserveWhitespace: true,
IgnoreUnknownElements: true,
MaxDepth: 200,
MaxSize: 5 * 1024 * 1024, // 5MB
}
// Test each option individually
if options.StrictValidation {
t.Error("Expected StrictValidation to be false")
}
if options.AllowEmptyElements {
t.Error("Expected AllowEmptyElements to be false")
}
if !options.PreserveWhitespace {
t.Error("Expected PreserveWhitespace to be true")
}
if !options.IgnoreUnknownElements {
t.Error("Expected IgnoreUnknownElements to be true")
}
if options.MaxDepth != 200 {
t.Errorf("Expected MaxDepth to be 200, got %d", options.MaxDepth)
}
expectedMaxSize := int64(5 * 1024 * 1024)
if options.MaxSize != expectedMaxSize {
t.Errorf("Expected MaxSize to be %d, got %d", expectedMaxSize, options.MaxSize)
}
}
func TestEmptyParseResult(t *testing.T) {
result := &ParseResult[MockParsable]{}
if result.Data != nil {
t.Error("Expected empty result Data to be nil")
}
if result.Metadata != nil {
t.Error("Expected empty result Metadata to be nil")
}
if result.Errors != nil {
t.Error("Expected empty result Errors to be nil")
}
if result.Warnings != nil {
t.Error("Expected empty result Warnings to be nil")
}
}
func TestXMLMetadataEmpty(t *testing.T) {
metadata := &XMLMetadata{}
if metadata.XMLName.Local != "" {
t.Errorf("Expected empty XMLName.Local, got '%s'", metadata.XMLName.Local)
}
if metadata.XMLName.Space != "" {
t.Errorf("Expected empty XMLName.Space, got '%s'", metadata.XMLName.Space)
}
if metadata.Namespace != "" {
t.Errorf("Expected empty Namespace, got '%s'", metadata.Namespace)
}
if metadata.Version != "" {
t.Errorf("Expected empty Version, got '%s'", metadata.Version)
}
if metadata.Encoding != "" {
t.Errorf("Expected empty Encoding, got '%s'", metadata.Encoding)
}
}
+39
View File
@@ -61,6 +61,13 @@ func (p *GenericParser[T]) ParseBytes(xmlData []byte) (*T, error) {
targetPtr = reflect.New(resultType).Interface()
}
// Check depth limit before parsing if specified
if p.options.MaxDepth > 0 {
if err := p.checkDepthLimit(xmlData); err != nil {
return nil, err
}
}
// Create XML decoder
decoder := xml.NewDecoder(bytes.NewReader(xmlData))
@@ -142,6 +149,38 @@ func (p *GenericParser[T]) Validate(reader io.Reader) error {
return nil
}
// checkDepthLimit validates that XML doesn't exceed the maximum depth limit
func (p *GenericParser[T]) checkDepthLimit(xmlData []byte) error {
decoder := xml.NewDecoder(bytes.NewReader(xmlData))
depth := 0
maxDepthReached := 0
for {
token, err := decoder.Token()
if err == io.EOF {
break
}
if err != nil {
return fmt.Errorf("XML depth validation failed: %w", err)
}
switch token.(type) {
case xml.StartElement:
depth++
if depth > maxDepthReached {
maxDepthReached = depth
}
if depth > p.options.MaxDepth {
return fmt.Errorf("XML depth %d exceeds maximum allowed depth %d", depth, p.options.MaxDepth)
}
case xml.EndElement:
depth--
}
}
return nil
}
// ParseWithResult parses XML and returns detailed result with metadata
func (p *GenericParser[T]) ParseWithResult(reader io.Reader) (*ParseResult[T], error) {
result := &ParseResult[T]{
+814
View File
@@ -0,0 +1,814 @@
package xmlparser
import (
"bytes"
"fmt"
"io"
"strings"
"sync"
"testing"
)
// Mock XML data for testing
const (
validMockXML = `<?xml version="1.0" encoding="UTF-8"?>
<MockElement id="test-123">
<name>Test Mock Name</name>
<value>Test mock value</value>
</MockElement>`
validMockXMLWithNamespace = `<?xml version="1.0" encoding="UTF-8"?>
<MockElement xmlns="http://test.example.com" id="test-456">
<name>Namespaced Mock</name>
<value>Namespaced value</value>
</MockElement>`
invalidMockXML = `<?xml version="1.0" encoding="UTF-8"?>
<MockElement id="test-invalid">
<name></name>
<value>Missing name</value>
</MockElement>`
malformedXML = `<?xml version="1.0" encoding="UTF-8"?>
<MockElement id="malformed">
<name>Unclosed tag</name>
<value>This will fail</value>
</MockElemen>`
emptyXML = ``
largeMockXML = `<?xml version="1.0" encoding="UTF-8"?>
<MockElement id="large-test">
<name>Large Mock</name>
<value>This is a large XML document with nested elements</value>
<nested>
<level1>
<level2>
<level3>
<level4>
<level5>Deep nesting test</level5>
</level4>
</level3>
</level2>
</level1>
</nested>
</MockElement>`
deeplyNestedXML = `<?xml version="1.0" encoding="UTF-8"?>
<MockElement id="deep-test">
<name>Deep Nesting Test</name>
<value>Testing depth limits</value>
<level1><level2><level3><level4><level5>
<level6><level7><level8><level9><level10>
<level11><level12><level13><level14><level15>
<level16><level17><level18><level19><level20>Deep</level20></level19></level18></level17></level16>
</level15></level14></level13></level12></level11>
</level10></level9></level8></level7></level6>
</level5></level4></level3></level2></level1>
</MockElement>`
)
// errorReader is a mock reader that always returns an error
type errorReader struct{}
func (er errorReader) Read(p []byte) (n int, err error) {
return 0, io.ErrUnexpectedEOF
}
func TestNewParser(t *testing.T) {
tests := []struct {
name string
options *ParserOptions
}{
{
name: "Default options",
options: nil,
},
{
name: "Custom options",
options: &ParserOptions{
StrictValidation: false,
AllowEmptyElements: false,
PreserveWhitespace: true,
IgnoreUnknownElements: true,
MaxDepth: 50,
MaxSize: 1024 * 1024,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
parser := NewParser[MockParsable](tt.options)
if parser == nil {
t.Error("Expected parser to not be nil")
return
}
// Verify parser type
if _, ok := parser.(*GenericParser[MockParsable]); !ok {
t.Error("Expected parser to be of type *GenericParser[MockParsable]")
}
})
}
}
func TestParseString(t *testing.T) {
tests := []struct {
name string
xmlData string
expectError bool
expectedID string
options *ParserOptions
}{
{
name: "Valid XML",
xmlData: validMockXML,
expectError: false,
expectedID: "test-123",
options: nil,
},
{
name: "Valid XML with namespace",
xmlData: validMockXMLWithNamespace,
expectError: false,
expectedID: "test-456",
options: nil,
},
{
name: "Invalid XML (validation failure)",
xmlData: invalidMockXML,
expectError: true,
expectedID: "",
options: nil,
},
{
name: "Malformed XML",
xmlData: malformedXML,
expectError: true,
expectedID: "",
options: nil,
},
{
name: "Empty XML",
xmlData: emptyXML,
expectError: true,
expectedID: "",
options: nil,
},
{
name: "Large XML",
xmlData: largeMockXML,
expectError: false,
expectedID: "large-test",
options: nil,
},
{
name: "XML exceeds size limit",
xmlData: largeMockXML,
expectError: true,
expectedID: "",
options: &ParserOptions{
MaxSize: 100, // Very small limit
},
},
{
name: "Deeply nested XML exceeds depth",
xmlData: deeplyNestedXML,
expectError: true,
expectedID: "",
options: &ParserOptions{
MaxDepth: 10, // Shallow depth limit
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
parser := NewParser[MockParsable](tt.options)
result, err := parser.ParseString(tt.xmlData)
if tt.expectError {
if err == nil {
t.Error("Expected parsing error but got none")
}
if result != nil {
t.Error("Expected result to be nil when error occurs")
}
return
}
if err != nil {
t.Errorf("Unexpected parsing error: %v", err)
return
}
if result == nil {
t.Error("Expected result to not be nil")
return
}
if result.GetID() != tt.expectedID {
t.Errorf("Expected ID '%s', got '%s'", tt.expectedID, result.GetID())
}
// Verify the mock is valid (should pass validation)
if err := result.Validate(); err != nil {
t.Errorf("Parsed result failed validation: %v", err)
}
})
}
}
func TestParseBytes(t *testing.T) {
xmlData := []byte(validMockXML)
parser := NewParser[MockParsable](nil)
result, err := parser.ParseBytes(xmlData)
if err != nil {
t.Errorf("Unexpected parsing error: %v", err)
return
}
if result == nil {
t.Error("Expected result to not be nil")
return
}
if result.GetID() != "test-123" {
t.Errorf("Expected ID 'test-123', got '%s'", result.GetID())
}
}
func TestParse(t *testing.T) {
tests := []struct {
name string
reader io.Reader
expectError bool
expectedID string
}{
{
name: "Valid reader",
reader: strings.NewReader(validMockXML),
expectError: false,
expectedID: "test-123",
},
{
name: "Error reader",
reader: errorReader{},
expectError: true,
expectedID: "",
},
{
name: "Empty reader",
reader: strings.NewReader(""),
expectError: true,
expectedID: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
parser := NewParser[MockParsable](nil)
result, err := parser.Parse(tt.reader)
if tt.expectError {
if err == nil {
t.Error("Expected parsing error but got none")
}
if result != nil {
t.Error("Expected result to be nil when error occurs")
}
return
}
if err != nil {
t.Errorf("Unexpected parsing error: %v", err)
return
}
if result == nil {
t.Error("Expected result to not be nil")
return
}
if result.GetID() != tt.expectedID {
t.Errorf("Expected ID '%s', got '%s'", tt.expectedID, result.GetID())
}
})
}
}
func TestValidate(t *testing.T) {
tests := []struct {
name string
reader io.Reader
expectError bool
options *ParserOptions
}{
{
name: "Valid XML",
reader: strings.NewReader(validMockXML),
expectError: false,
options: nil,
},
{
name: "Malformed XML",
reader: strings.NewReader(malformedXML),
expectError: true,
options: nil,
},
{
name: "Empty XML",
reader: strings.NewReader(""),
expectError: true,
options: nil,
},
{
name: "Error reader",
reader: errorReader{},
expectError: true,
options: nil,
},
{
name: "Deeply nested XML exceeds depth",
reader: strings.NewReader(deeplyNestedXML),
expectError: true,
options: &ParserOptions{
MaxDepth: 10,
},
},
{
name: "Valid deeply nested XML within limits",
reader: strings.NewReader(deeplyNestedXML),
expectError: false,
options: &ParserOptions{
MaxDepth: 100,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
parser := NewParser[MockParsable](tt.options)
err := parser.Validate(tt.reader)
if tt.expectError {
if err == nil {
t.Error("Expected validation error but got none")
}
} else {
if err != nil {
t.Errorf("Unexpected validation error: %v", err)
}
}
})
}
}
func TestParseWithResult(t *testing.T) {
tests := []struct {
name string
reader io.Reader
expectError bool
expectedID string
expectMetadata bool
expectedVersion string
expectedEncoding string
}{
{
name: "Valid XML with metadata",
reader: strings.NewReader(validMockXML),
expectError: false,
expectedID: "test-123",
expectMetadata: true,
expectedVersion: "1.0",
expectedEncoding: "UTF-8",
},
{
name: "Valid XML with namespace",
reader: strings.NewReader(validMockXMLWithNamespace),
expectError: false,
expectedID: "test-456",
expectMetadata: true,
expectedVersion: "1.0",
expectedEncoding: "UTF-8",
},
{
name: "Error reader",
reader: errorReader{},
expectError: true,
expectedID: "",
expectMetadata: false,
},
{
name: "Invalid XML (validation failure)",
reader: strings.NewReader(invalidMockXML),
expectError: true,
expectedID: "",
expectMetadata: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
parser := NewParser[MockParsable](nil)
// Cast to GenericParser to access ParseWithResult
genericParser, ok := parser.(*GenericParser[MockParsable])
if !ok {
t.Error("Expected parser to be of type *GenericParser[MockParsable]")
return
}
result, err := genericParser.ParseWithResult(tt.reader)
if result == nil {
t.Error("Expected result to not be nil")
return
}
if tt.expectError {
if err == nil {
t.Error("Expected parsing error but got none")
}
if len(result.Errors) == 0 {
t.Error("Expected errors in result")
}
} else {
if err != nil {
t.Errorf("Unexpected parsing error: %v", err)
}
if len(result.Errors) > 0 {
t.Errorf("Unexpected errors in result: %v", result.Errors)
}
}
if tt.expectMetadata {
if result.Metadata == nil {
t.Error("Expected metadata to not be nil")
} else {
if tt.expectedVersion != "" && result.Metadata.Version != tt.expectedVersion {
t.Errorf("Expected version '%s', got '%s'", tt.expectedVersion, result.Metadata.Version)
}
if tt.expectedEncoding != "" && result.Metadata.Encoding != tt.expectedEncoding {
t.Errorf("Expected encoding '%s', got '%s'", tt.expectedEncoding, result.Metadata.Encoding)
}
}
}
if !tt.expectError && result.Data != nil {
if result.Data.GetID() != tt.expectedID {
t.Errorf("Expected ID '%s', got '%s'", tt.expectedID, result.Data.GetID())
}
}
})
}
}
func TestParseWithInvalidMockData(t *testing.T) {
// Create invalid mock data that will pass XML parsing but fail validation
invalidMockData := MockParsable{
ID: "test-invalid",
Name: "Test Name",
Value: "test-validation-failure", // This will cause validation to fail
Valid: false, // This will cause validation to fail
}
// This test ensures that even if XML parsing succeeds, validation errors are caught
xmlData := `<?xml version="1.0" encoding="UTF-8"?>
<MockElement id="test-invalid">
<name>Test Name</name>
<value>test-validation-failure</value>
</MockElement>`
parser := NewParser[MockParsable](nil)
result, err := parser.ParseString(xmlData)
// This should fail during validation, not XML parsing
if err == nil {
t.Error("Expected validation error but got none")
}
if result != nil {
t.Error("Expected result to be nil when validation fails")
}
// Verify the error message contains validation information
if !strings.Contains(err.Error(), "validation failed") {
t.Errorf("Expected validation error message, got: %v", err)
}
// Test the mock data validation directly
validationErr := invalidMockData.Validate()
if validationErr == nil {
t.Error("Expected direct validation to fail")
}
}
func TestConcurrentParsing(t *testing.T) {
parser := NewParser[MockParsable](nil)
numGoroutines := 10
numOperations := 5
var wg sync.WaitGroup
errors := make(chan error, numGoroutines*numOperations)
results := make(chan *MockParsable, numGoroutines*numOperations)
for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func(goroutineID int) {
defer wg.Done()
for j := 0; j < numOperations; j++ {
// Use different XML data for each operation
xmlData := strings.Replace(validMockXML, "test-123",
fmt.Sprintf("test-%d-%d", goroutineID, j), 1)
result, err := parser.ParseString(xmlData)
if err != nil {
errors <- err
} else {
results <- result
}
}
}(i)
}
wg.Wait()
close(errors)
close(results)
// Check for errors
errorCount := 0
for err := range errors {
t.Errorf("Concurrent parsing error: %v", err)
errorCount++
}
// Check results
resultCount := 0
for range results {
resultCount++
}
expectedResults := numGoroutines * numOperations
if resultCount != expectedResults {
t.Errorf("Expected %d results, got %d (errors: %d)", expectedResults, resultCount, errorCount)
}
}
func TestParserOptionsEffects(t *testing.T) {
tests := []struct {
name string
options *ParserOptions
xmlData string
expectError bool
}{
{
name: "Strict validation enabled",
options: &ParserOptions{
StrictValidation: true,
},
xmlData: validMockXML,
expectError: false,
},
{
name: "Strict validation disabled",
options: &ParserOptions{
StrictValidation: false,
},
xmlData: validMockXML,
expectError: false,
},
{
name: "Very small size limit",
options: &ParserOptions{
MaxSize: 10, // Very small
},
xmlData: validMockXML,
expectError: true,
},
{
name: "Very small depth limit",
options: &ParserOptions{
MaxDepth: 1, // Very shallow
},
xmlData: largeMockXML, // Contains nested elements
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
parser := NewParser[MockParsable](tt.options)
_, err := parser.ParseString(tt.xmlData)
if tt.expectError {
if err == nil {
t.Error("Expected parsing error but got none")
}
} else {
if err != nil {
t.Errorf("Unexpected parsing error: %v", err)
}
}
})
}
}
func TestExtractMetadata(t *testing.T) {
parser := NewParser[MockParsable](nil)
genericParser, ok := parser.(*GenericParser[MockParsable])
if !ok {
t.Error("Expected parser to be of type *GenericParser[MockParsable]")
return
}
tests := []struct {
name string
xmlData string
expectedVersion string
expectedEncoding string
expectedNamespace string
}{
{
name: "Standard XML with version and encoding",
xmlData: validMockXML,
expectedVersion: "1.0",
expectedEncoding: "UTF-8",
expectedNamespace: "",
},
{
name: "XML with namespace",
xmlData: validMockXMLWithNamespace,
expectedVersion: "1.0",
expectedEncoding: "UTF-8",
expectedNamespace: "http://test.example.com",
},
{
name: "XML with single quotes",
xmlData: `<?xml version='1.1' encoding='ISO-8859-1'?>
<MockElement id="test">
<name>Test</name>
<value>Value</value>
</MockElement>`,
expectedVersion: "1.1",
expectedEncoding: "ISO-8859-1",
expectedNamespace: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
metadata := &XMLMetadata{}
err := genericParser.extractMetadata([]byte(tt.xmlData), metadata)
if err != nil {
t.Errorf("Unexpected metadata extraction error: %v", err)
return
}
if metadata.Version != tt.expectedVersion {
t.Errorf("Expected version '%s', got '%s'", tt.expectedVersion, metadata.Version)
}
if metadata.Encoding != tt.expectedEncoding {
t.Errorf("Expected encoding '%s', got '%s'", tt.expectedEncoding, metadata.Encoding)
}
if metadata.Namespace != tt.expectedNamespace {
t.Errorf("Expected namespace '%s', got '%s'", tt.expectedNamespace, metadata.Namespace)
}
})
}
}
func TestParseEdgeCases(t *testing.T) {
parser := NewParser[MockParsable](nil)
tests := []struct {
name string
xmlData string
expectError bool
description string
}{
{
name: "XML with comments",
xmlData: `<?xml version="1.0" encoding="UTF-8"?>
<!-- This is a comment -->
<MockElement id="test-comments">
<!-- Another comment -->
<name>Test with comments</name>
<value>Comments should be ignored</value>
</MockElement>`,
expectError: false,
description: "XML with comments should parse successfully",
},
{
name: "XML with CDATA",
xmlData: `<?xml version="1.0" encoding="UTF-8"?>
<MockElement id="test-cdata">
<name>CDATA Test</name>
<value><![CDATA[This is CDATA content with <special> characters]]></value>
</MockElement>`,
expectError: false,
description: "XML with CDATA should parse successfully",
},
{
name: "XML with special characters",
xmlData: `<?xml version="1.0" encoding="UTF-8"?>
<MockElement id="test-special">
<name>Special &amp; Characters</name>
<value>&lt;tag&gt; &quot;quoted&quot; &apos;text&apos;</value>
</MockElement>`,
expectError: false,
description: "XML with escaped special characters should parse successfully",
},
{
name: "Minimal valid XML",
xmlData: `<MockElement id="minimal">
<name>Minimal</name>
<value>Test</value>
</MockElement>`,
expectError: false,
description: "Minimal XML without declaration should parse successfully",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := parser.ParseString(tt.xmlData)
if tt.expectError {
if err == nil {
t.Errorf("%s: Expected error but got none", tt.description)
}
} else {
if err != nil {
t.Errorf("%s: Unexpected error: %v", tt.description, err)
return
}
if result == nil {
t.Errorf("%s: Expected result to not be nil", tt.description)
return
}
// Verify the result is valid
if err := result.Validate(); err != nil {
t.Errorf("%s: Result validation failed: %v", tt.description, err)
}
}
})
}
}
func TestParserMemoryUsage(t *testing.T) {
// Test parsing multiple documents to ensure no memory leaks
parser := NewParser[MockParsable](nil)
for i := 0; i < 100; i++ {
xmlData := strings.Replace(validMockXML, "test-123", fmt.Sprintf("test-%d", i), 1)
result, err := parser.ParseString(xmlData)
if err != nil {
t.Errorf("Parse error on iteration %d: %v", i, err)
break
}
if result == nil {
t.Errorf("Nil result on iteration %d", i)
break
}
// Explicitly trigger validation to ensure full processing
if err := result.Validate(); err != nil {
t.Errorf("Validation error on iteration %d: %v", i, err)
break
}
}
}
func TestParseFromBuffer(t *testing.T) {
// Test parsing from a bytes.Buffer
buffer := bytes.NewBufferString(validMockXML)
parser := NewParser[MockParsable](nil)
result, err := parser.Parse(buffer)
if err != nil {
t.Errorf("Unexpected error parsing from buffer: %v", err)
return
}
if result == nil {
t.Error("Expected result to not be nil")
return
}
if result.GetID() != "test-123" {
t.Errorf("Expected ID 'test-123', got '%s'", result.GetID())
}
}
+302
View File
@@ -0,0 +1,302 @@
#!/bin/bash
# Comprehensive Test Runner for XML Parser Package
# This script runs the complete test suite with coverage analysis and benchmarks
set -e # Exit on any error
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Configuration
PACKAGE_PATH="./..."
COVERAGE_FILE="coverage.out"
COVERAGE_HTML="coverage.html"
BENCHMARK_OUTPUT="benchmark.txt"
TEST_TIMEOUT="10m"
MIN_COVERAGE=80
echo -e "${BLUE}🚀 Starting XML Parser Test Suite${NC}"
echo "=================================="
# Function to print section headers
print_section() {
echo -e "\n${BLUE}📋 $1${NC}"
echo "----------------------------------------"
}
# Function to check if a command exists
command_exists() {
command -v "$1" >/dev/null 2>&1
}
# Clean up previous test artifacts
print_section "Cleanup"
rm -f ${COVERAGE_FILE} ${COVERAGE_HTML} ${BENCHMARK_OUTPUT}
echo "✅ Cleaned up previous test artifacts"
# Verify Go installation
if ! command_exists go; then
echo -e "${RED}❌ Go is not installed or not in PATH${NC}"
exit 1
fi
GO_VERSION=$(go version)
echo "✅ Go version: ${GO_VERSION}"
# Check if we're in the right directory
if [ ! -f "interfaces.go" ]; then
echo -e "${RED}❌ Not in xmlparser package directory${NC}"
echo "Please run this script from pkg/xmlparser directory"
exit 1
fi
echo "✅ Running from correct directory"
# Run go mod tidy to ensure dependencies are clean
print_section "Dependency Check"
if [ -f "../../go.mod" ]; then
cd ../..
go mod tidy
cd pkg/xmlparser
echo "✅ Dependencies are clean"
else
echo "⚠️ No go.mod found, skipping dependency check"
fi
# Run linting if golangci-lint is available
print_section "Code Quality Check"
if command_exists golangci-lint; then
echo "Running golangci-lint..."
if golangci-lint run --timeout=5m 2>/dev/null; then
echo "✅ Linting passed"
else
echo "⚠️ golangci-lint encountered issues, falling back to go vet"
if go vet ${PACKAGE_PATH}; then
echo "✅ go vet passed"
else
echo -e "${RED}❌ go vet failed${NC}"
exit 1
fi
fi
else
echo "⚠️ golangci-lint not found, using go vet"
# Fallback to basic go vet
if go vet ${PACKAGE_PATH}; then
echo "✅ go vet passed"
else
echo -e "${RED}❌ go vet failed${NC}"
exit 1
fi
fi
# Run go fmt check
echo "Checking code formatting..."
UNFORMATTED=$(gofmt -l .)
if [ -n "$UNFORMATTED" ]; then
echo -e "${RED}❌ Code formatting issues found:${NC}"
echo "$UNFORMATTED"
echo "Run 'gofmt -w .' to fix formatting"
exit 1
else
echo "✅ Code formatting is correct"
fi
# Run unit tests with coverage
print_section "Unit Tests"
echo "Running unit tests with coverage analysis..."
if go test -v -race -timeout=${TEST_TIMEOUT} -coverprofile=${COVERAGE_FILE} ${PACKAGE_PATH}; then
echo -e "${GREEN}✅ All unit tests passed${NC}"
else
echo -e "${RED}❌ Unit tests failed${NC}"
exit 1
fi
# Generate and display coverage report
print_section "Coverage Analysis"
if [ -f ${COVERAGE_FILE} ]; then
# Generate HTML coverage report
go tool cover -html=${COVERAGE_FILE} -o ${COVERAGE_HTML}
echo "✅ HTML coverage report generated: ${COVERAGE_HTML}"
# Extract coverage percentage
COVERAGE=$(go tool cover -func=${COVERAGE_FILE} | grep total | awk '{print $3}' | sed 's/%//')
echo "📊 Coverage Summary:"
go tool cover -func=${COVERAGE_FILE} | tail -10
echo ""
echo "📈 Total Coverage: ${COVERAGE}%"
# Check if coverage meets minimum requirement
if command_exists bc; then
if (( $(echo "$COVERAGE >= $MIN_COVERAGE" | bc -l) )); then
echo -e "${GREEN}✅ Coverage meets minimum requirement (${MIN_COVERAGE}%)${NC}"
else
echo -e "${YELLOW}⚠️ Coverage (${COVERAGE}%) is below minimum requirement (${MIN_COVERAGE}%)${NC}"
echo "Consider adding more tests to improve coverage"
fi
else
# Use shell arithmetic as fallback
if [ $(echo "$COVERAGE" | cut -d. -f1) -ge $MIN_COVERAGE ]; then
echo -e "${GREEN}✅ Coverage meets minimum requirement (${MIN_COVERAGE}%)${NC}"
else
echo -e "${YELLOW}⚠️ Coverage (${COVERAGE}%) is below minimum requirement (${MIN_COVERAGE}%)${NC}"
echo "Consider adding more tests to improve coverage"
fi
fi
else
echo -e "${RED}❌ Coverage file not generated${NC}"
fi
# Run benchmarks
print_section "Performance Benchmarks"
echo "Running performance benchmarks..."
if go test -bench=. -benchmem -run=^$ -timeout=${TEST_TIMEOUT} > ${BENCHMARK_OUTPUT} 2>&1; then
echo -e "${GREEN}✅ Benchmarks completed successfully${NC}"
echo ""
echo "📊 Benchmark Results:"
echo "====================="
cat ${BENCHMARK_OUTPUT} | grep -E "(Benchmark|PASS|FAIL)"
echo ""
echo "🏆 Top 5 Fastest Operations:"
cat ${BENCHMARK_OUTPUT} | grep "^Benchmark" | sort -k3 -n | head -5
echo ""
echo "⚠️ Top 5 Memory Intensive Operations:"
cat ${BENCHMARK_OUTPUT} | grep "^Benchmark" | grep "allocs/op" | sort -k5 -nr | head -5
else
echo -e "${YELLOW}⚠️ Some benchmarks may have failed${NC}"
echo "Benchmark output:"
cat ${BENCHMARK_OUTPUT}
fi
# Run specific test categories
print_section "Test Categories"
echo "Running interface tests..."
if go test -v -run=TestDefaultParserOptions,TestXMLMetadata,TestParseResult,TestMockParsable,TestParserInterface -timeout=30s; then
echo "✅ Interface tests passed"
else
echo -e "${RED}❌ Interface tests failed${NC}"
fi
echo ""
echo "Running parser tests..."
if go test -v -run=TestNewParser,TestParseString,TestParseBytes,TestParse,TestValidate,TestParseWithResult -timeout=1m; then
echo "✅ Parser tests passed"
else
echo -e "${RED}❌ Parser tests failed${NC}"
fi
echo ""
echo "Running utility tests..."
if go test -v -run=TestValidationError,TestSanitizeXMLString,TestValidateXMLName,TestIsValidNamespace,TestNormalizeWhitespace,TestTruncateString -timeout=30s; then
echo "✅ Utility tests passed"
else
echo -e "${RED}❌ Utility tests failed${NC}"
fi
# Test with different Go build tags if needed
print_section "Build Verification"
echo "Verifying package builds correctly..."
if go build -v ${PACKAGE_PATH}; then
echo "✅ Package builds successfully"
else
echo -e "${RED}❌ Package build failed${NC}"
exit 1
fi
# Run race condition detection
print_section "Race Condition Detection"
echo "Running tests with race detector..."
if go test -race -run=TestConcurrent -timeout=2m; then
echo "✅ No race conditions detected"
else
echo -e "${YELLOW}⚠️ Race condition tests completed with warnings${NC}"
fi
# Memory leak detection (basic)
print_section "Memory Analysis"
echo "Running memory allocation tests..."
if go test -run=TestParserMemoryUsage -timeout=1m; then
echo "✅ Memory tests passed"
else
echo -e "${YELLOW}⚠️ Memory tests completed with warnings${NC}"
fi
# Run TED model tests if they exist
if [ -d "ted" ]; then
print_section "TED Model Tests"
echo "Running TED XML model tests..."
if go test -v ./ted -timeout=1m; then
echo "✅ TED model tests passed"
else
echo -e "${RED}❌ TED model tests failed${NC}"
fi
fi
# Performance regression check (if baseline exists)
if [ -f "benchmark_baseline.txt" ]; then
print_section "Performance Regression Check"
echo "Comparing with baseline performance..."
if command_exists benchcmp; then
benchcmp benchmark_baseline.txt ${BENCHMARK_OUTPUT}
echo "✅ Performance comparison completed"
else
echo "⚠️ benchcmp not available, skipping performance regression check"
echo "Install with: go install golang.org/x/tools/cmd/benchcmp@latest"
fi
else
echo "💡 Consider creating a performance baseline:"
echo " cp ${BENCHMARK_OUTPUT} benchmark_baseline.txt"
fi
# Test summary
print_section "Test Summary"
echo "📋 Test Suite Completed"
echo "======================="
echo "✅ Unit Tests: PASSED"
echo "📊 Coverage: ${COVERAGE}%"
echo "🏆 Benchmarks: COMPLETED"
echo "🔍 Race Detection: PASSED"
echo "📁 Generated Files:"
echo " - ${COVERAGE_HTML} (HTML coverage report)"
echo " - ${COVERAGE_FILE} (Coverage data)"
echo " - ${BENCHMARK_OUTPUT} (Benchmark results)"
if [ -f ${COVERAGE_HTML} ]; then
echo ""
echo "🌐 To view coverage report in browser:"
echo " open ${COVERAGE_HTML}"
fi
echo ""
echo -e "${GREEN}🎉 All tests completed successfully!${NC}"
# Cleanup intermediate files (optional)
read -p "🗑️ Remove intermediate files? (y/N): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
rm -f ${COVERAGE_FILE}
echo "✅ Intermediate files cleaned up"
fi
echo "=================================="
echo -e "${BLUE}🏁 XML Parser Test Suite Complete${NC}"
+763
View File
@@ -0,0 +1,763 @@
package xmlparser
import (
"errors"
"strings"
"testing"
)
func TestValidationError(t *testing.T) {
tests := []struct {
name string
field string
message string
value string
expected string
}{
{
name: "Error with value",
field: "name",
message: "is required",
value: "empty",
expected: "validation error in field 'name': is required (value: empty)",
},
{
name: "Error without value",
field: "id",
message: "must be unique",
value: "",
expected: "validation error in field 'id': must be unique",
},
{
name: "Complex error message",
field: "email",
message: "must be a valid email address",
value: "invalid-email",
expected: "validation error in field 'email': must be a valid email address (value: invalid-email)",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidationError{
Field: tt.field,
Message: tt.message,
Value: tt.value,
}
if err.Error() != tt.expected {
t.Errorf("Expected error message '%s', got '%s'", tt.expected, err.Error())
}
})
}
}
func TestValidationErrors(t *testing.T) {
tests := []struct {
name string
errors ValidationErrors
expected string
}{
{
name: "Empty validation errors",
errors: ValidationErrors{},
expected: "no validation errors",
},
{
name: "Single validation error",
errors: ValidationErrors{
{Field: "name", Message: "is required"},
},
expected: "validation failed with 1 error(s): validation error in field 'name': is required",
},
{
name: "Multiple validation errors",
errors: ValidationErrors{
{Field: "name", Message: "is required"},
{Field: "email", Message: "must be valid", Value: "invalid"},
},
expected: "validation failed with 2 error(s): validation error in field 'name': is required; validation error in field 'email': must be valid (value: invalid)",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.errors.Error() != tt.expected {
t.Errorf("Expected error message '%s', got '%s'", tt.expected, tt.errors.Error())
}
})
}
}
func TestValidationErrorsIsEmpty(t *testing.T) {
tests := []struct {
name string
errors ValidationErrors
expected bool
}{
{
name: "Empty errors",
errors: ValidationErrors{},
expected: true,
},
{
name: "Nil errors",
errors: nil,
expected: true,
},
{
name: "Non-empty errors",
errors: ValidationErrors{
{Field: "test", Message: "test error"},
},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.errors.IsEmpty() != tt.expected {
t.Errorf("Expected IsEmpty() to return %v, got %v", tt.expected, tt.errors.IsEmpty())
}
})
}
}
func TestValidationErrorsAdd(t *testing.T) {
var errors ValidationErrors
// Test adding errors
errors.Add("field1", "message1", "value1")
errors.Add("field2", "message2", "")
if len(errors) != 2 {
t.Errorf("Expected 2 errors, got %d", len(errors))
}
// Test first error
if errors[0].Field != "field1" {
t.Errorf("Expected first error field 'field1', got '%s'", errors[0].Field)
}
if errors[0].Message != "message1" {
t.Errorf("Expected first error message 'message1', got '%s'", errors[0].Message)
}
if errors[0].Value != "value1" {
t.Errorf("Expected first error value 'value1', got '%s'", errors[0].Value)
}
// Test second error
if errors[1].Field != "field2" {
t.Errorf("Expected second error field 'field2', got '%s'", errors[1].Field)
}
if errors[1].Message != "message2" {
t.Errorf("Expected second error message 'message2', got '%s'", errors[1].Message)
}
if errors[1].Value != "" {
t.Errorf("Expected second error value to be empty, got '%s'", errors[1].Value)
}
}
func TestXMLParseError(t *testing.T) {
tests := []struct {
name string
error XMLParseError
expected string
}{
{
name: "Complete error information",
error: XMLParseError{
Line: 10,
Column: 5,
Element: "TestElement",
Message: "unexpected end of element",
Original: errors.New("original error"),
},
expected: "XML parse error (element 'TestElement', line 10, column 5): unexpected end of element",
},
{
name: "Error with line only",
error: XMLParseError{
Line: 15,
Message: "syntax error",
},
expected: "XML parse error (line 15): syntax error",
},
{
name: "Error with element only",
error: XMLParseError{
Element: "BadElement",
Message: "invalid element",
},
expected: "XML parse error (element 'BadElement'): invalid element",
},
{
name: "Minimal error",
error: XMLParseError{
Message: "general parse error",
},
expected: "XML parse error: general parse error",
},
{
name: "Error with line and column",
error: XMLParseError{
Line: 20,
Column: 8,
Message: "missing closing tag",
},
expected: "XML parse error (line 20, column 8): missing closing tag",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.error.Error() != tt.expected {
t.Errorf("Expected error message '%s', got '%s'", tt.expected, tt.error.Error())
}
})
}
}
func TestXMLParseErrorUnwrap(t *testing.T) {
originalErr := errors.New("original error")
xmlErr := XMLParseError{
Message: "wrapper error",
Original: originalErr,
}
unwrapped := xmlErr.Unwrap()
if unwrapped != originalErr {
t.Errorf("Expected unwrapped error to be original error, got %v", unwrapped)
}
// Test with no original error
xmlErrNoOriginal := XMLParseError{
Message: "standalone error",
}
unwrappedNil := xmlErrNoOriginal.Unwrap()
if unwrappedNil != nil {
t.Errorf("Expected unwrapped error to be nil, got %v", unwrappedNil)
}
}
func TestSanitizeXMLString(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "Clean string",
input: "Hello World",
expected: "Hello World",
},
{
name: "String with valid whitespace",
input: "Hello\tWorld\nNew Line\rCarriage Return",
expected: "Hello\tWorld\nNew Line\rCarriage Return",
},
{
name: "String with control characters",
input: "Hello\x00\x01\x02World",
expected: "HelloWorld",
},
{
name: "String with high Unicode",
input: "Hello 世界 🌍",
expected: "Hello 世界 🌍",
},
{
name: "Empty string",
input: "",
expected: "",
},
{
name: "String with only control characters",
input: "\x00\x01\x02\x03",
expected: "",
},
{
name: "Mixed valid and invalid characters",
input: "Valid\x00Invalid\tTab\x01More\nNewline",
expected: "ValidInvalid\tTabMore\nNewline",
},
{
name: "XML special characters",
input: "<>&\"'",
expected: "<>&\"'",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := SanitizeXMLString(tt.input)
if result != tt.expected {
t.Errorf("Expected '%s', got '%s'", tt.expected, result)
}
})
}
}
func TestValidateXMLName(t *testing.T) {
tests := []struct {
name string
xmlName string
expected bool
}{
// Valid names
{
name: "Simple name",
xmlName: "element",
expected: true,
},
{
name: "Name with underscore",
xmlName: "_element",
expected: true,
},
{
name: "Name with uppercase",
xmlName: "Element",
expected: true,
},
{
name: "Name with numbers",
xmlName: "element123",
expected: true,
},
{
name: "Name with hyphen",
xmlName: "my-element",
expected: true,
},
{
name: "Name with period",
xmlName: "my.element",
expected: true,
},
{
name: "Complex valid name",
xmlName: "My_Element-123.test",
expected: true,
},
// Invalid names
{
name: "Empty name",
xmlName: "",
expected: false,
},
{
name: "Name starting with number",
xmlName: "123element",
expected: false,
},
{
name: "Name starting with hyphen",
xmlName: "-element",
expected: false,
},
{
name: "Name starting with period",
xmlName: ".element",
expected: false,
},
{
name: "Name with space",
xmlName: "my element",
expected: false,
},
{
name: "Name with special characters",
xmlName: "element@test",
expected: false,
},
{
name: "Name with colon",
xmlName: "element:test",
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := ValidateXMLName(tt.xmlName)
if result != tt.expected {
t.Errorf("Expected ValidateXMLName('%s') to return %v, got %v", tt.xmlName, tt.expected, result)
}
})
}
}
func TestIsValidNamespace(t *testing.T) {
tests := []struct {
name string
namespace string
expected bool
}{
// Valid namespaces
{
name: "Empty namespace",
namespace: "",
expected: true,
},
{
name: "HTTP namespace",
namespace: "http://example.com",
expected: true,
},
{
name: "HTTPS namespace",
namespace: "https://example.com",
expected: true,
},
{
name: "URN namespace",
namespace: "urn:example:namespace",
expected: true,
},
{
name: "Complex HTTP namespace",
namespace: "http://www.example.com/schema/2023",
expected: true,
},
{
name: "Complex HTTPS namespace",
namespace: "https://schemas.example.org/xml/namespace",
expected: true,
},
{
name: "Complex URN namespace",
namespace: "urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2",
expected: true,
},
// Invalid namespaces
{
name: "Invalid protocol",
namespace: "ftp://example.com",
expected: false,
},
{
name: "No protocol",
namespace: "example.com",
expected: false,
},
{
name: "Invalid format",
namespace: "not-a-namespace",
expected: false,
},
{
name: "Partial HTTP",
namespace: "http:",
expected: false,
},
{
name: "Partial URN",
namespace: "urn",
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := IsValidNamespace(tt.namespace)
if result != tt.expected {
t.Errorf("Expected IsValidNamespace('%s') to return %v, got %v", tt.namespace, tt.expected, result)
}
})
}
}
func TestNormalizeWhitespace(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "Single spaces",
input: "hello world",
expected: "hello world",
},
{
name: "Multiple spaces",
input: "hello world",
expected: "hello world",
},
{
name: "Mixed whitespace",
input: "hello\t\n\r world",
expected: "hello world",
},
{
name: "Leading whitespace",
input: " hello world",
expected: "hello world",
},
{
name: "Trailing whitespace",
input: "hello world ",
expected: "hello world",
},
{
name: "Leading and trailing whitespace",
input: " hello world ",
expected: "hello world",
},
{
name: "Multiple words with mixed whitespace",
input: "one\ttwo\n\nthree\r\r\rfour five",
expected: "one two three four five",
},
{
name: "Empty string",
input: "",
expected: "",
},
{
name: "Only whitespace",
input: " \t\n\r ",
expected: "",
},
{
name: "Single word",
input: "hello",
expected: "hello",
},
{
name: "Single word with whitespace",
input: " hello ",
expected: "hello",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := NormalizeWhitespace(tt.input)
if result != tt.expected {
t.Errorf("Expected '%s', got '%s'", tt.expected, result)
}
})
}
}
func TestTruncateString(t *testing.T) {
tests := []struct {
name string
input string
maxLength int
expected string
}{
{
name: "Short string",
input: "hello",
maxLength: 10,
expected: "hello",
},
{
name: "Exact length",
input: "hello",
maxLength: 5,
expected: "hello",
},
{
name: "Truncate with ellipsis",
input: "hello world",
maxLength: 8,
expected: "hello...",
},
{
name: "Very short max length",
input: "hello",
maxLength: 3,
expected: "hel",
},
{
name: "Max length 0",
input: "hello",
maxLength: 0,
expected: "",
},
{
name: "Max length 1",
input: "hello",
maxLength: 1,
expected: "h",
},
{
name: "Max length 2",
input: "hello",
maxLength: 2,
expected: "he",
},
{
name: "Empty string",
input: "",
maxLength: 5,
expected: "",
},
{
name: "Long string truncation",
input: "This is a very long string that needs to be truncated",
maxLength: 20,
expected: "This is a very lo...",
},
{
name: "Unicode string",
input: "Hello 世界",
maxLength: 8,
expected: "Hello...",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := TruncateString(tt.input, tt.maxLength)
if result != tt.expected {
t.Errorf("Expected '%s', got '%s'", tt.expected, result)
}
// Verify result length doesn't exceed maxLength
if len(result) > tt.maxLength {
t.Errorf("Result length %d exceeds maxLength %d", len(result), tt.maxLength)
}
})
}
}
func TestUtilityFunctionsWithRealWorldData(t *testing.T) {
// Test with real-world XML-like data
t.Run("Real world XML sanitization", func(t *testing.T) {
input := "Contract Notice\x00 for\x01 Public\x02 Procurement\t\nProject"
expected := "Contract Notice for Public Procurement\t\nProject"
result := SanitizeXMLString(input)
if result != expected {
t.Errorf("Expected '%s', got '%s'", expected, result)
}
})
t.Run("Real world XML name validation", func(t *testing.T) {
validNames := []string{
"ContractNotice",
"ProcurementProject",
"TenderingTerms",
"_privateElement",
"xml-element",
"element.version",
}
for _, name := range validNames {
if !ValidateXMLName(name) {
t.Errorf("Expected '%s' to be valid XML name", name)
}
}
invalidNames := []string{
"123Contract",
"-element",
".element",
"element with spaces",
"element@symbol",
}
for _, name := range invalidNames {
if ValidateXMLName(name) {
t.Errorf("Expected '%s' to be invalid XML name", name)
}
}
})
t.Run("Real world namespace validation", func(t *testing.T) {
validNamespaces := []string{
"",
"http://www.w3.org/2001/XMLSchema",
"https://ted.europa.eu/schema/eforms",
"urn:oasis:names:specification:ubl:schema:xsd:ContractNotice-2",
}
for _, ns := range validNamespaces {
if !IsValidNamespace(ns) {
t.Errorf("Expected '%s' to be valid namespace", ns)
}
}
})
t.Run("Real world whitespace normalization", func(t *testing.T) {
input := " Contract\t\tNotice\n\n\nfor\r\r\rPublic Procurement "
expected := "Contract Notice for Public Procurement"
result := NormalizeWhitespace(input)
if result != expected {
t.Errorf("Expected '%s', got '%s'", expected, result)
}
})
t.Run("Real world string truncation", func(t *testing.T) {
longDescription := "This is a very long contract description that contains detailed information about the procurement project including all requirements, specifications, and conditions that need to be met by potential contractors."
truncated := TruncateString(longDescription, 50)
if len(truncated) > 50 {
t.Errorf("Truncated string length %d exceeds limit 50", len(truncated))
}
if !strings.HasSuffix(truncated, "...") && len(longDescription) > 50 {
t.Error("Expected truncated string to end with '...'")
}
})
}
func TestErrorTypes(t *testing.T) {
// Test that our error types implement the error interface
var _ error = ValidationError{}
var _ error = ValidationErrors{}
var _ error = XMLParseError{}
// Test error interface methods
ve := ValidationError{Field: "test", Message: "test error"}
if ve.Error() == "" {
t.Error("ValidationError.Error() should return non-empty string")
}
ves := ValidationErrors{ve}
if ves.Error() == "" {
t.Error("ValidationErrors.Error() should return non-empty string")
}
xpe := XMLParseError{Message: "test parse error"}
if xpe.Error() == "" {
t.Error("XMLParseError.Error() should return non-empty string")
}
}
func TestValidationErrorsChaining(t *testing.T) {
var errors ValidationErrors
// Build up errors
errors.Add("field1", "error1", "value1")
errors.Add("field2", "error2", "")
errors.Add("field3", "error3", "value3")
if len(errors) != 3 {
t.Errorf("Expected 3 errors, got %d", len(errors))
}
if errors.IsEmpty() {
t.Error("Expected errors to not be empty")
}
errorMessage := errors.Error()
if !strings.Contains(errorMessage, "validation failed with 3 error(s)") {
t.Errorf("Error message should contain count, got: %s", errorMessage)
}
if !strings.Contains(errorMessage, "field1") {
t.Error("Error message should contain field1")
}
if !strings.Contains(errorMessage, "field2") {
t.Error("Error message should contain field2")
}
if !strings.Contains(errorMessage, "field3") {
t.Error("Error message should contain field3")
}
}