377 lines
10 KiB
Go
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, ®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 := 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 := ®istry.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
|
|
}
|