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:
@@ -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()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -61,6 +61,13 @@ func (p *GenericParser[T]) ParseBytes(xmlData []byte) (*T, error) {
|
|||||||
targetPtr = reflect.New(resultType).Interface()
|
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
|
// Create XML decoder
|
||||||
decoder := xml.NewDecoder(bytes.NewReader(xmlData))
|
decoder := xml.NewDecoder(bytes.NewReader(xmlData))
|
||||||
|
|
||||||
@@ -142,6 +149,38 @@ func (p *GenericParser[T]) Validate(reader io.Reader) error {
|
|||||||
return nil
|
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
|
// ParseWithResult parses XML and returns detailed result with metadata
|
||||||
func (p *GenericParser[T]) ParseWithResult(reader io.Reader) (*ParseResult[T], error) {
|
func (p *GenericParser[T]) ParseWithResult(reader io.Reader) (*ParseResult[T], error) {
|
||||||
result := &ParseResult[T]{
|
result := &ParseResult[T]{
|
||||||
|
|||||||
@@ -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 & Characters</name>
|
||||||
|
<value><tag> "quoted" 'text'</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())
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+302
@@ -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}"
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user