backbeat: add module sources
This commit is contained in:
		
							
								
								
									
										279
									
								
								contracts/tests/integration/ci_helper.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										279
									
								
								contracts/tests/integration/ci_helper.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,279 @@ | ||||
| // 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 | ||||
| } | ||||
		Reference in New Issue
	
	Block a user
	 anthonyrawlins
					anthonyrawlins