// Package onviftesting provides testing utilities for ONVIF client testing. package onviftesting import ( "encoding/json" "fmt" "os" "path/filepath" "time" ) // Registry holds information about all available camera captures. type Registry struct { Version string `json:"version"` LastUpdated time.Time `json:"last_updated"` Cameras []CameraEntry `json:"cameras"` Coverage map[string]Coverage `json:"coverage"` } // CameraEntry represents a single camera in the registry. type CameraEntry struct { ID string `json:"id"` Manufacturer string `json:"manufacturer"` Model string `json:"model"` Firmware string `json:"firmware"` CaptureFile string `json:"capture_file"` CaptureVersion string `json:"capture_version,omitempty"` Capabilities []string `json:"capabilities"` OperationsCaptured int `json:"operations_captured"` ProfileCompliance []string `json:"profile_compliance,omitempty"` TestFile string `json:"test_file,omitempty"` Notes string `json:"notes,omitempty"` AddedDate string `json:"added_date,omitempty"` } // Coverage tracks operation coverage per service. type Coverage struct { Total int `json:"total"` Captured int `json:"captured"` } // RegistryVersion is the current registry format version. const RegistryVersion = "1.0" // DefaultRegistryPath is the default path for the registry file. const DefaultRegistryPath = "testdata/captures/registry.json" // LoadRegistry loads the capture registry from a file. func LoadRegistry(path string) (*Registry, error) { data, err := os.ReadFile(path) if err != nil { if os.IsNotExist(err) { // Return empty registry if file doesn't exist return &Registry{ Version: RegistryVersion, LastUpdated: time.Now(), Cameras: []CameraEntry{}, Coverage: make(map[string]Coverage), }, nil } return nil, fmt.Errorf("failed to read registry: %w", err) } var registry Registry if err := json.Unmarshal(data, ®istry); err != nil { return nil, fmt.Errorf("failed to unmarshal registry: %w", err) } return ®istry, nil } // SaveRegistry saves the registry to a file. func SaveRegistry(registry *Registry, path string) error { registry.LastUpdated = time.Now() data, err := json.MarshalIndent(registry, "", " ") if err != nil { return fmt.Errorf("failed to marshal registry: %w", err) } // Ensure directory exists dir := filepath.Dir(path) if err := os.MkdirAll(dir, 0750); err != nil { //nolint:mnd return fmt.Errorf("failed to create directory: %w", err) } if err := os.WriteFile(path, data, 0600); err != nil { //nolint:mnd return fmt.Errorf("failed to write registry: %w", err) } return nil } // AddCamera adds a new camera to the registry. func (r *Registry) AddCamera(entry CameraEntry) { // Check if camera already exists for i, cam := range r.Cameras { if cam.ID == entry.ID { // Update existing entry r.Cameras[i] = entry return } } // Add new entry if entry.AddedDate == "" { entry.AddedDate = time.Now().Format("2006-01-02") } r.Cameras = append(r.Cameras, entry) } // GetCamera retrieves a camera entry by ID. func (r *Registry) GetCamera(id string) *CameraEntry { for i := range r.Cameras { if r.Cameras[i].ID == id { return &r.Cameras[i] } } return nil } // RemoveCamera removes a camera from the registry. func (r *Registry) RemoveCamera(id string) bool { for i, cam := range r.Cameras { if cam.ID == id { r.Cameras = append(r.Cameras[:i], r.Cameras[i+1:]...) return true } } return false } // GetCamerasByManufacturer returns all cameras from a specific manufacturer. func (r *Registry) GetCamerasByManufacturer(manufacturer string) []CameraEntry { var cameras []CameraEntry for _, cam := range r.Cameras { if cam.Manufacturer == manufacturer { cameras = append(cameras, cam) } } return cameras } // UpdateCoverage updates the coverage statistics based on registered cameras. func (r *Registry) UpdateCoverage() { // Define total operations per service totals := map[string]int{ "Device": len(DeviceReadOperations), "Media": len(MediaReadOperations), "PTZ": len(PTZReadOperations), "Imaging": len(ImagingReadOperations), "Event": len(EventReadOperations), "DeviceIO": len(DeviceIOReadOperations), } // Initialize coverage r.Coverage = make(map[string]Coverage) for service, total := range totals { r.Coverage[service] = Coverage{ Total: total, Captured: 0, // Would need to analyze captures to determine actual coverage } } } // GetTotalCoverage returns the total coverage across all services. func (r *Registry) GetTotalCoverage() (total int, captured int) { for _, cov := range r.Coverage { total += cov.Total captured += cov.Captured } return total, captured } // GenerateCameraID generates a unique ID for a camera. func GenerateCameraID(manufacturer, model, firmware string) string { // Sanitize and combine id := fmt.Sprintf("%s_%s_%s", manufacturer, model, firmware) id = sanitizeID(id) return id } // sanitizeID removes or replaces invalid characters in an ID. func sanitizeID(s string) string { result := make([]byte, 0, len(s)) for i := 0; i < len(s); i++ { c := s[i] switch { case c >= 'a' && c <= 'z': result = append(result, c) case c >= 'A' && c <= 'Z': result = append(result, c+'a'-'A') // lowercase case c >= '0' && c <= '9': result = append(result, c) case c == ' ' || c == '-' || c == '_' || c == '.': result = append(result, '_') } } return string(result) } // ValidateRegistry checks if all referenced capture files exist. func ValidateRegistry(registry *Registry, basePath string) []string { var errors []string for _, cam := range registry.Cameras { capturePath := filepath.Join(basePath, cam.CaptureFile) if _, err := os.Stat(capturePath); os.IsNotExist(err) { errors = append(errors, fmt.Sprintf("camera %s: capture file not found: %s", cam.ID, cam.CaptureFile)) } if cam.TestFile != "" { testPath := filepath.Join(basePath, cam.TestFile) if _, err := os.Stat(testPath); os.IsNotExist(err) { errors = append(errors, fmt.Sprintf("camera %s: test file not found: %s", cam.ID, cam.TestFile)) } } } return errors } // CreateCameraEntryFromCapture creates a registry entry from a capture archive. func CreateCameraEntryFromCapture(archivePath string) (*CameraEntry, error) { capture, metadata, err := LoadCaptureFromArchiveV2(archivePath) if err != nil { return nil, err } // Extract camera info var cameraInfo CameraInfo if metadata != nil { cameraInfo = metadata.CameraInfo } else { // Try to extract from GetDeviceInformation response for _, ex := range capture.Exchanges { if ex.OperationName == "GetDeviceInformation" { cameraInfo.Manufacturer = ExtractXMLElement(ex.ResponseBody, "Manufacturer") cameraInfo.Model = ExtractXMLElement(ex.ResponseBody, "Model") cameraInfo.FirmwareVersion = ExtractXMLElement(ex.ResponseBody, "FirmwareVersion") break } } } // Determine capabilities from captured operations capabilities := detectCapabilities(capture) entry := &CameraEntry{ ID: GenerateCameraID(cameraInfo.Manufacturer, cameraInfo.Model, cameraInfo.FirmwareVersion), Manufacturer: cameraInfo.Manufacturer, Model: cameraInfo.Model, Firmware: cameraInfo.FirmwareVersion, CaptureFile: filepath.Base(archivePath), OperationsCaptured: len(capture.Exchanges), Capabilities: capabilities, AddedDate: time.Now().Format("2006-01-02"), } if metadata != nil { entry.CaptureVersion = metadata.Version } return entry, nil } // detectCapabilities determines which services are captured. func detectCapabilities(capture *CameraCaptureV2) []string { services := make(map[string]bool) for _, ex := range capture.Exchanges { if ex.ServiceType != "" { services[string(ex.ServiceType)] = true } else { // Infer from operation name svc := inferServiceFromOperation(ex.OperationName) if svc != "" { services[svc] = true } } } var result []string for svc := range services { result = append(result, svc) } return result } // inferServiceFromOperation guesses the service type from an operation name. func inferServiceFromOperation(op string) string { // Media operations typically have these patterns mediaOps := []string{"Profile", "Stream", "Encoder", "VideoSource", "AudioSource", "OSD", "Metadata"} for _, pattern := range mediaOps { if containsSubstring(op, pattern) { return "Media" } } // PTZ operations if containsSubstring(op, "PTZ") || containsSubstring(op, "Preset") || containsSubstring(op, "Move") { return "PTZ" } // Imaging operations if containsSubstring(op, "Imaging") || op == "GetOptions" || op == "GetMoveOptions" { return "Imaging" } // Event operations if containsSubstring(op, "Event") || containsSubstring(op, "Subscription") { return "Event" } // Default to Device return "Device" } // containsSubstring checks if s contains substr (case-sensitive). func containsSubstring(s, substr string) bool { return len(s) >= len(substr) && findSubstring(s, substr) >= 0 } // findSubstring finds substr in s, returns -1 if not found. func findSubstring(s, substr string) int { for i := 0; i <= len(s)-len(substr); i++ { if s[i:i+len(substr)] == substr { return i } } return -1 } // RegistrySummary provides a summary of the registry. type RegistrySummary struct { TotalCameras int TotalOperations int CapturedOperations int ManufacturerCount map[string]int ServiceCoverage map[string]float64 } // GetSummary generates a summary of the registry. func (r *Registry) GetSummary() RegistrySummary { summary := RegistrySummary{ TotalCameras: len(r.Cameras), ManufacturerCount: make(map[string]int), ServiceCoverage: make(map[string]float64), } // Count by manufacturer for _, cam := range r.Cameras { summary.ManufacturerCount[cam.Manufacturer]++ } // Calculate coverage percentages for service, cov := range r.Coverage { summary.TotalOperations += cov.Total summary.CapturedOperations += cov.Captured if cov.Total > 0 { summary.ServiceCoverage[service] = float64(cov.Captured) / float64(cov.Total) * 100 } } return summary }