5322d3cd8e
- 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.
764 lines
17 KiB
Go
764 lines
17 KiB
Go
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")
|
|
}
|
|
}
|