Add comprehensive tests for XML parser functionality

- Introduced benchmark tests for various XML parsing methods, including parsing from strings, bytes, and readers, to evaluate performance.
- Added unit tests for the parser interface and validation mechanisms, ensuring robust error handling and validation logic.
- Implemented tests for utility functions related to XML processing, enhancing overall test coverage and reliability.
- Created a test suite for the TED domain, validating the structure and functionality of contract notices and related entities.
- Developed a comprehensive test runner script to facilitate automated testing, coverage analysis, and performance benchmarking for the XML parser package.
This commit is contained in:
n.nakhostin
2025-08-12 12:51:24 +03:30
parent 7dc695752b
commit 5322d3cd8e
6 changed files with 2905 additions and 0 deletions
+763
View File
@@ -0,0 +1,763 @@
package xmlparser
import (
"errors"
"strings"
"testing"
)
func TestValidationError(t *testing.T) {
tests := []struct {
name string
field string
message string
value string
expected string
}{
{
name: "Error with value",
field: "name",
message: "is required",
value: "empty",
expected: "validation error in field 'name': is required (value: empty)",
},
{
name: "Error without value",
field: "id",
message: "must be unique",
value: "",
expected: "validation error in field 'id': must be unique",
},
{
name: "Complex error message",
field: "email",
message: "must be a valid email address",
value: "invalid-email",
expected: "validation error in field 'email': must be a valid email address (value: invalid-email)",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidationError{
Field: tt.field,
Message: tt.message,
Value: tt.value,
}
if err.Error() != tt.expected {
t.Errorf("Expected error message '%s', got '%s'", tt.expected, err.Error())
}
})
}
}
func TestValidationErrors(t *testing.T) {
tests := []struct {
name string
errors ValidationErrors
expected string
}{
{
name: "Empty validation errors",
errors: ValidationErrors{},
expected: "no validation errors",
},
{
name: "Single validation error",
errors: ValidationErrors{
{Field: "name", Message: "is required"},
},
expected: "validation failed with 1 error(s): validation error in field 'name': is required",
},
{
name: "Multiple validation errors",
errors: ValidationErrors{
{Field: "name", Message: "is required"},
{Field: "email", Message: "must be valid", Value: "invalid"},
},
expected: "validation failed with 2 error(s): validation error in field 'name': is required; validation error in field 'email': must be valid (value: invalid)",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.errors.Error() != tt.expected {
t.Errorf("Expected error message '%s', got '%s'", tt.expected, tt.errors.Error())
}
})
}
}
func TestValidationErrorsIsEmpty(t *testing.T) {
tests := []struct {
name string
errors ValidationErrors
expected bool
}{
{
name: "Empty errors",
errors: ValidationErrors{},
expected: true,
},
{
name: "Nil errors",
errors: nil,
expected: true,
},
{
name: "Non-empty errors",
errors: ValidationErrors{
{Field: "test", Message: "test error"},
},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.errors.IsEmpty() != tt.expected {
t.Errorf("Expected IsEmpty() to return %v, got %v", tt.expected, tt.errors.IsEmpty())
}
})
}
}
func TestValidationErrorsAdd(t *testing.T) {
var errors ValidationErrors
// Test adding errors
errors.Add("field1", "message1", "value1")
errors.Add("field2", "message2", "")
if len(errors) != 2 {
t.Errorf("Expected 2 errors, got %d", len(errors))
}
// Test first error
if errors[0].Field != "field1" {
t.Errorf("Expected first error field 'field1', got '%s'", errors[0].Field)
}
if errors[0].Message != "message1" {
t.Errorf("Expected first error message 'message1', got '%s'", errors[0].Message)
}
if errors[0].Value != "value1" {
t.Errorf("Expected first error value 'value1', got '%s'", errors[0].Value)
}
// Test second error
if errors[1].Field != "field2" {
t.Errorf("Expected second error field 'field2', got '%s'", errors[1].Field)
}
if errors[1].Message != "message2" {
t.Errorf("Expected second error message 'message2', got '%s'", errors[1].Message)
}
if errors[1].Value != "" {
t.Errorf("Expected second error value to be empty, got '%s'", errors[1].Value)
}
}
func TestXMLParseError(t *testing.T) {
tests := []struct {
name string
error XMLParseError
expected string
}{
{
name: "Complete error information",
error: XMLParseError{
Line: 10,
Column: 5,
Element: "TestElement",
Message: "unexpected end of element",
Original: errors.New("original error"),
},
expected: "XML parse error (element 'TestElement', line 10, column 5): unexpected end of element",
},
{
name: "Error with line only",
error: XMLParseError{
Line: 15,
Message: "syntax error",
},
expected: "XML parse error (line 15): syntax error",
},
{
name: "Error with element only",
error: XMLParseError{
Element: "BadElement",
Message: "invalid element",
},
expected: "XML parse error (element 'BadElement'): invalid element",
},
{
name: "Minimal error",
error: XMLParseError{
Message: "general parse error",
},
expected: "XML parse error: general parse error",
},
{
name: "Error with line and column",
error: XMLParseError{
Line: 20,
Column: 8,
Message: "missing closing tag",
},
expected: "XML parse error (line 20, column 8): missing closing tag",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.error.Error() != tt.expected {
t.Errorf("Expected error message '%s', got '%s'", tt.expected, tt.error.Error())
}
})
}
}
func TestXMLParseErrorUnwrap(t *testing.T) {
originalErr := errors.New("original error")
xmlErr := XMLParseError{
Message: "wrapper error",
Original: originalErr,
}
unwrapped := xmlErr.Unwrap()
if unwrapped != originalErr {
t.Errorf("Expected unwrapped error to be original error, got %v", unwrapped)
}
// Test with no original error
xmlErrNoOriginal := XMLParseError{
Message: "standalone error",
}
unwrappedNil := xmlErrNoOriginal.Unwrap()
if unwrappedNil != nil {
t.Errorf("Expected unwrapped error to be nil, got %v", unwrappedNil)
}
}
func TestSanitizeXMLString(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "Clean string",
input: "Hello World",
expected: "Hello World",
},
{
name: "String with valid whitespace",
input: "Hello\tWorld\nNew Line\rCarriage Return",
expected: "Hello\tWorld\nNew Line\rCarriage Return",
},
{
name: "String with control characters",
input: "Hello\x00\x01\x02World",
expected: "HelloWorld",
},
{
name: "String with high Unicode",
input: "Hello 世界 🌍",
expected: "Hello 世界 🌍",
},
{
name: "Empty string",
input: "",
expected: "",
},
{
name: "String with only control characters",
input: "\x00\x01\x02\x03",
expected: "",
},
{
name: "Mixed valid and invalid characters",
input: "Valid\x00Invalid\tTab\x01More\nNewline",
expected: "ValidInvalid\tTabMore\nNewline",
},
{
name: "XML special characters",
input: "<>&\"'",
expected: "<>&\"'",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := SanitizeXMLString(tt.input)
if result != tt.expected {
t.Errorf("Expected '%s', got '%s'", tt.expected, result)
}
})
}
}
func TestValidateXMLName(t *testing.T) {
tests := []struct {
name string
xmlName string
expected bool
}{
// Valid names
{
name: "Simple name",
xmlName: "element",
expected: true,
},
{
name: "Name with underscore",
xmlName: "_element",
expected: true,
},
{
name: "Name with uppercase",
xmlName: "Element",
expected: true,
},
{
name: "Name with numbers",
xmlName: "element123",
expected: true,
},
{
name: "Name with hyphen",
xmlName: "my-element",
expected: true,
},
{
name: "Name with period",
xmlName: "my.element",
expected: true,
},
{
name: "Complex valid name",
xmlName: "My_Element-123.test",
expected: true,
},
// Invalid names
{
name: "Empty name",
xmlName: "",
expected: false,
},
{
name: "Name starting with number",
xmlName: "123element",
expected: false,
},
{
name: "Name starting with hyphen",
xmlName: "-element",
expected: false,
},
{
name: "Name starting with period",
xmlName: ".element",
expected: false,
},
{
name: "Name with space",
xmlName: "my element",
expected: false,
},
{
name: "Name with special characters",
xmlName: "element@test",
expected: false,
},
{
name: "Name with colon",
xmlName: "element:test",
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := ValidateXMLName(tt.xmlName)
if result != tt.expected {
t.Errorf("Expected ValidateXMLName('%s') to return %v, got %v", tt.xmlName, tt.expected, result)
}
})
}
}
func TestIsValidNamespace(t *testing.T) {
tests := []struct {
name string
namespace string
expected bool
}{
// Valid namespaces
{
name: "Empty namespace",
namespace: "",
expected: true,
},
{
name: "HTTP namespace",
namespace: "http://example.com",
expected: true,
},
{
name: "HTTPS namespace",
namespace: "https://example.com",
expected: true,
},
{
name: "URN namespace",
namespace: "urn:example:namespace",
expected: true,
},
{
name: "Complex HTTP namespace",
namespace: "http://www.example.com/schema/2023",
expected: true,
},
{
name: "Complex HTTPS namespace",
namespace: "https://schemas.example.org/xml/namespace",
expected: true,
},
{
name: "Complex URN namespace",
namespace: "urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2",
expected: true,
},
// Invalid namespaces
{
name: "Invalid protocol",
namespace: "ftp://example.com",
expected: false,
},
{
name: "No protocol",
namespace: "example.com",
expected: false,
},
{
name: "Invalid format",
namespace: "not-a-namespace",
expected: false,
},
{
name: "Partial HTTP",
namespace: "http:",
expected: false,
},
{
name: "Partial URN",
namespace: "urn",
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := IsValidNamespace(tt.namespace)
if result != tt.expected {
t.Errorf("Expected IsValidNamespace('%s') to return %v, got %v", tt.namespace, tt.expected, result)
}
})
}
}
func TestNormalizeWhitespace(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "Single spaces",
input: "hello world",
expected: "hello world",
},
{
name: "Multiple spaces",
input: "hello world",
expected: "hello world",
},
{
name: "Mixed whitespace",
input: "hello\t\n\r world",
expected: "hello world",
},
{
name: "Leading whitespace",
input: " hello world",
expected: "hello world",
},
{
name: "Trailing whitespace",
input: "hello world ",
expected: "hello world",
},
{
name: "Leading and trailing whitespace",
input: " hello world ",
expected: "hello world",
},
{
name: "Multiple words with mixed whitespace",
input: "one\ttwo\n\nthree\r\r\rfour five",
expected: "one two three four five",
},
{
name: "Empty string",
input: "",
expected: "",
},
{
name: "Only whitespace",
input: " \t\n\r ",
expected: "",
},
{
name: "Single word",
input: "hello",
expected: "hello",
},
{
name: "Single word with whitespace",
input: " hello ",
expected: "hello",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := NormalizeWhitespace(tt.input)
if result != tt.expected {
t.Errorf("Expected '%s', got '%s'", tt.expected, result)
}
})
}
}
func TestTruncateString(t *testing.T) {
tests := []struct {
name string
input string
maxLength int
expected string
}{
{
name: "Short string",
input: "hello",
maxLength: 10,
expected: "hello",
},
{
name: "Exact length",
input: "hello",
maxLength: 5,
expected: "hello",
},
{
name: "Truncate with ellipsis",
input: "hello world",
maxLength: 8,
expected: "hello...",
},
{
name: "Very short max length",
input: "hello",
maxLength: 3,
expected: "hel",
},
{
name: "Max length 0",
input: "hello",
maxLength: 0,
expected: "",
},
{
name: "Max length 1",
input: "hello",
maxLength: 1,
expected: "h",
},
{
name: "Max length 2",
input: "hello",
maxLength: 2,
expected: "he",
},
{
name: "Empty string",
input: "",
maxLength: 5,
expected: "",
},
{
name: "Long string truncation",
input: "This is a very long string that needs to be truncated",
maxLength: 20,
expected: "This is a very lo...",
},
{
name: "Unicode string",
input: "Hello 世界",
maxLength: 8,
expected: "Hello...",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := TruncateString(tt.input, tt.maxLength)
if result != tt.expected {
t.Errorf("Expected '%s', got '%s'", tt.expected, result)
}
// Verify result length doesn't exceed maxLength
if len(result) > tt.maxLength {
t.Errorf("Result length %d exceeds maxLength %d", len(result), tt.maxLength)
}
})
}
}
func TestUtilityFunctionsWithRealWorldData(t *testing.T) {
// Test with real-world XML-like data
t.Run("Real world XML sanitization", func(t *testing.T) {
input := "Contract Notice\x00 for\x01 Public\x02 Procurement\t\nProject"
expected := "Contract Notice for Public Procurement\t\nProject"
result := SanitizeXMLString(input)
if result != expected {
t.Errorf("Expected '%s', got '%s'", expected, result)
}
})
t.Run("Real world XML name validation", func(t *testing.T) {
validNames := []string{
"ContractNotice",
"ProcurementProject",
"TenderingTerms",
"_privateElement",
"xml-element",
"element.version",
}
for _, name := range validNames {
if !ValidateXMLName(name) {
t.Errorf("Expected '%s' to be valid XML name", name)
}
}
invalidNames := []string{
"123Contract",
"-element",
".element",
"element with spaces",
"element@symbol",
}
for _, name := range invalidNames {
if ValidateXMLName(name) {
t.Errorf("Expected '%s' to be invalid XML name", name)
}
}
})
t.Run("Real world namespace validation", func(t *testing.T) {
validNamespaces := []string{
"",
"http://www.w3.org/2001/XMLSchema",
"https://ted.europa.eu/schema/eforms",
"urn:oasis:names:specification:ubl:schema:xsd:ContractNotice-2",
}
for _, ns := range validNamespaces {
if !IsValidNamespace(ns) {
t.Errorf("Expected '%s' to be valid namespace", ns)
}
}
})
t.Run("Real world whitespace normalization", func(t *testing.T) {
input := " Contract\t\tNotice\n\n\nfor\r\r\rPublic Procurement "
expected := "Contract Notice for Public Procurement"
result := NormalizeWhitespace(input)
if result != expected {
t.Errorf("Expected '%s', got '%s'", expected, result)
}
})
t.Run("Real world string truncation", func(t *testing.T) {
longDescription := "This is a very long contract description that contains detailed information about the procurement project including all requirements, specifications, and conditions that need to be met by potential contractors."
truncated := TruncateString(longDescription, 50)
if len(truncated) > 50 {
t.Errorf("Truncated string length %d exceeds limit 50", len(truncated))
}
if !strings.HasSuffix(truncated, "...") && len(longDescription) > 50 {
t.Error("Expected truncated string to end with '...'")
}
})
}
func TestErrorTypes(t *testing.T) {
// Test that our error types implement the error interface
var _ error = ValidationError{}
var _ error = ValidationErrors{}
var _ error = XMLParseError{}
// Test error interface methods
ve := ValidationError{Field: "test", Message: "test error"}
if ve.Error() == "" {
t.Error("ValidationError.Error() should return non-empty string")
}
ves := ValidationErrors{ve}
if ves.Error() == "" {
t.Error("ValidationErrors.Error() should return non-empty string")
}
xpe := XMLParseError{Message: "test parse error"}
if xpe.Error() == "" {
t.Error("XMLParseError.Error() should return non-empty string")
}
}
func TestValidationErrorsChaining(t *testing.T) {
var errors ValidationErrors
// Build up errors
errors.Add("field1", "error1", "value1")
errors.Add("field2", "error2", "")
errors.Add("field3", "error3", "value3")
if len(errors) != 3 {
t.Errorf("Expected 3 errors, got %d", len(errors))
}
if errors.IsEmpty() {
t.Error("Expected errors to not be empty")
}
errorMessage := errors.Error()
if !strings.Contains(errorMessage, "validation failed with 3 error(s)") {
t.Errorf("Error message should contain count, got: %s", errorMessage)
}
if !strings.Contains(errorMessage, "field1") {
t.Error("Error message should contain field1")
}
if !strings.Contains(errorMessage, "field2") {
t.Error("Error message should contain field2")
}
if !strings.Contains(errorMessage, "field3") {
t.Error("Error message should contain field3")
}
}