279 lines
8.4 KiB
Go
279 lines
8.4 KiB
Go
// Package integration provides CI helper functions for BACKBEAT contract testing
|
|
package integration
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// CIHelper provides utilities for continuous integration testing
|
|
type CIHelper struct {
|
|
validator *MessageValidator
|
|
}
|
|
|
|
// NewCIHelper creates a new CI helper with a message validator
|
|
func NewCIHelper(schemaDir string) (*CIHelper, error) {
|
|
validator, err := NewMessageValidator(schemaDir)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create validator: %w", err)
|
|
}
|
|
|
|
return &CIHelper{
|
|
validator: validator,
|
|
}, nil
|
|
}
|
|
|
|
// ValidateDirectory validates all JSON files in a directory against BACKBEAT schemas
|
|
func (ci *CIHelper) ValidateDirectory(dir string) (*DirectoryValidationResult, error) {
|
|
result := &DirectoryValidationResult{
|
|
Directory: dir,
|
|
Files: make(map[string]*FileValidationResult),
|
|
}
|
|
|
|
err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Skip non-JSON files
|
|
if d.IsDir() || !strings.HasSuffix(strings.ToLower(path), ".json") {
|
|
return nil
|
|
}
|
|
|
|
fileResult, validateErr := ci.validateFile(path)
|
|
if validateErr != nil {
|
|
result.Errors = append(result.Errors, fmt.Sprintf("Failed to validate %s: %v", path, validateErr))
|
|
} else {
|
|
relPath, _ := filepath.Rel(dir, path)
|
|
result.Files[relPath] = fileResult
|
|
result.TotalFiles++
|
|
if fileResult.AllValid {
|
|
result.ValidFiles++
|
|
} else {
|
|
result.InvalidFiles++
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to walk directory: %w", err)
|
|
}
|
|
|
|
result.ValidationRate = float64(result.ValidFiles) / float64(result.TotalFiles)
|
|
return result, nil
|
|
}
|
|
|
|
// validateFile validates a single JSON file
|
|
func (ci *CIHelper) validateFile(filePath string) (*FileValidationResult, error) {
|
|
data, err := os.ReadFile(filePath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read file: %w", err)
|
|
}
|
|
|
|
result := &FileValidationResult{
|
|
FilePath: filePath,
|
|
AllValid: true,
|
|
}
|
|
|
|
// Try to parse as single message first
|
|
var singleMessage map[string]interface{}
|
|
if err := json.Unmarshal(data, &singleMessage); err == nil {
|
|
if msgType, hasType := singleMessage["type"].(string); hasType && ci.validator.IsMessageTypeSupported(msgType) {
|
|
// Single BACKBEAT message
|
|
validationResult, validateErr := ci.validator.ValidateMessage(data)
|
|
if validateErr != nil {
|
|
return nil, validateErr
|
|
}
|
|
result.Messages = []*ValidationResult{validationResult}
|
|
result.AllValid = validationResult.Valid
|
|
return result, nil
|
|
}
|
|
}
|
|
|
|
// Try to parse as array of messages
|
|
var messageArray []map[string]interface{}
|
|
if err := json.Unmarshal(data, &messageArray); err == nil {
|
|
for i, msg := range messageArray {
|
|
msgBytes, marshalErr := json.Marshal(msg)
|
|
if marshalErr != nil {
|
|
result.Errors = append(result.Errors, fmt.Sprintf("Message %d: failed to marshal: %v", i, marshalErr))
|
|
result.AllValid = false
|
|
continue
|
|
}
|
|
|
|
validationResult, validateErr := ci.validator.ValidateMessage(msgBytes)
|
|
if validateErr != nil {
|
|
result.Errors = append(result.Errors, fmt.Sprintf("Message %d: validation error: %v", i, validateErr))
|
|
result.AllValid = false
|
|
continue
|
|
}
|
|
|
|
result.Messages = append(result.Messages, validationResult)
|
|
if !validationResult.Valid {
|
|
result.AllValid = false
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// Try to parse as examples format (array with description and message fields)
|
|
var examples []ExampleMessage
|
|
if err := json.Unmarshal(data, &examples); err == nil {
|
|
for i, example := range examples {
|
|
msgBytes, marshalErr := json.Marshal(example.Message)
|
|
if marshalErr != nil {
|
|
result.Errors = append(result.Errors, fmt.Sprintf("Example %d (%s): failed to marshal: %v", i, example.Description, marshalErr))
|
|
result.AllValid = false
|
|
continue
|
|
}
|
|
|
|
validationResult, validateErr := ci.validator.ValidateMessage(msgBytes)
|
|
if validateErr != nil {
|
|
result.Errors = append(result.Errors, fmt.Sprintf("Example %d (%s): validation error: %v", i, example.Description, validateErr))
|
|
result.AllValid = false
|
|
continue
|
|
}
|
|
|
|
result.Messages = append(result.Messages, validationResult)
|
|
if !validationResult.Valid {
|
|
result.AllValid = false
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
return nil, fmt.Errorf("file does not contain valid JSON message format")
|
|
}
|
|
|
|
// ExampleMessage represents a message example with description
|
|
type ExampleMessage struct {
|
|
Description string `json:"description"`
|
|
Message map[string]interface{} `json:"message"`
|
|
}
|
|
|
|
// DirectoryValidationResult contains results for validating a directory
|
|
type DirectoryValidationResult struct {
|
|
Directory string `json:"directory"`
|
|
TotalFiles int `json:"total_files"`
|
|
ValidFiles int `json:"valid_files"`
|
|
InvalidFiles int `json:"invalid_files"`
|
|
ValidationRate float64 `json:"validation_rate"`
|
|
Files map[string]*FileValidationResult `json:"files"`
|
|
Errors []string `json:"errors,omitempty"`
|
|
}
|
|
|
|
// FileValidationResult contains results for validating a single file
|
|
type FileValidationResult struct {
|
|
FilePath string `json:"file_path"`
|
|
AllValid bool `json:"all_valid"`
|
|
Messages []*ValidationResult `json:"messages"`
|
|
Errors []string `json:"errors,omitempty"`
|
|
}
|
|
|
|
// GenerateCIReport generates a formatted report suitable for CI systems
|
|
func (ci *CIHelper) GenerateCIReport(result *DirectoryValidationResult) string {
|
|
var sb strings.Builder
|
|
|
|
sb.WriteString("BACKBEAT Contract Validation Report\n")
|
|
sb.WriteString("===================================\n\n")
|
|
|
|
sb.WriteString(fmt.Sprintf("Directory: %s\n", result.Directory))
|
|
sb.WriteString(fmt.Sprintf("Total Files: %d\n", result.TotalFiles))
|
|
sb.WriteString(fmt.Sprintf("Valid Files: %d\n", result.ValidFiles))
|
|
sb.WriteString(fmt.Sprintf("Invalid Files: %d\n", result.InvalidFiles))
|
|
sb.WriteString(fmt.Sprintf("Validation Rate: %.2f%%\n\n", result.ValidationRate*100))
|
|
|
|
if len(result.Errors) > 0 {
|
|
sb.WriteString("Directory-level Errors:\n")
|
|
for _, err := range result.Errors {
|
|
sb.WriteString(fmt.Sprintf(" - %s\n", err))
|
|
}
|
|
sb.WriteString("\n")
|
|
}
|
|
|
|
// Group files by validation status
|
|
validFiles := make([]string, 0)
|
|
invalidFiles := make([]string, 0)
|
|
|
|
for filePath, fileResult := range result.Files {
|
|
if fileResult.AllValid {
|
|
validFiles = append(validFiles, filePath)
|
|
} else {
|
|
invalidFiles = append(invalidFiles, filePath)
|
|
}
|
|
}
|
|
|
|
if len(validFiles) > 0 {
|
|
sb.WriteString("Valid Files:\n")
|
|
for _, file := range validFiles {
|
|
sb.WriteString(fmt.Sprintf(" ✓ %s\n", file))
|
|
}
|
|
sb.WriteString("\n")
|
|
}
|
|
|
|
if len(invalidFiles) > 0 {
|
|
sb.WriteString("Invalid Files:\n")
|
|
for _, file := range invalidFiles {
|
|
fileResult := result.Files[file]
|
|
sb.WriteString(fmt.Sprintf(" ✗ %s\n", file))
|
|
|
|
for _, err := range fileResult.Errors {
|
|
sb.WriteString(fmt.Sprintf(" - %s\n", err))
|
|
}
|
|
|
|
for i, msg := range fileResult.Messages {
|
|
if !msg.Valid {
|
|
sb.WriteString(fmt.Sprintf(" Message %d (%s):\n", i+1, msg.MessageType))
|
|
for _, valErr := range msg.Errors {
|
|
sb.WriteString(fmt.Sprintf(" - %s: %s\n", valErr.Field, valErr.Message))
|
|
}
|
|
}
|
|
}
|
|
sb.WriteString("\n")
|
|
}
|
|
}
|
|
|
|
return sb.String()
|
|
}
|
|
|
|
// ExitWithStatus exits the program with appropriate status code for CI
|
|
func (ci *CIHelper) ExitWithStatus(result *DirectoryValidationResult) {
|
|
if result.InvalidFiles > 0 || len(result.Errors) > 0 {
|
|
fmt.Fprint(os.Stderr, ci.GenerateCIReport(result))
|
|
os.Exit(1)
|
|
} else {
|
|
fmt.Print(ci.GenerateCIReport(result))
|
|
os.Exit(0)
|
|
}
|
|
}
|
|
|
|
// ValidateExamples validates the built-in example messages
|
|
func (ci *CIHelper) ValidateExamples() ([]*ValidationResult, error) {
|
|
examples := ExampleMessages()
|
|
results := make([]*ValidationResult, 0, len(examples))
|
|
|
|
for name, example := range examples {
|
|
result, err := ci.validator.ValidateStruct(example)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to validate example %s: %w", name, err)
|
|
}
|
|
results = append(results, result)
|
|
}
|
|
|
|
return results, nil
|
|
}
|
|
|
|
// GetSchemaInfo returns information about loaded schemas
|
|
func (ci *CIHelper) GetSchemaInfo() map[string]string {
|
|
info := make(map[string]string)
|
|
for _, msgType := range ci.validator.GetSupportedMessageTypes() {
|
|
info[msgType] = getSchemaVersion(msgType)
|
|
}
|
|
return info
|
|
} |