Merge pull request 'XML Parser' (#4) from feature/xml-parser into develop
Reviewed-on: https://repo.ravanertebat.com/TM/tm_back/pulls/4
This commit is contained in:
@@ -45,106 +45,113 @@ This system implements **Clean Architecture** with:
|
|||||||
- **Input Validation**: Govalidator integration for request validation
|
- **Input Validation**: Govalidator integration for request validation
|
||||||
- **Error Handling**: Consistent error responses and logging
|
- **Error Handling**: Consistent error responses and logging
|
||||||
- **Time Handling**: Unix timestamps throughout the application
|
- **Time Handling**: Unix timestamps throughout the application
|
||||||
- **API Documentation**: Auto-generated Swagger documentation
|
- **Generic XML Parser**: Parse any XML structure into Go structs using generics
|
||||||
|
|
||||||
## 📋 API Documentation
|
## 🔧 New Features
|
||||||
|
|
||||||
### Swagger/OpenAPI 3.0 Documentation
|
### Generic XML Parser
|
||||||
|
|
||||||
The Tender Management API features comprehensive, auto-generated Swagger documentation with:
|
A powerful, generic XML parser built with Go generics that can parse any XML structure into Go structs. Perfect for handling complex XML formats like TED (Tenders Electronic Daily) notices.
|
||||||
|
|
||||||
#### 🚀 **Version 2.0.0 Features**
|
#### Features
|
||||||
- **Comprehensive API Coverage**: All endpoints documented with detailed descriptions, examples, and error responses
|
- **Generic Support**: Parse any XML structure that implements the `Parseable` interface
|
||||||
- **Interactive API Testing**: Built-in Swagger UI for testing endpoints directly from the documentation
|
- **Flexible Configuration**: Customizable parsing options for different use cases
|
||||||
- **Authentication Integration**: Bearer token authentication examples and testing capability
|
- **Built-in Validation**: Automatic validation of parsed structures
|
||||||
- **Request/Response Examples**: Real-world examples for all data structures and API calls
|
- **Metadata Extraction**: Extract XML metadata and parsing information
|
||||||
- **Error Documentation**: Detailed error codes, messages, and troubleshooting information
|
- **TED XML Support**: Pre-built models for TED contract notices
|
||||||
|
- **Error Reporting**: Detailed error and warning reporting
|
||||||
|
|
||||||
#### 📊 **API Endpoints Categories**
|
#### Quick Example
|
||||||
- **Health**: System monitoring and health check endpoints
|
```go
|
||||||
- **Authorization**: User authentication, token management, and session handling
|
// Define your XML structure
|
||||||
- **Users**: Administrative user management with RBAC (Role-Based Access Control)
|
type Contract struct {
|
||||||
- **Admin-Customers**: Web panel customer management with advanced filtering
|
XMLName xml.Name `xml:"Contract"`
|
||||||
- **Customers-Authorization**: Mobile app customer authentication and profile access
|
ID string `xml:"id,attr"`
|
||||||
- **Admin-Companies**: Comprehensive company management with search, filtering, and analytics
|
Name string `xml:"name"`
|
||||||
|
Value string `xml:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
#### 🔧 **Documentation Access**
|
func (c Contract) Validate() error { /* validation logic */ }
|
||||||
- **Local Development**: `http://localhost:8081/swagger/`
|
func (c Contract) GetID() string { return c.ID }
|
||||||
- **Interactive Testing**: All endpoints can be tested directly from the Swagger UI
|
|
||||||
- **Authentication**: Use the "Authorize" button to set Bearer tokens for protected endpoints
|
|
||||||
- **Export Options**: JSON and YAML formats available for API specifications
|
|
||||||
|
|
||||||
#### 📝 **Enhanced Features**
|
// Parse XML
|
||||||
- **Detailed Descriptions**: Each endpoint includes comprehensive business logic explanations
|
parser := xmlparser.NewParser[Contract](nil)
|
||||||
- **Parameter Validation**: Complete validation rules and constraints for all input parameters
|
contract, err := parser.ParseString(xmlData)
|
||||||
- **Response Examples**: Real-world response examples with proper HTTP status codes
|
|
||||||
- **Security Schemes**: JWT Bearer token authentication clearly documented
|
|
||||||
- **Error Handling**: Comprehensive error response documentation with troubleshooting guides
|
|
||||||
- **Filtering & Pagination**: Advanced query parameters for list endpoints with examples
|
|
||||||
- **Business Logic**: Context-aware descriptions explaining when and how to use each endpoint
|
|
||||||
|
|
||||||
#### 🏗️ **Technical Specifications**
|
|
||||||
- **OpenAPI/Swagger 2.0**: Industry-standard API documentation format
|
|
||||||
- **Auto-Generation**: Documentation automatically updated from code annotations
|
|
||||||
- **Type Safety**: Strong typing with Go struct validation and examples
|
|
||||||
- **Validation Rules**: Complete govalidator integration for request validation
|
|
||||||
- **Consistent Response Format**: Standardized API response structure with metadata
|
|
||||||
|
|
||||||
#### 💡 **Usage Examples**
|
|
||||||
```bash
|
|
||||||
# Access Swagger UI
|
|
||||||
curl http://localhost:8081/swagger/
|
|
||||||
|
|
||||||
# Download API specification
|
|
||||||
curl http://localhost:8081/swagger/doc.json
|
|
||||||
|
|
||||||
# Test authentication endpoint
|
|
||||||
curl -X POST "http://localhost:8081/admin/v1/login" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"username": "admin", "password": "password"}'
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 🔐 **Authentication Testing**
|
See [pkg/xmlparser/README.md](pkg/xmlparser/README.md) for complete documentation and examples.
|
||||||
1. Navigate to Swagger UI at `/swagger/`
|
|
||||||
2. Click "Authorize" button
|
|
||||||
3. Enter: `Bearer <your_jwt_token>`
|
|
||||||
4. Test protected endpoints directly from the interface
|
|
||||||
|
|
||||||
### API Design Principles
|
## 📦 Package Structure
|
||||||
- **RESTful Design**: Consistent REST principles with proper HTTP methods and status codes
|
|
||||||
- **Clean Architecture**: Domain-driven design with clear separation of concerns
|
|
||||||
- **Validation**: Comprehensive input validation with meaningful error messages
|
|
||||||
- **Security**: JWT-based authentication with proper token management
|
|
||||||
- **Performance**: Optimized queries with pagination and filtering capabilities
|
|
||||||
- **Maintainability**: Auto-generated documentation stays in sync with code changes
|
|
||||||
|
|
||||||
## 🔧 Development
|
```
|
||||||
|
├── cmd/ # Application entry points
|
||||||
|
├── internal/ # Private application code
|
||||||
|
│ ├── company/ # Company domain
|
||||||
|
│ ├── customer/ # Customer domain
|
||||||
|
│ └── user/ # User domain
|
||||||
|
├── pkg/ # Public packages
|
||||||
|
│ ├── authorization/ # Authorization utilities
|
||||||
|
│ ├── logger/ # Structured logging
|
||||||
|
│ ├── mongo/ # MongoDB utilities
|
||||||
|
│ ├── redis/ # Redis utilities
|
||||||
|
│ ├── response/ # HTTP response utilities
|
||||||
|
│ └── xmlparser/ # Generic XML parser
|
||||||
|
├── infra/ # Infrastructure configuration
|
||||||
|
└── docs/ # Documentation
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🧪 Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
make test
|
||||||
|
|
||||||
|
# Test specific package
|
||||||
|
go test ./pkg/xmlparser/...
|
||||||
|
|
||||||
|
# Test with coverage
|
||||||
|
make test-coverage
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛠️ Development
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
- Go 1.23+
|
- Go 1.23+
|
||||||
- MongoDB 4.4+
|
- MongoDB
|
||||||
- Valid MongoDB connection
|
- Redis (optional)
|
||||||
|
|
||||||
### Project Structure
|
### Local Development
|
||||||
```
|
```bash
|
||||||
tm_back/
|
# Clone repository
|
||||||
├── cmd/web/ # Application entry point
|
git clone <repository-url>
|
||||||
├── internal/ # Private application code
|
cd tm_back
|
||||||
│ ├── customer/ # Customer domain
|
|
||||||
│ └── ... # Other domains
|
# Install dependencies
|
||||||
├── pkg/ # Public packages
|
go mod tidy
|
||||||
│ ├── logger/ # Logging utilities
|
|
||||||
│ ├── mongo/ # MongoDB utilities
|
# Run tests
|
||||||
│ └── response/ # API response utilities
|
make test
|
||||||
├── docs/ # 📚 All documentation
|
|
||||||
└── config.yaml # Configuration file
|
# Start development server
|
||||||
|
make dev
|
||||||
```
|
```
|
||||||
|
|
||||||
## 📞 Support
|
### Adding New Features
|
||||||
|
1. Follow Clean Architecture principles
|
||||||
|
2. Implement proper logging in service layer
|
||||||
|
3. Add comprehensive validation
|
||||||
|
4. Write unit tests
|
||||||
|
5. Update documentation
|
||||||
|
|
||||||
For detailed documentation, guides, and examples, please visit the [`docs/`](./docs/) directory.
|
## 📝 Contributing
|
||||||
|
|
||||||
---
|
1. Fork the repository
|
||||||
|
2. Create a feature branch
|
||||||
|
3. Follow coding standards
|
||||||
|
4. Add tests for new features
|
||||||
|
5. Update documentation
|
||||||
|
6. Submit a pull request
|
||||||
|
|
||||||
**Version**: 1.0.0
|
## 📄 License
|
||||||
**Last Updated**: $(date)
|
|
||||||
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||||
Binary file not shown.
@@ -6,7 +6,6 @@ toolchain go1.24.4
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||||
github.com/google/uuid v1.4.0
|
|
||||||
github.com/labstack/echo/v4 v4.13.4
|
github.com/labstack/echo/v4 v4.13.4
|
||||||
github.com/redis/go-redis/v9 v9.12.0
|
github.com/redis/go-redis/v9 v9.12.0
|
||||||
github.com/spf13/viper v1.18.2
|
github.com/spf13/viper v1.18.2
|
||||||
|
|||||||
@@ -34,8 +34,6 @@ github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
|||||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
|
|
||||||
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||||
|
|||||||
@@ -0,0 +1,307 @@
|
|||||||
|
# Generic XML Parser Package
|
||||||
|
|
||||||
|
This package is a generic XML parser implemented using Go Generics that can parse any XML structure into Go structs.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Generic**: Support for any struct type that implements the `Parseable` interface
|
||||||
|
- **Flexible**: Configurable parsing options for different use cases
|
||||||
|
- **Validation**: Automatic validation of XML and parsed structures
|
||||||
|
- **Metadata Extraction**: Extract XML metadata and parsing information
|
||||||
|
- **Advanced Error Handling**: Detailed error and warning reporting
|
||||||
|
- **TED XML Support**: Pre-built models for TED XML formats
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go mod tidy
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Parse XML from URL
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"tm/pkg/xmlparser"
|
||||||
|
"tm/pkg/xmlparser/ted"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Create parser for Contract Notice
|
||||||
|
parser := xmlparser.NewParser[ted.ContractNotice](nil)
|
||||||
|
|
||||||
|
// Fetch XML from TED website
|
||||||
|
resp, err := http.Get("https://ted.europa.eu/en/notice/447983-2025/xml")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// Parse XML
|
||||||
|
notice, err := parser.Parse(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Notice ID: %s\n", notice.GetID())
|
||||||
|
fmt.Printf("Notice Type: %s\n", notice.GetNoticeType())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parse XML from String
|
||||||
|
|
||||||
|
```go
|
||||||
|
xmlData := `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ContractNotice xmlns="urn:oasis:names:specification:ubl:schema:xsd:ContractNotice-2">
|
||||||
|
<ID>sample-notice-123</ID>
|
||||||
|
<ContractFolderID>sample-folder-456</ContractFolderID>
|
||||||
|
<IssueDate>2025-01-09+02:00</IssueDate>
|
||||||
|
<NoticeTypeCode>cn-standard</NoticeTypeCode>
|
||||||
|
<NoticeLanguageCode>ENG</NoticeLanguageCode>
|
||||||
|
</ContractNotice>`
|
||||||
|
|
||||||
|
parser := xmlparser.NewParser[ted.ContractNotice](nil)
|
||||||
|
notice, err := parser.ParseString(xmlData)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Parsed Notice ID: %s\n", notice.GetID())
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Interfaces
|
||||||
|
|
||||||
|
### Parseable Interface
|
||||||
|
|
||||||
|
Every struct to be parsed must implement this interface:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Parseable interface {
|
||||||
|
Validate() error // Validate the parsed structure
|
||||||
|
GetID() string // Return unique identifier
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parser Interface
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Parser[T Parseable] interface {
|
||||||
|
Parse(reader io.Reader) (*T, error)
|
||||||
|
ParseString(xmlData string) (*T, error)
|
||||||
|
ParseBytes(xmlData []byte) (*T, error)
|
||||||
|
Validate(reader io.Reader) error
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parser Configuration
|
||||||
|
|
||||||
|
```go
|
||||||
|
options := &xmlparser.ParserOptions{
|
||||||
|
StrictValidation: true, // Strict XML validation
|
||||||
|
AllowEmptyElements: true, // Allow empty XML elements
|
||||||
|
PreserveWhitespace: false, // Preserve whitespace
|
||||||
|
IgnoreUnknownElements: false, // Ignore unknown XML elements
|
||||||
|
MaxDepth: 100, // Maximum nesting depth
|
||||||
|
MaxSize: 10 * 1024 * 1024, // Maximum XML size (10MB)
|
||||||
|
}
|
||||||
|
|
||||||
|
parser := xmlparser.NewParser[ted.ContractNotice](options)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parse with Detailed Results
|
||||||
|
|
||||||
|
```go
|
||||||
|
parser := xmlparser.NewParser[ted.ContractNotice](nil).(*xmlparser.GenericParser[ted.ContractNotice])
|
||||||
|
|
||||||
|
result, err := parser.ParseWithResult(strings.NewReader(xmlData))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Parse error: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display parsed information
|
||||||
|
if result.Data != nil {
|
||||||
|
fmt.Printf("Notice ID: %s\n", result.Data.GetID())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display metadata
|
||||||
|
if result.Metadata != nil {
|
||||||
|
fmt.Printf("XML Version: %s\n", result.Metadata.Version)
|
||||||
|
fmt.Printf("Encoding: %s\n", result.Metadata.Encoding)
|
||||||
|
fmt.Printf("Namespace: %s\n", result.Metadata.Namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display errors and warnings
|
||||||
|
for _, err := range result.Errors {
|
||||||
|
fmt.Printf("Error: %s\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, warning := range result.Warnings {
|
||||||
|
fmt.Printf("Warning: %s\n", warning)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## TED XML Models
|
||||||
|
|
||||||
|
The package includes ready-to-use models for TED XML formats:
|
||||||
|
|
||||||
|
### Contract Notice
|
||||||
|
|
||||||
|
```go
|
||||||
|
type ContractNotice struct {
|
||||||
|
ID string
|
||||||
|
ContractFolderID string
|
||||||
|
IssueDate string
|
||||||
|
NoticeTypeCode string
|
||||||
|
NoticeLanguageCode string
|
||||||
|
ProcurementProject *ProcurementProject
|
||||||
|
// ... other fields
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Creating Custom Models
|
||||||
|
|
||||||
|
To create custom models, implement the `Parseable` interface:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type CustomModel struct {
|
||||||
|
XMLName xml.Name `xml:"CustomElement"`
|
||||||
|
ID string `xml:"id,attr"`
|
||||||
|
Name string `xml:"name"`
|
||||||
|
Value string `xml:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c CustomModel) Validate() error {
|
||||||
|
if c.ID == "" {
|
||||||
|
return fmt.Errorf("ID is required")
|
||||||
|
}
|
||||||
|
if c.Name == "" {
|
||||||
|
return fmt.Errorf("Name is required")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c CustomModel) GetID() string {
|
||||||
|
return c.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
parser := xmlparser.NewParser[CustomModel](nil)
|
||||||
|
result, err := parser.ParseString(`<CustomElement id="123"><name>Test</name><value>Sample</value></CustomElement>`)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Factory Function for Creating Parsers
|
||||||
|
|
||||||
|
```go
|
||||||
|
func CreateTEDParser[T xmlparser.Parseable](options *xmlparser.ParserOptions) xmlparser.Parser[T] {
|
||||||
|
return xmlparser.NewParser[T](options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
contractParser := CreateTEDParser[ted.ContractNotice](nil)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Practical Examples
|
||||||
|
|
||||||
|
The `example/` folder contains complete usage examples:
|
||||||
|
|
||||||
|
- Parse from URL
|
||||||
|
- Parse from String
|
||||||
|
- Using different parser options
|
||||||
|
- Parse with detailed results
|
||||||
|
|
||||||
|
To run the examples:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd pkg/xmlparser/example
|
||||||
|
go run main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
pkg/xmlparser/
|
||||||
|
├── interfaces.go # Interface definitions
|
||||||
|
├── parser.go # Generic parser implementation
|
||||||
|
├── utils.go # Utility functions
|
||||||
|
├── parser_test.go # Comprehensive tests
|
||||||
|
├── ted/
|
||||||
|
│ └── models.go # TED XML models
|
||||||
|
├── example/
|
||||||
|
│ └── main.go # Usage examples
|
||||||
|
└── README.md # This file
|
||||||
|
```
|
||||||
|
|
||||||
|
## TED XML Support
|
||||||
|
|
||||||
|
This package is specifically designed for parsing TED (Tenders Electronic Daily) XML:
|
||||||
|
|
||||||
|
- **Contract Notice**: Tender announcements
|
||||||
|
- **Prior Information Notice**: Preliminary notices
|
||||||
|
- **Contract Award Notice**: Award announcements
|
||||||
|
|
||||||
|
### Supported Formats
|
||||||
|
|
||||||
|
- eForms XML (new TED version)
|
||||||
|
- TED Schema XML (legacy version)
|
||||||
|
- UBL 2.3 with eForms Extensions
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
The parser supports various types of errors:
|
||||||
|
|
||||||
|
```go
|
||||||
|
result, err := parser.ParseString(invalidXML)
|
||||||
|
if err != nil {
|
||||||
|
// General parsing error
|
||||||
|
fmt.Printf("Parse failed: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detailed errors in result
|
||||||
|
for _, parseError := range result.Errors {
|
||||||
|
fmt.Printf("Parse error: %s\n", parseError)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warnings
|
||||||
|
for _, warning := range result.Warnings {
|
||||||
|
fmt.Printf("Warning: %s\n", warning)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Optimization
|
||||||
|
|
||||||
|
- XML size limits (default: 10MB)
|
||||||
|
- Nesting depth limits (default: 100 levels)
|
||||||
|
- Stream parsing for large files
|
||||||
|
- Memory-efficient parsing
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Run comprehensive tests:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
go test ./...
|
||||||
|
|
||||||
|
# Run with verbose output
|
||||||
|
go test -v ./...
|
||||||
|
|
||||||
|
# Run benchmarks
|
||||||
|
go test -bench=. ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Utility Functions
|
||||||
|
|
||||||
|
The package includes helpful utility functions:
|
||||||
|
|
||||||
|
- `SanitizeXMLString()` - Clean invalid XML characters
|
||||||
|
- `ValidateXMLName()` - Validate XML element names
|
||||||
|
- `IsValidNamespace()` - Validate namespace URIs
|
||||||
|
- `NormalizeWhitespace()` - Normalize text content
|
||||||
|
- `TruncateString()` - Truncate strings for display
|
||||||
|
|
||||||
@@ -0,0 +1,350 @@
|
|||||||
|
# XML Parser Test Suite
|
||||||
|
|
||||||
|
This document describes the comprehensive test suite for the `xmlparser` package and related TED XML models.
|
||||||
|
|
||||||
|
## 📁 Test Files Overview
|
||||||
|
|
||||||
|
### Core Package Tests (`pkg/xmlparser/`)
|
||||||
|
|
||||||
|
- **`interfaces_test.go`** - Tests for interfaces, types, and basic structures
|
||||||
|
- **`parser_test.go`** - Tests for XML parsing functionality and core parser logic
|
||||||
|
- **`utils_test.go`** - Tests for utility functions and error handling
|
||||||
|
- **`benchmark_test.go`** - Performance benchmarks and memory allocation tests
|
||||||
|
|
||||||
|
### TED Models Tests (`ted/`)
|
||||||
|
|
||||||
|
- **`ted_test.go`** - Tests for TED XML models and validation logic
|
||||||
|
|
||||||
|
### Test Infrastructure
|
||||||
|
|
||||||
|
- **`run_tests.sh`** - Comprehensive test runner script with coverage analysis
|
||||||
|
- **`TEST_SUITE.md`** - This documentation file
|
||||||
|
|
||||||
|
## 🧪 Test Categories
|
||||||
|
|
||||||
|
### 1. Interface and Type Tests (`interfaces_test.go`)
|
||||||
|
|
||||||
|
- **Default Parser Options**: Validates default configuration values
|
||||||
|
- **XML Metadata**: Tests metadata extraction and structure
|
||||||
|
- **Parse Results**: Validates result structure and error handling
|
||||||
|
- **Mock Implementation**: Tests the `MockParseable` test helper
|
||||||
|
- **Parser Interface**: Ensures interface compliance and method availability
|
||||||
|
|
||||||
|
**Key Test Functions:**
|
||||||
|
- `TestDefaultParserOptions()`
|
||||||
|
- `TestXMLMetadata()`
|
||||||
|
- `TestParseResult()`
|
||||||
|
- `TestMockParseable()`
|
||||||
|
- `TestParserInterface()`
|
||||||
|
|
||||||
|
### 2. Parser Functionality Tests (`parser_test.go`)
|
||||||
|
|
||||||
|
- **Parser Creation**: Tests parser instantiation with various options
|
||||||
|
- **XML Parsing**: Tests parsing from string, bytes, and readers
|
||||||
|
- **Validation**: Tests XML structure validation and depth limits
|
||||||
|
- **Size Constraints**: Tests parsing with size and depth limitations
|
||||||
|
- **Error Scenarios**: Tests handling of malformed XML and validation failures
|
||||||
|
- **Metadata Extraction**: Tests XML metadata extraction (version, encoding, namespace)
|
||||||
|
- **Concurrent Parsing**: Tests thread safety and concurrent access
|
||||||
|
|
||||||
|
**Key Test Functions:**
|
||||||
|
- `TestNewParser()`
|
||||||
|
- `TestParseString()` / `TestParseBytes()` / `TestParse()`
|
||||||
|
- `TestValidate()`
|
||||||
|
- `TestParseWithResult()`
|
||||||
|
- `TestConcurrentParsing()`
|
||||||
|
|
||||||
|
### 3. Utility Function Tests (`utils_test.go`)
|
||||||
|
|
||||||
|
- **Validation Errors**: Tests error structures and formatting
|
||||||
|
- **XML Sanitization**: Tests removal of invalid XML characters
|
||||||
|
- **XML Name Validation**: Tests XML element name validation rules
|
||||||
|
- **Namespace Validation**: Tests XML namespace URI validation
|
||||||
|
- **String Utilities**: Tests whitespace normalization and string truncation
|
||||||
|
- **Error Types**: Tests error interface implementations
|
||||||
|
|
||||||
|
**Key Test Functions:**
|
||||||
|
- `TestValidationError()` / `TestValidationErrors()`
|
||||||
|
- `TestXMLParseError()`
|
||||||
|
- `TestSanitizeXMLString()`
|
||||||
|
- `TestValidateXMLName()`
|
||||||
|
- `TestIsValidNamespace()`
|
||||||
|
- `TestNormalizeWhitespace()`
|
||||||
|
- `TestTruncateString()`
|
||||||
|
|
||||||
|
### 4. TED Model Tests (`ted/ted_test.go`)
|
||||||
|
|
||||||
|
- **Contract Notice Validation**: Tests validation rules and business logic
|
||||||
|
- **Getter Methods**: Tests all getter methods for extracting data from XML
|
||||||
|
- **Extension Handling**: Tests handling of UBL extensions and eForms data
|
||||||
|
- **Complex Scenarios**: Tests nested structures and edge cases
|
||||||
|
- **Date/Time Validation**: Tests various date format validations
|
||||||
|
- **Currency and Duration**: Tests validation of currency codes and duration units
|
||||||
|
|
||||||
|
**Key Test Functions:**
|
||||||
|
- `TestContractNoticeValidation()`
|
||||||
|
- `TestContractNoticeBasicGetters()`
|
||||||
|
- `TestContractNoticeExtensionGetters()`
|
||||||
|
- `TestContractNoticeProcurementDetails()`
|
||||||
|
- `TestValidDateFormats()`
|
||||||
|
|
||||||
|
### 5. Performance Benchmarks (`benchmark_test.go`)
|
||||||
|
|
||||||
|
- **Parsing Performance**: Benchmarks for different XML sizes (small, medium, large)
|
||||||
|
- **Memory Allocation**: Tests memory usage patterns during parsing
|
||||||
|
- **Utility Performance**: Benchmarks for utility functions
|
||||||
|
- **Concurrent Performance**: Tests performance under concurrent load
|
||||||
|
- **Validation Performance**: Benchmarks for different validation scenarios
|
||||||
|
|
||||||
|
**Key Benchmark Functions:**
|
||||||
|
- `BenchmarkParseString()` / `BenchmarkParseBytes()`
|
||||||
|
- `BenchmarkParseSmallXML()` / `BenchmarkParseMediumXML()` / `BenchmarkParseLargeXML()`
|
||||||
|
- `BenchmarkConcurrentParsing()`
|
||||||
|
- `BenchmarkMemoryAllocation()`
|
||||||
|
|
||||||
|
## 🚀 Running Tests
|
||||||
|
|
||||||
|
### Quick Test Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
go test -v ./...
|
||||||
|
|
||||||
|
# Run tests with coverage
|
||||||
|
go test -v -cover ./...
|
||||||
|
|
||||||
|
# Run specific test category
|
||||||
|
go test -v -run=TestParser
|
||||||
|
go test -v -run=TestValidation
|
||||||
|
go test -v -run=TestUtils
|
||||||
|
```
|
||||||
|
|
||||||
|
### Comprehensive Test Suite
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run the complete test suite with coverage and benchmarks
|
||||||
|
cd pkg/xmlparser
|
||||||
|
./run_tests.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Benchmark Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all benchmarks
|
||||||
|
go test -bench=. -benchmem
|
||||||
|
|
||||||
|
# Run specific benchmarks
|
||||||
|
go test -bench=BenchmarkParse -benchmem
|
||||||
|
go test -bench=BenchmarkConcurrent -benchmem
|
||||||
|
|
||||||
|
# Run benchmarks with profiling
|
||||||
|
go test -bench=. -cpuprofile=cpu.prof -memprofile=mem.prof
|
||||||
|
```
|
||||||
|
|
||||||
|
### Coverage Analysis
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Generate coverage report
|
||||||
|
go test -coverprofile=coverage.out
|
||||||
|
go tool cover -html=coverage.out -o coverage.html
|
||||||
|
|
||||||
|
# View coverage in terminal
|
||||||
|
go tool cover -func=coverage.out
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 Test Statistics
|
||||||
|
|
||||||
|
### Coverage Targets
|
||||||
|
|
||||||
|
- **Overall Coverage**: ≥ 80%
|
||||||
|
- **Critical Paths**: ≥ 95%
|
||||||
|
- **Error Scenarios**: ≥ 90%
|
||||||
|
- **Utility Functions**: ≥ 95%
|
||||||
|
|
||||||
|
### Test Counts (Approximate)
|
||||||
|
|
||||||
|
- **Unit Tests**: 50+ test functions
|
||||||
|
- **Benchmarks**: 20+ benchmark functions
|
||||||
|
- **Test Cases**: 200+ individual test scenarios
|
||||||
|
- **Mock Objects**: Full `MockParseable` implementation
|
||||||
|
|
||||||
|
## 🔧 Test Infrastructure
|
||||||
|
|
||||||
|
### Mock Objects
|
||||||
|
|
||||||
|
**`MockParseable`**: Test implementation of the `Parseable` interface
|
||||||
|
- Supports both valid and invalid states for testing validation
|
||||||
|
- Includes all required XML structure elements
|
||||||
|
- Implements proper validation logic for testing error scenarios
|
||||||
|
|
||||||
|
### Test Utilities
|
||||||
|
|
||||||
|
- **Error Readers**: For testing I/O error scenarios
|
||||||
|
- **XML Test Data**: Various XML samples for different test cases
|
||||||
|
- **Concurrent Test Helpers**: For testing thread safety
|
||||||
|
|
||||||
|
### Test Configuration
|
||||||
|
|
||||||
|
- **Parallel Execution**: Concurrent tests are properly isolated
|
||||||
|
- **Race Detection**: All tests pass with `-race` flag
|
||||||
|
- **Memory Testing**: Benchmarks include memory allocation tracking
|
||||||
|
- **Timeout Handling**: Long-running tests have appropriate timeouts
|
||||||
|
|
||||||
|
## 🚨 Common Test Scenarios
|
||||||
|
|
||||||
|
### Success Cases
|
||||||
|
|
||||||
|
- Valid XML parsing with all data types
|
||||||
|
- Proper validation of business rules
|
||||||
|
- Correct metadata extraction
|
||||||
|
- Successful error handling and recovery
|
||||||
|
|
||||||
|
### Error Cases
|
||||||
|
|
||||||
|
- Malformed XML structure
|
||||||
|
- Invalid business data (missing required fields)
|
||||||
|
- Size and depth limit violations
|
||||||
|
- I/O errors during reading
|
||||||
|
- Validation failures
|
||||||
|
|
||||||
|
### Edge Cases
|
||||||
|
|
||||||
|
- Empty XML documents
|
||||||
|
- Very large XML documents
|
||||||
|
- Deeply nested structures
|
||||||
|
- Unicode and special characters
|
||||||
|
- Concurrent access patterns
|
||||||
|
|
||||||
|
## 📈 Performance Expectations
|
||||||
|
|
||||||
|
### Parsing Performance
|
||||||
|
|
||||||
|
- **Small XML** (< 1KB): < 10μs per operation
|
||||||
|
- **Medium XML** (1-10KB): < 100μs per operation
|
||||||
|
- **Large XML** (10-100KB): < 1ms per operation
|
||||||
|
|
||||||
|
### Memory Usage
|
||||||
|
|
||||||
|
- **Parsing Overhead**: < 3KB per operation
|
||||||
|
- **Memory Allocations**: < 100 allocations per parse
|
||||||
|
- **No Memory Leaks**: All tests pass memory leak detection
|
||||||
|
|
||||||
|
### Concurrent Performance
|
||||||
|
|
||||||
|
- **Thread Safety**: All operations are thread-safe
|
||||||
|
- **Scalability**: Performance scales linearly with CPU cores
|
||||||
|
- **Resource Sharing**: Parsers can be safely shared between goroutines
|
||||||
|
|
||||||
|
## 🔍 Debugging Tests
|
||||||
|
|
||||||
|
### Running Individual Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run specific test with verbose output
|
||||||
|
go test -v -run=TestSpecificFunction
|
||||||
|
|
||||||
|
# Run test with race detection
|
||||||
|
go test -race -run=TestConcurrent
|
||||||
|
|
||||||
|
# Run test with memory profiling
|
||||||
|
go test -run=TestMemory -memprofile=mem.prof
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Debugging Tips
|
||||||
|
|
||||||
|
1. **Use `-v` flag** for verbose output to see individual test progress
|
||||||
|
2. **Use `-race` flag** to detect race conditions in concurrent tests
|
||||||
|
3. **Use `-count=1`** to disable test caching for consistent results
|
||||||
|
4. **Use `-timeout`** to prevent hanging tests in CI environments
|
||||||
|
5. **Check coverage reports** to identify untested code paths
|
||||||
|
|
||||||
|
## 📝 Adding New Tests
|
||||||
|
|
||||||
|
### Guidelines for New Tests
|
||||||
|
|
||||||
|
1. **Follow naming conventions**: `TestFunctionName` for unit tests, `BenchmarkFunctionName` for benchmarks
|
||||||
|
2. **Use table-driven tests** for multiple test cases
|
||||||
|
3. **Include both success and failure scenarios**
|
||||||
|
4. **Add appropriate benchmarks** for performance-critical code
|
||||||
|
5. **Update this documentation** when adding new test categories
|
||||||
|
|
||||||
|
### Test Structure Template
|
||||||
|
|
||||||
|
```go
|
||||||
|
func TestNewFeature(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input InputType
|
||||||
|
expected ExpectedType
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Valid case",
|
||||||
|
input: validInput,
|
||||||
|
expected: expectedOutput,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Error case",
|
||||||
|
input: invalidInput,
|
||||||
|
expected: nil,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result, err := FunctionUnderTest(tt.input)
|
||||||
|
|
||||||
|
if tt.expectError {
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Expected error but got none")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unexpected error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(result, tt.expected) {
|
||||||
|
t.Errorf("Expected %v, got %v", tt.expected, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 Continuous Integration
|
||||||
|
|
||||||
|
### CI Pipeline Tests
|
||||||
|
|
||||||
|
1. **Unit Tests**: All unit tests must pass
|
||||||
|
2. **Race Detection**: Tests must pass with `-race` flag
|
||||||
|
3. **Coverage Check**: Coverage must meet minimum thresholds
|
||||||
|
4. **Benchmark Regression**: Performance must not regress significantly
|
||||||
|
5. **Lint Checks**: Code must pass all linting rules
|
||||||
|
|
||||||
|
### Quality Gates
|
||||||
|
|
||||||
|
- ✅ All tests pass
|
||||||
|
- ✅ Coverage ≥ 80%
|
||||||
|
- ✅ No race conditions detected
|
||||||
|
- ✅ Benchmarks within acceptable range
|
||||||
|
- ✅ No critical linting errors
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 Support
|
||||||
|
|
||||||
|
For questions about the test suite or adding new tests, please refer to:
|
||||||
|
|
||||||
|
1. **Code Comments**: Inline documentation in test files
|
||||||
|
2. **README.md**: Main package documentation
|
||||||
|
3. **Architecture Documentation**: Project-wide coding standards
|
||||||
|
4. **Team Knowledge Base**: Internal development guidelines
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*This test suite ensures the reliability, performance, and maintainability of the XML parser package and related TED models.*
|
||||||
@@ -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,70 @@
|
|||||||
|
package xmlparser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Parsable defines the interface that models must implement to be parseable
|
||||||
|
type Parsable interface {
|
||||||
|
// Validate validates the parsed model
|
||||||
|
Validate() error
|
||||||
|
// GetID returns a unique identifier for the model
|
||||||
|
GetID() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parser defines the generic XML parser interface
|
||||||
|
type Parser[T Parsable] interface {
|
||||||
|
// Parse parses XML from reader into the target type
|
||||||
|
Parse(reader io.Reader) (*T, error)
|
||||||
|
// ParseString parses XML from string into the target type
|
||||||
|
ParseString(xmlData string) (*T, error)
|
||||||
|
// ParseBytes parses XML from byte slice into the target type
|
||||||
|
ParseBytes(xmlData []byte) (*T, error)
|
||||||
|
// Validate validates XML against schema (if supported)
|
||||||
|
Validate(reader io.Reader) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// XMLMetadata contains metadata about the parsed XML
|
||||||
|
type XMLMetadata struct {
|
||||||
|
XMLName xml.Name `xml:""`
|
||||||
|
Namespace string `json:"namespace,omitempty"`
|
||||||
|
Version string `json:"version,omitempty"`
|
||||||
|
Encoding string `json:"encoding,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseResult contains the result of XML parsing
|
||||||
|
type ParseResult[T Parsable] struct {
|
||||||
|
Data *T `json:"data"`
|
||||||
|
Metadata *XMLMetadata `json:"metadata,omitempty"`
|
||||||
|
Errors []string `json:"errors,omitempty"`
|
||||||
|
Warnings []string `json:"warnings,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParserOptions configures the parser behavior
|
||||||
|
type ParserOptions struct {
|
||||||
|
// StrictValidation enables strict XML validation
|
||||||
|
StrictValidation bool
|
||||||
|
// AllowEmptyElements allows empty XML elements
|
||||||
|
AllowEmptyElements bool
|
||||||
|
// PreserveWhitespace preserves whitespace in text elements
|
||||||
|
PreserveWhitespace bool
|
||||||
|
// IgnoreUnknownElements ignores unknown XML elements
|
||||||
|
IgnoreUnknownElements bool
|
||||||
|
// MaxDepth maximum nesting depth allowed
|
||||||
|
MaxDepth int
|
||||||
|
// MaxSize maximum XML size in bytes
|
||||||
|
MaxSize int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultParserOptions returns default parser options
|
||||||
|
func DefaultParserOptions() *ParserOptions {
|
||||||
|
return &ParserOptions{
|
||||||
|
StrictValidation: true,
|
||||||
|
AllowEmptyElements: true,
|
||||||
|
PreserveWhitespace: false,
|
||||||
|
IgnoreUnknownElements: false,
|
||||||
|
MaxDepth: 100,
|
||||||
|
MaxSize: 10 * 1024 * 1024, // 10MB
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,272 @@
|
|||||||
|
package xmlparser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GenericParser implements the Parser interface using Go generics
|
||||||
|
type GenericParser[T Parsable] struct {
|
||||||
|
options *ParserOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewParser creates a new generic XML parser
|
||||||
|
func NewParser[T Parsable](options *ParserOptions) Parser[T] {
|
||||||
|
if options == nil {
|
||||||
|
options = DefaultParserOptions()
|
||||||
|
}
|
||||||
|
return &GenericParser[T]{
|
||||||
|
options: options,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse parses XML from reader into the target type
|
||||||
|
func (p *GenericParser[T]) Parse(reader io.Reader) (*T, error) {
|
||||||
|
// Read all data from reader
|
||||||
|
data, err := io.ReadAll(reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read XML data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.ParseBytes(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseString parses XML from string into the target type
|
||||||
|
func (p *GenericParser[T]) ParseString(xmlData string) (*T, error) {
|
||||||
|
return p.ParseBytes([]byte(xmlData))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseBytes parses XML from byte slice into the target type
|
||||||
|
func (p *GenericParser[T]) ParseBytes(xmlData []byte) (*T, error) {
|
||||||
|
// Check size limit
|
||||||
|
if p.options.MaxSize > 0 && int64(len(xmlData)) > p.options.MaxSize {
|
||||||
|
return nil, fmt.Errorf("XML size %d exceeds maximum allowed size %d", len(xmlData), p.options.MaxSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new instance of T
|
||||||
|
var result T
|
||||||
|
resultType := reflect.TypeOf(result)
|
||||||
|
|
||||||
|
// Create a pointer to the struct if it's not already a pointer
|
||||||
|
var targetPtr interface{}
|
||||||
|
if resultType.Kind() == reflect.Ptr {
|
||||||
|
// T is already a pointer type
|
||||||
|
targetPtr = reflect.New(resultType.Elem()).Interface()
|
||||||
|
} else {
|
||||||
|
// T is a value type, create a pointer to it
|
||||||
|
targetPtr = reflect.New(resultType).Interface()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check depth limit before parsing if specified
|
||||||
|
if p.options.MaxDepth > 0 {
|
||||||
|
if err := p.checkDepthLimit(xmlData); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create XML decoder
|
||||||
|
decoder := xml.NewDecoder(bytes.NewReader(xmlData))
|
||||||
|
|
||||||
|
// Configure decoder options
|
||||||
|
if !p.options.PreserveWhitespace {
|
||||||
|
decoder.CharsetReader = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.options.StrictValidation {
|
||||||
|
decoder.Strict = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse XML into the pointer
|
||||||
|
err := decoder.Decode(targetPtr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to decode XML: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert back to the target type
|
||||||
|
var parsedResult T
|
||||||
|
if resultType.Kind() == reflect.Ptr {
|
||||||
|
// T is a pointer type, so targetPtr is already T
|
||||||
|
parsedResult = targetPtr.(T)
|
||||||
|
} else {
|
||||||
|
// T is a value type, so dereference the pointer
|
||||||
|
parsedResult = reflect.ValueOf(targetPtr).Elem().Interface().(T)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the parsed result
|
||||||
|
if err := parsedResult.Validate(); err != nil {
|
||||||
|
return nil, fmt.Errorf("validation failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &parsedResult, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates XML against schema (basic validation for now)
|
||||||
|
func (p *GenericParser[T]) Validate(reader io.Reader) error {
|
||||||
|
data, err := io.ReadAll(reader)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read XML data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for empty data
|
||||||
|
if len(data) == 0 {
|
||||||
|
return fmt.Errorf("XML data is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic XML well-formedness validation
|
||||||
|
decoder := xml.NewDecoder(bytes.NewReader(data))
|
||||||
|
|
||||||
|
// Count depth for max depth validation
|
||||||
|
depth := 0
|
||||||
|
maxDepthReached := 0
|
||||||
|
|
||||||
|
for {
|
||||||
|
token, err := decoder.Token()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("XML validation failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch token.(type) {
|
||||||
|
case xml.StartElement:
|
||||||
|
depth++
|
||||||
|
if depth > maxDepthReached {
|
||||||
|
maxDepthReached = depth
|
||||||
|
}
|
||||||
|
if p.options.MaxDepth > 0 && 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkDepthLimit validates that XML doesn't exceed the maximum depth limit
|
||||||
|
func (p *GenericParser[T]) checkDepthLimit(xmlData []byte) error {
|
||||||
|
decoder := xml.NewDecoder(bytes.NewReader(xmlData))
|
||||||
|
depth := 0
|
||||||
|
maxDepthReached := 0
|
||||||
|
|
||||||
|
for {
|
||||||
|
token, err := decoder.Token()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("XML depth validation failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch token.(type) {
|
||||||
|
case xml.StartElement:
|
||||||
|
depth++
|
||||||
|
if depth > maxDepthReached {
|
||||||
|
maxDepthReached = depth
|
||||||
|
}
|
||||||
|
if depth > p.options.MaxDepth {
|
||||||
|
return fmt.Errorf("XML depth %d exceeds maximum allowed depth %d", depth, p.options.MaxDepth)
|
||||||
|
}
|
||||||
|
case xml.EndElement:
|
||||||
|
depth--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseWithResult parses XML and returns detailed result with metadata
|
||||||
|
func (p *GenericParser[T]) ParseWithResult(reader io.Reader) (*ParseResult[T], error) {
|
||||||
|
result := &ParseResult[T]{
|
||||||
|
Metadata: &XMLMetadata{},
|
||||||
|
Errors: []string{},
|
||||||
|
Warnings: []string{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read all data
|
||||||
|
data, err := io.ReadAll(reader)
|
||||||
|
if err != nil {
|
||||||
|
result.Errors = append(result.Errors, fmt.Sprintf("Failed to read XML data: %v", err))
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract XML metadata
|
||||||
|
if err := p.extractMetadata(data, result.Metadata); err != nil {
|
||||||
|
result.Warnings = append(result.Warnings, fmt.Sprintf("Failed to extract metadata: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the XML
|
||||||
|
parsed, err := p.ParseBytes(data)
|
||||||
|
if err != nil {
|
||||||
|
result.Errors = append(result.Errors, err.Error())
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Data = parsed
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractMetadata extracts basic metadata from XML
|
||||||
|
func (p *GenericParser[T]) extractMetadata(xmlData []byte, metadata *XMLMetadata) error {
|
||||||
|
// Basic extraction of XML declaration and root element
|
||||||
|
xmlStr := string(xmlData)
|
||||||
|
|
||||||
|
// Extract encoding from XML declaration
|
||||||
|
if strings.HasPrefix(xmlStr, "<?xml") {
|
||||||
|
endIdx := strings.Index(xmlStr, "?>")
|
||||||
|
if endIdx > 0 {
|
||||||
|
declaration := xmlStr[:endIdx+2]
|
||||||
|
|
||||||
|
// Extract encoding
|
||||||
|
if encodingIdx := strings.Index(declaration, "encoding="); encodingIdx > 0 {
|
||||||
|
start := encodingIdx + 9
|
||||||
|
if start < len(declaration) {
|
||||||
|
quote := declaration[start]
|
||||||
|
if quote == '"' || quote == '\'' {
|
||||||
|
end := strings.IndexByte(declaration[start+1:], byte(quote))
|
||||||
|
if end > 0 {
|
||||||
|
metadata.Encoding = declaration[start+1 : start+1+end]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract version
|
||||||
|
if versionIdx := strings.Index(declaration, "version="); versionIdx > 0 {
|
||||||
|
start := versionIdx + 8
|
||||||
|
if start < len(declaration) {
|
||||||
|
quote := declaration[start]
|
||||||
|
if quote == '"' || quote == '\'' {
|
||||||
|
end := strings.IndexByte(declaration[start+1:], byte(quote))
|
||||||
|
if end > 0 {
|
||||||
|
metadata.Version = declaration[start+1 : start+1+end]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract root element namespace
|
||||||
|
decoder := xml.NewDecoder(bytes.NewReader(xmlData))
|
||||||
|
for {
|
||||||
|
token, err := decoder.Token()
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if startElement, ok := token.(xml.StartElement); ok {
|
||||||
|
metadata.XMLName = startElement.Name
|
||||||
|
metadata.Namespace = startElement.Name.Space
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -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,158 @@
|
|||||||
|
package xmlparser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ValidationError represents a validation error with details
|
||||||
|
type ValidationError struct {
|
||||||
|
Field string `json:"field"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Value string `json:"value,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ve ValidationError) Error() string {
|
||||||
|
if ve.Value != "" {
|
||||||
|
return fmt.Sprintf("validation error in field '%s': %s (value: %s)", ve.Field, ve.Message, ve.Value)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("validation error in field '%s': %s", ve.Field, ve.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidationErrors represents multiple validation errors
|
||||||
|
type ValidationErrors []ValidationError
|
||||||
|
|
||||||
|
func (ve ValidationErrors) Error() string {
|
||||||
|
if len(ve) == 0 {
|
||||||
|
return "no validation errors"
|
||||||
|
}
|
||||||
|
|
||||||
|
var messages []string
|
||||||
|
for _, err := range ve {
|
||||||
|
messages = append(messages, err.Error())
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("validation failed with %d error(s): %s", len(ve), strings.Join(messages, "; "))
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEmpty returns true if there are no validation errors
|
||||||
|
func (ve ValidationErrors) IsEmpty() bool {
|
||||||
|
return len(ve) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add adds a validation error
|
||||||
|
func (ve *ValidationErrors) Add(field, message, value string) {
|
||||||
|
*ve = append(*ve, ValidationError{
|
||||||
|
Field: field,
|
||||||
|
Message: message,
|
||||||
|
Value: value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// XMLParseError represents an XML parsing error with context
|
||||||
|
type XMLParseError struct {
|
||||||
|
Line int `json:"line,omitempty"`
|
||||||
|
Column int `json:"column,omitempty"`
|
||||||
|
Element string `json:"element,omitempty"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Original error `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (xpe XMLParseError) Error() string {
|
||||||
|
var parts []string
|
||||||
|
|
||||||
|
if xpe.Element != "" {
|
||||||
|
parts = append(parts, fmt.Sprintf("element '%s'", xpe.Element))
|
||||||
|
}
|
||||||
|
|
||||||
|
if xpe.Line > 0 {
|
||||||
|
if xpe.Column > 0 {
|
||||||
|
parts = append(parts, fmt.Sprintf("line %d, column %d", xpe.Line, xpe.Column))
|
||||||
|
} else {
|
||||||
|
parts = append(parts, fmt.Sprintf("line %d", xpe.Line))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context := ""
|
||||||
|
if len(parts) > 0 {
|
||||||
|
context = " (" + strings.Join(parts, ", ") + ")"
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("XML parse error%s: %s", context, xpe.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unwrap returns the original error
|
||||||
|
func (xpe XMLParseError) Unwrap() error {
|
||||||
|
return xpe.Original
|
||||||
|
}
|
||||||
|
|
||||||
|
// SanitizeXMLString removes invalid XML characters and cleans the string
|
||||||
|
func SanitizeXMLString(s string) string {
|
||||||
|
// Remove control characters except tab, newline, and carriage return
|
||||||
|
var result strings.Builder
|
||||||
|
for _, r := range s {
|
||||||
|
if (r >= 0x20 && r <= 0xD7FF) ||
|
||||||
|
(r >= 0xE000 && r <= 0xFFFD) ||
|
||||||
|
(r >= 0x10000 && r <= 0x10FFFF) ||
|
||||||
|
r == 0x09 || r == 0x0A || r == 0x0D {
|
||||||
|
result.WriteRune(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateXMLName checks if a string is a valid XML name
|
||||||
|
func ValidateXMLName(name string) bool {
|
||||||
|
if name == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// XML names must start with a letter or underscore
|
||||||
|
first := []rune(name)[0]
|
||||||
|
if !((first >= 'A' && first <= 'Z') ||
|
||||||
|
(first >= 'a' && first <= 'z') ||
|
||||||
|
first == '_') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subsequent characters can be letters, digits, hyphens, periods, or underscores
|
||||||
|
for _, r := range []rune(name)[1:] {
|
||||||
|
if !((r >= 'A' && r <= 'Z') ||
|
||||||
|
(r >= 'a' && r <= 'z') ||
|
||||||
|
(r >= '0' && r <= '9') ||
|
||||||
|
r == '-' || r == '.' || r == '_') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValidNamespace checks if a string is a valid XML namespace URI
|
||||||
|
func IsValidNamespace(namespace string) bool {
|
||||||
|
if namespace == "" {
|
||||||
|
return true // Empty namespace is valid
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic URI validation - should start with http:// or https:// or urn:
|
||||||
|
return strings.HasPrefix(namespace, "http://") ||
|
||||||
|
strings.HasPrefix(namespace, "https://") ||
|
||||||
|
strings.HasPrefix(namespace, "urn:")
|
||||||
|
}
|
||||||
|
|
||||||
|
// NormalizeWhitespace normalizes whitespace in XML text content
|
||||||
|
func NormalizeWhitespace(s string) string {
|
||||||
|
// Replace all whitespace sequences with single spaces
|
||||||
|
fields := strings.Fields(s)
|
||||||
|
return strings.Join(fields, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TruncateString truncates a string to maxLength and adds "..." if needed
|
||||||
|
func TruncateString(s string, maxLength int) string {
|
||||||
|
if len(s) <= maxLength {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
if maxLength <= 3 {
|
||||||
|
return s[:maxLength]
|
||||||
|
}
|
||||||
|
return s[:maxLength-3] + "..."
|
||||||
|
}
|
||||||
@@ -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