cleanup
This commit is contained in:
@@ -0,0 +1,366 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user