Files
onvif-go/testing/registry.go
T

377 lines
10 KiB
Go

// Package onviftesting provides testing utilities for ONVIF client testing.
package onviftesting
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"time"
)
const percentScale = 100
// 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) //nolint:gosec // Registry path is from constant or test data, safe
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, &registry); err != nil {
return nil, fmt.Errorf("failed to unmarshal registry: %w", err)
}
return &registry, 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 := range r.Cameras {
cam := &r.Cameras[i]
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 := range r.Cameras {
cam := &r.Cameras[i]
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 i := range r.Cameras {
if r.Cameras[i].Manufacturer == manufacturer {
cameras = append(cameras, &r.Cameras[i])
}
}
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, 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 i := range registry.Cameras {
cam := &registry.Cameras[i]
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 i := range capture.Exchanges {
ex := &capture.Exchanges[i]
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 i := range capture.Exchanges {
ex := &capture.Exchanges[i]
if ex.ServiceType != "" {
services[string(ex.ServiceType)] = true
} else {
// Infer from operation name
svc := inferServiceFromOperation(ex.OperationName)
if svc != "" {
services[svc] = true
}
}
}
result := make([]string, 0, len(services))
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 i := range r.Cameras {
summary.ManufacturerCount[r.Cameras[i].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) * percentScale
}
}
return summary
}