927 lines
26 KiB
Go
927 lines
26 KiB
Go
package main
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"text/template"
|
|
"time"
|
|
|
|
onviftesting "github.com/0x524a/onvif-go/testing"
|
|
)
|
|
|
|
var (
|
|
captureArchive = flag.String("capture", "", "Path to XML capture archive (.tar.gz)")
|
|
outputDir = flag.String("output", "./", "Output directory for generated test file")
|
|
packageName = flag.String("package", "onvif_test", "Package name for generated test")
|
|
updateRegistry = flag.Bool("update-registry", true, "Update registry.json with camera info")
|
|
registryPath = flag.String("registry", "", "Path to registry.json (default: testdata/captures/registry.json)")
|
|
coverageReport = flag.Bool("coverage-report", false, "Generate coverage report from registry")
|
|
coverageOutput = flag.String("coverage-output", "", "Output path for coverage report (default: stdout)")
|
|
)
|
|
|
|
const testTemplate = `package {{.PackageName}}
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/0x524a/onvif-go"
|
|
onviftesting "github.com/0x524a/onvif-go/testing"
|
|
)
|
|
|
|
// Test{{.CameraName}} tests ONVIF client against {{.CameraDescription}} captured responses.
|
|
// Capture format: V2 with parameter-aware matching
|
|
// Total captured operations: {{.TotalExchanges}}
|
|
func Test{{.CameraName}}(t *testing.T) {
|
|
// Load capture archive (relative to project root)
|
|
captureArchive := "{{.CaptureArchiveRelPath}}"
|
|
|
|
mockServer, err := onviftesting.NewMockSOAPServerV2(captureArchive)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create mock server: %v", err)
|
|
}
|
|
defer mockServer.Close()
|
|
|
|
// Create ONVIF client pointing to mock server
|
|
client, err := onvif.NewClient(
|
|
mockServer.URL()+"/onvif/device_service",
|
|
onvif.WithCredentials("testuser", "testpass"),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create ONVIF client: %v", err)
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
// =========================================================================
|
|
// Device Service Operations
|
|
// =========================================================================
|
|
{{range .DeviceTests}}
|
|
t.Run("{{.Name}}", func(t *testing.T) {
|
|
{{.Code}}
|
|
})
|
|
{{end}}
|
|
// =========================================================================
|
|
// Media Service Operations
|
|
// =========================================================================
|
|
{{if .NeedsInit}}
|
|
// Initialize to discover service endpoints (required for Media/PTZ/Imaging)
|
|
if err := client.Initialize(ctx); err != nil {
|
|
t.Fatalf("Failed to initialize client: %v", err)
|
|
}
|
|
{{end}}
|
|
{{range .MediaTests}}
|
|
t.Run("{{.Name}}", func(t *testing.T) {
|
|
{{.Code}}
|
|
})
|
|
{{end}}
|
|
// =========================================================================
|
|
// Profile-Dependent Operations
|
|
// =========================================================================
|
|
{{range .ProfileTests}}
|
|
t.Run("{{.Name}}", func(t *testing.T) {
|
|
{{.Code}}
|
|
})
|
|
{{end}}
|
|
// =========================================================================
|
|
// PTZ Operations
|
|
// =========================================================================
|
|
{{range .PTZTests}}
|
|
t.Run("{{.Name}}", func(t *testing.T) {
|
|
{{.Code}}
|
|
})
|
|
{{end}}
|
|
// =========================================================================
|
|
// Imaging Operations
|
|
// =========================================================================
|
|
{{range .ImagingTests}}
|
|
t.Run("{{.Name}}", func(t *testing.T) {
|
|
{{.Code}}
|
|
})
|
|
{{end}}
|
|
}
|
|
`
|
|
|
|
type TestData struct {
|
|
PackageName string
|
|
CameraName string
|
|
CameraDescription string
|
|
CaptureArchiveRelPath string
|
|
TotalExchanges int
|
|
NeedsInit bool
|
|
DeviceTests []GeneratedTest
|
|
MediaTests []GeneratedTest
|
|
ProfileTests []GeneratedTest
|
|
PTZTests []GeneratedTest
|
|
ImagingTests []GeneratedTest
|
|
}
|
|
|
|
type GeneratedTest struct {
|
|
Name string
|
|
Code string
|
|
}
|
|
|
|
// operationInfo holds info about captured operations
|
|
type operationInfo struct {
|
|
OperationName string
|
|
ServiceType onviftesting.ServiceType
|
|
Parameters map[string]interface{}
|
|
Success bool
|
|
}
|
|
|
|
func main() {
|
|
flag.Parse()
|
|
|
|
// Set default registry path
|
|
regPath := *registryPath
|
|
if regPath == "" {
|
|
regPath = onviftesting.DefaultRegistryPath
|
|
}
|
|
|
|
// Handle coverage report mode
|
|
if *coverageReport {
|
|
generateCoverageReport(regPath)
|
|
return
|
|
}
|
|
|
|
if *captureArchive == "" {
|
|
fmt.Println("Error: -capture flag is required")
|
|
fmt.Println()
|
|
fmt.Println("Usage:")
|
|
flag.PrintDefaults()
|
|
fmt.Println()
|
|
fmt.Println("Example:")
|
|
fmt.Println(" ./generate-tests -capture camera-logs/Bosch_FLEXIDOME_indoor_5100i_IR_8.71.0066_xmlcapture_*.tar.gz")
|
|
fmt.Println()
|
|
fmt.Println("Coverage report:")
|
|
fmt.Println(" ./generate-tests -coverage-report")
|
|
os.Exit(1)
|
|
}
|
|
|
|
outputFile := generateTests()
|
|
|
|
// Update registry if requested
|
|
if *updateRegistry {
|
|
updateCameraRegistry(regPath, *captureArchive, outputFile)
|
|
}
|
|
}
|
|
|
|
func generateTests() string {
|
|
// Load capture with V2 support
|
|
capture, metadata, err := onviftesting.LoadCaptureFromArchiveV2(*captureArchive)
|
|
if err != nil {
|
|
log.Fatalf("Failed to load capture: %v", err)
|
|
}
|
|
|
|
// Extract camera name from archive filename
|
|
baseName := filepath.Base(*captureArchive)
|
|
parts := strings.Split(baseName, "_xmlcapture_")
|
|
cameraID := parts[0]
|
|
|
|
// Convert to valid Go identifier
|
|
cameraName := strings.ReplaceAll(cameraID, "-", "")
|
|
cameraName = strings.ReplaceAll(cameraName, ".", "")
|
|
cameraName = strings.ReplaceAll(cameraName, " ", "")
|
|
|
|
// Get camera description from metadata or extract from captures
|
|
cameraDesc := cameraID
|
|
if metadata != nil && metadata.CameraInfo.Manufacturer != "" {
|
|
cameraDesc = fmt.Sprintf("%s %s (Firmware: %s)",
|
|
metadata.CameraInfo.Manufacturer,
|
|
metadata.CameraInfo.Model,
|
|
metadata.CameraInfo.FirmwareVersion)
|
|
} else {
|
|
// Try to extract from GetDeviceInformation response
|
|
for _, ex := range capture.Exchanges {
|
|
if ex.OperationName == "GetDeviceInformation" && ex.Success {
|
|
manufacturer := extractXMLValue(ex.ResponseBody, "Manufacturer")
|
|
model := extractXMLValue(ex.ResponseBody, "Model")
|
|
firmware := extractXMLValue(ex.ResponseBody, "FirmwareVersion")
|
|
if manufacturer != "" && model != "" {
|
|
cameraDesc = fmt.Sprintf("%s %s (Firmware: %s)", manufacturer, model, firmware)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// Analyze captured operations
|
|
ops := analyzeOperations(capture)
|
|
|
|
// Generate tests by service type
|
|
testData := TestData{
|
|
PackageName: *packageName,
|
|
CameraName: cameraName,
|
|
CameraDescription: cameraDesc,
|
|
CaptureArchiveRelPath: makeRelativePath(*captureArchive, *outputDir),
|
|
TotalExchanges: len(capture.Exchanges),
|
|
NeedsInit: hasNonDeviceOperations(ops),
|
|
DeviceTests: generateDeviceTests(ops),
|
|
MediaTests: generateMediaTests(ops),
|
|
ProfileTests: generateProfileDependentTests(ops),
|
|
PTZTests: generatePTZTests(ops),
|
|
ImagingTests: generateImagingTests(ops),
|
|
}
|
|
|
|
// Generate test file
|
|
tmpl, err := template.New("test").Parse(testTemplate)
|
|
if err != nil {
|
|
log.Fatalf("Failed to parse template: %v", err)
|
|
}
|
|
|
|
outputFile := filepath.Join(*outputDir, fmt.Sprintf("%s_test.go", strings.ToLower(cameraID)))
|
|
f, err := os.Create(outputFile) //nolint:gosec // Filename is generated from test data, safe
|
|
if err != nil {
|
|
log.Fatalf("Failed to create output file: %v", err)
|
|
}
|
|
defer func() {
|
|
_ = f.Close()
|
|
}()
|
|
|
|
if err := tmpl.Execute(f, testData); err != nil {
|
|
_ = f.Close()
|
|
log.Fatalf("Failed to execute template: %v", err)
|
|
}
|
|
|
|
fmt.Printf("✓ Generated test file: %s\n", outputFile)
|
|
fmt.Printf(" Camera: %s\n", cameraDesc)
|
|
fmt.Printf(" Captured operations: %d\n", len(capture.Exchanges))
|
|
fmt.Printf(" Generated subtests: Device=%d, Media=%d, Profile=%d, PTZ=%d, Imaging=%d\n",
|
|
len(testData.DeviceTests), len(testData.MediaTests), len(testData.ProfileTests),
|
|
len(testData.PTZTests), len(testData.ImagingTests))
|
|
fmt.Println()
|
|
fmt.Println("Run tests with:")
|
|
fmt.Printf(" go test -v %s\n", outputFile)
|
|
|
|
return outputFile
|
|
}
|
|
|
|
func analyzeOperations(capture *onviftesting.CameraCaptureV2) []operationInfo {
|
|
var ops []operationInfo
|
|
seen := make(map[string]bool)
|
|
|
|
for _, ex := range capture.Exchanges {
|
|
// Create unique key for deduplication
|
|
key := ex.OperationName
|
|
if token := ex.GetProfileToken(); token != "" {
|
|
key += "_" + token
|
|
} else if token := ex.GetConfigurationToken(); token != "" {
|
|
key += "_" + token
|
|
} else if token := ex.GetVideoSourceToken(); token != "" {
|
|
key += "_" + token
|
|
}
|
|
|
|
if seen[key] {
|
|
continue
|
|
}
|
|
seen[key] = true
|
|
|
|
ops = append(ops, operationInfo{
|
|
OperationName: ex.OperationName,
|
|
ServiceType: ex.ServiceType,
|
|
Parameters: ex.Parameters,
|
|
Success: ex.Success,
|
|
})
|
|
}
|
|
|
|
return ops
|
|
}
|
|
|
|
func hasNonDeviceOperations(ops []operationInfo) bool {
|
|
for _, op := range ops {
|
|
switch op.ServiceType {
|
|
case onviftesting.ServiceMedia, onviftesting.ServicePTZ, onviftesting.ServiceImaging:
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func generateDeviceTests(ops []operationInfo) []GeneratedTest {
|
|
var tests []GeneratedTest
|
|
|
|
// Standard device tests
|
|
deviceOps := map[string]string{
|
|
"GetDeviceInformation": `info, err := client.GetDeviceInformation(ctx)
|
|
if err != nil {
|
|
t.Errorf("GetDeviceInformation failed: %v", err)
|
|
return
|
|
}
|
|
if info.Manufacturer == "" {
|
|
t.Error("Manufacturer is empty")
|
|
}
|
|
if info.Model == "" {
|
|
t.Error("Model is empty")
|
|
}
|
|
t.Logf("Device: %s %s (Firmware: %s)", info.Manufacturer, info.Model, info.FirmwareVersion)`,
|
|
|
|
"GetSystemDateAndTime": `_, err := client.GetSystemDateAndTime(ctx)
|
|
if err != nil {
|
|
t.Errorf("GetSystemDateAndTime failed: %v", err)
|
|
}`,
|
|
|
|
"GetCapabilities": `caps, err := client.GetCapabilities(ctx)
|
|
if err != nil {
|
|
t.Errorf("GetCapabilities failed: %v", err)
|
|
return
|
|
}
|
|
t.Logf("Capabilities: Device=%v, Media=%v, Imaging=%v, PTZ=%v",
|
|
caps.Device != nil, caps.Media != nil, caps.Imaging != nil, caps.PTZ != nil)`,
|
|
|
|
"GetHostname": `hostname, err := client.GetHostname(ctx)
|
|
if err != nil {
|
|
t.Errorf("GetHostname failed: %v", err)
|
|
return
|
|
}
|
|
t.Logf("Hostname: %s", hostname)`,
|
|
|
|
"GetScopes": `scopes, err := client.GetScopes(ctx)
|
|
if err != nil {
|
|
t.Errorf("GetScopes failed: %v", err)
|
|
return
|
|
}
|
|
t.Logf("Scopes: %d", len(scopes))`,
|
|
|
|
"GetNetworkInterfaces": `interfaces, err := client.GetNetworkInterfaces(ctx)
|
|
if err != nil {
|
|
t.Errorf("GetNetworkInterfaces failed: %v", err)
|
|
return
|
|
}
|
|
t.Logf("Network interfaces: %d", len(interfaces))`,
|
|
|
|
"GetServices": `services, err := client.GetServices(ctx, true)
|
|
if err != nil {
|
|
t.Errorf("GetServices failed: %v", err)
|
|
return
|
|
}
|
|
t.Logf("Services: %d", len(services))`,
|
|
}
|
|
|
|
// Generate tests for captured operations
|
|
for _, op := range ops {
|
|
if op.ServiceType != onviftesting.ServiceDevice && op.ServiceType != onviftesting.ServiceUnknown {
|
|
continue
|
|
}
|
|
if code, ok := deviceOps[op.OperationName]; ok {
|
|
tests = append(tests, GeneratedTest{
|
|
Name: op.OperationName,
|
|
Code: code,
|
|
})
|
|
delete(deviceOps, op.OperationName) // Don't duplicate
|
|
}
|
|
}
|
|
|
|
// Sort by name for consistent output
|
|
sort.Slice(tests, func(i, j int) bool {
|
|
return tests[i].Name < tests[j].Name
|
|
})
|
|
|
|
return tests
|
|
}
|
|
|
|
func generateMediaTests(ops []operationInfo) []GeneratedTest {
|
|
var tests []GeneratedTest
|
|
|
|
mediaOps := map[string]string{
|
|
"GetProfiles": `profiles, err := client.GetProfiles(ctx)
|
|
if err != nil {
|
|
t.Errorf("GetProfiles failed: %v", err)
|
|
return
|
|
}
|
|
if len(profiles) == 0 {
|
|
t.Error("No profiles returned")
|
|
}
|
|
t.Logf("Found %d profile(s)", len(profiles))`,
|
|
|
|
"GetVideoSources": `sources, err := client.GetVideoSources(ctx)
|
|
if err != nil {
|
|
t.Errorf("GetVideoSources failed: %v", err)
|
|
return
|
|
}
|
|
t.Logf("Video sources: %d", len(sources))`,
|
|
|
|
"GetVideoSourceConfigurations": `configs, err := client.GetVideoSourceConfigurations(ctx)
|
|
if err != nil {
|
|
t.Errorf("GetVideoSourceConfigurations failed: %v", err)
|
|
return
|
|
}
|
|
t.Logf("Video source configs: %d", len(configs))`,
|
|
|
|
"GetVideoEncoderConfigurations": `configs, err := client.GetVideoEncoderConfigurations(ctx)
|
|
if err != nil {
|
|
t.Errorf("GetVideoEncoderConfigurations failed: %v", err)
|
|
return
|
|
}
|
|
t.Logf("Video encoder configs: %d", len(configs))`,
|
|
|
|
"GetAudioSources": `sources, err := client.GetAudioSources(ctx)
|
|
if err != nil {
|
|
t.Errorf("GetAudioSources failed: %v", err)
|
|
return
|
|
}
|
|
t.Logf("Audio sources: %d", len(sources))`,
|
|
|
|
"GetAudioSourceConfigurations": `configs, err := client.GetAudioSourceConfigurations(ctx)
|
|
if err != nil {
|
|
t.Errorf("GetAudioSourceConfigurations failed: %v", err)
|
|
return
|
|
}
|
|
t.Logf("Audio source configs: %d", len(configs))`,
|
|
|
|
"GetMetadataConfigurations": `configs, err := client.GetMetadataConfigurations(ctx)
|
|
if err != nil {
|
|
t.Errorf("GetMetadataConfigurations failed: %v", err)
|
|
return
|
|
}
|
|
t.Logf("Metadata configs: %d", len(configs))`,
|
|
}
|
|
|
|
for _, op := range ops {
|
|
if op.ServiceType != onviftesting.ServiceMedia {
|
|
continue
|
|
}
|
|
if code, ok := mediaOps[op.OperationName]; ok {
|
|
tests = append(tests, GeneratedTest{
|
|
Name: op.OperationName,
|
|
Code: code,
|
|
})
|
|
delete(mediaOps, op.OperationName)
|
|
}
|
|
}
|
|
|
|
sort.Slice(tests, func(i, j int) bool {
|
|
return tests[i].Name < tests[j].Name
|
|
})
|
|
|
|
return tests
|
|
}
|
|
|
|
func generateProfileDependentTests(ops []operationInfo) []GeneratedTest {
|
|
var tests []GeneratedTest
|
|
|
|
// Group operations by profile token
|
|
profileOps := make(map[string][]operationInfo)
|
|
for _, op := range ops {
|
|
if token, ok := op.Parameters["ProfileToken"].(string); ok && token != "" {
|
|
profileOps[token] = append(profileOps[token], op)
|
|
}
|
|
}
|
|
|
|
// Generate GetStreamURI tests for each profile
|
|
for token, opList := range profileOps {
|
|
for _, op := range opList {
|
|
switch op.OperationName {
|
|
case "GetStreamURI":
|
|
testName := fmt.Sprintf("GetStreamURI_%s", sanitizeToken(token))
|
|
tests = append(tests, GeneratedTest{
|
|
Name: testName,
|
|
Code: fmt.Sprintf(`uri, err := client.GetStreamURI(ctx, "%s")
|
|
if err != nil {
|
|
t.Errorf("GetStreamURI failed: %%v", err)
|
|
return
|
|
}
|
|
if uri.URI == "" {
|
|
t.Error("Stream URI is empty")
|
|
}
|
|
t.Logf("Stream URI: %%s", uri.URI)`, token),
|
|
})
|
|
|
|
case "GetSnapshotURI":
|
|
testName := fmt.Sprintf("GetSnapshotURI_%s", sanitizeToken(token))
|
|
tests = append(tests, GeneratedTest{
|
|
Name: testName,
|
|
Code: fmt.Sprintf(`uri, err := client.GetSnapshotURI(ctx, "%s")
|
|
if err != nil {
|
|
t.Errorf("GetSnapshotURI failed: %%v", err)
|
|
return
|
|
}
|
|
if uri.URI == "" {
|
|
t.Error("Snapshot URI is empty")
|
|
}
|
|
t.Logf("Snapshot URI: %%s", uri.URI)`, token),
|
|
})
|
|
|
|
case "GetProfile":
|
|
testName := fmt.Sprintf("GetProfile_%s", sanitizeToken(token))
|
|
tests = append(tests, GeneratedTest{
|
|
Name: testName,
|
|
Code: fmt.Sprintf(`profile, err := client.GetProfile(ctx, "%s")
|
|
if err != nil {
|
|
t.Errorf("GetProfile failed: %%v", err)
|
|
return
|
|
}
|
|
if profile.Token != "%s" {
|
|
t.Errorf("Expected token %%s, got %%s", "%s", profile.Token)
|
|
}
|
|
t.Logf("Profile: %%s", profile.Name)`, token, token, token),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// Deduplicate tests
|
|
seen := make(map[string]bool)
|
|
var uniqueTests []GeneratedTest
|
|
for _, t := range tests {
|
|
if !seen[t.Name] {
|
|
seen[t.Name] = true
|
|
uniqueTests = append(uniqueTests, t)
|
|
}
|
|
}
|
|
|
|
sort.Slice(uniqueTests, func(i, j int) bool {
|
|
return uniqueTests[i].Name < uniqueTests[j].Name
|
|
})
|
|
|
|
return uniqueTests
|
|
}
|
|
|
|
func generatePTZTests(ops []operationInfo) []GeneratedTest {
|
|
var tests []GeneratedTest
|
|
|
|
ptzOps := map[string]string{
|
|
"GetNodes": `nodes, err := client.GetNodes(ctx)
|
|
if err != nil {
|
|
t.Errorf("GetNodes failed: %v", err)
|
|
return
|
|
}
|
|
t.Logf("PTZ nodes: %d", len(nodes))`,
|
|
|
|
"GetConfigurations": `configs, err := client.GetConfigurations(ctx)
|
|
if err != nil {
|
|
t.Errorf("GetConfigurations failed: %v", err)
|
|
return
|
|
}
|
|
t.Logf("PTZ configs: %d", len(configs))`,
|
|
}
|
|
|
|
// Group by profile token for status and presets
|
|
profileOps := make(map[string][]operationInfo)
|
|
for _, op := range ops {
|
|
if op.ServiceType != onviftesting.ServicePTZ {
|
|
continue
|
|
}
|
|
if code, ok := ptzOps[op.OperationName]; ok {
|
|
tests = append(tests, GeneratedTest{
|
|
Name: op.OperationName,
|
|
Code: code,
|
|
})
|
|
delete(ptzOps, op.OperationName)
|
|
continue
|
|
}
|
|
if token, ok := op.Parameters["ProfileToken"].(string); ok && token != "" {
|
|
profileOps[token] = append(profileOps[token], op)
|
|
}
|
|
}
|
|
|
|
// Generate profile-specific PTZ tests
|
|
for token, opList := range profileOps {
|
|
for _, op := range opList {
|
|
switch op.OperationName {
|
|
case "GetStatus":
|
|
testName := fmt.Sprintf("PTZ_GetStatus_%s", sanitizeToken(token))
|
|
tests = append(tests, GeneratedTest{
|
|
Name: testName,
|
|
Code: fmt.Sprintf(`status, err := client.GetStatus(ctx, "%s")
|
|
if err != nil {
|
|
t.Errorf("GetStatus failed: %%v", err)
|
|
return
|
|
}
|
|
t.Logf("PTZ Status retrieved for profile %s")
|
|
_ = status`, token, token),
|
|
})
|
|
|
|
case "GetPresets":
|
|
testName := fmt.Sprintf("PTZ_GetPresets_%s", sanitizeToken(token))
|
|
tests = append(tests, GeneratedTest{
|
|
Name: testName,
|
|
Code: fmt.Sprintf(`presets, err := client.GetPresets(ctx, "%s")
|
|
if err != nil {
|
|
t.Errorf("GetPresets failed: %%v", err)
|
|
return
|
|
}
|
|
t.Logf("Found %%d preset(s) for profile %s", len(presets))`, token, token),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// Deduplicate
|
|
seen := make(map[string]bool)
|
|
var uniqueTests []GeneratedTest
|
|
for _, t := range tests {
|
|
if !seen[t.Name] {
|
|
seen[t.Name] = true
|
|
uniqueTests = append(uniqueTests, t)
|
|
}
|
|
}
|
|
|
|
sort.Slice(uniqueTests, func(i, j int) bool {
|
|
return uniqueTests[i].Name < uniqueTests[j].Name
|
|
})
|
|
|
|
return uniqueTests
|
|
}
|
|
|
|
func generateImagingTests(ops []operationInfo) []GeneratedTest {
|
|
var tests []GeneratedTest
|
|
|
|
// Group by video source token
|
|
sourceOps := make(map[string][]operationInfo)
|
|
for _, op := range ops {
|
|
if op.ServiceType != onviftesting.ServiceImaging {
|
|
continue
|
|
}
|
|
if token, ok := op.Parameters["VideoSourceToken"].(string); ok && token != "" {
|
|
sourceOps[token] = append(sourceOps[token], op)
|
|
}
|
|
}
|
|
|
|
for token, opList := range sourceOps {
|
|
for _, op := range opList {
|
|
switch op.OperationName {
|
|
case "GetImagingSettings":
|
|
testName := fmt.Sprintf("GetImagingSettings_%s", sanitizeToken(token))
|
|
tests = append(tests, GeneratedTest{
|
|
Name: testName,
|
|
Code: fmt.Sprintf(`settings, err := client.GetImagingSettings(ctx, "%s")
|
|
if err != nil {
|
|
t.Errorf("GetImagingSettings failed: %%v", err)
|
|
return
|
|
}
|
|
t.Logf("Imaging settings retrieved for source %s")
|
|
_ = settings`, token, token),
|
|
})
|
|
|
|
case "GetOptions":
|
|
testName := fmt.Sprintf("GetImagingOptions_%s", sanitizeToken(token))
|
|
tests = append(tests, GeneratedTest{
|
|
Name: testName,
|
|
Code: fmt.Sprintf(`options, err := client.GetOptions(ctx, "%s")
|
|
if err != nil {
|
|
t.Errorf("GetOptions failed: %%v", err)
|
|
return
|
|
}
|
|
t.Logf("Imaging options retrieved for source %s")
|
|
_ = options`, token, token),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// Deduplicate
|
|
seen := make(map[string]bool)
|
|
var uniqueTests []GeneratedTest
|
|
for _, t := range tests {
|
|
if !seen[t.Name] {
|
|
seen[t.Name] = true
|
|
uniqueTests = append(uniqueTests, t)
|
|
}
|
|
}
|
|
|
|
sort.Slice(uniqueTests, func(i, j int) bool {
|
|
return uniqueTests[i].Name < uniqueTests[j].Name
|
|
})
|
|
|
|
return uniqueTests
|
|
}
|
|
|
|
func sanitizeToken(token string) string {
|
|
// Make token safe for test name
|
|
token = strings.ReplaceAll(token, "-", "_")
|
|
token = strings.ReplaceAll(token, ".", "_")
|
|
token = strings.ReplaceAll(token, " ", "_")
|
|
// Truncate if too long
|
|
if len(token) > 20 {
|
|
token = token[:20]
|
|
}
|
|
return token
|
|
}
|
|
|
|
func makeRelativePath(archivePath, outputDir string) string {
|
|
if absOutput, err := filepath.Abs(outputDir); err == nil {
|
|
if absArchive, err := filepath.Abs(archivePath); err == nil {
|
|
if rel, err := filepath.Rel(filepath.Dir(absOutput), absArchive); err == nil {
|
|
return rel
|
|
}
|
|
}
|
|
}
|
|
return archivePath
|
|
}
|
|
|
|
func extractXMLValue(xmlStr, tagName string) string {
|
|
start := fmt.Sprintf("<%s>", tagName)
|
|
end := fmt.Sprintf("</%s>", tagName)
|
|
|
|
startIdx := strings.Index(xmlStr, start)
|
|
if startIdx == -1 {
|
|
start = fmt.Sprintf(":%s>", tagName)
|
|
startIdx = strings.Index(xmlStr, start)
|
|
if startIdx == -1 {
|
|
return ""
|
|
}
|
|
startIdx += len(start)
|
|
} else {
|
|
startIdx += len(start)
|
|
}
|
|
|
|
endIdx := strings.Index(xmlStr[startIdx:], end)
|
|
if endIdx == -1 {
|
|
end = fmt.Sprintf(":/%s>", tagName)
|
|
endIdx = strings.Index(xmlStr[startIdx:], end)
|
|
if endIdx == -1 {
|
|
return ""
|
|
}
|
|
}
|
|
|
|
return strings.TrimSpace(xmlStr[startIdx : startIdx+endIdx])
|
|
}
|
|
|
|
// updateCameraRegistry updates the registry with camera information from the capture.
|
|
func updateCameraRegistry(regPath, archivePath, testFile string) {
|
|
registry, err := onviftesting.LoadRegistry(regPath)
|
|
if err != nil {
|
|
log.Printf("Warning: Failed to load registry: %v", err)
|
|
return
|
|
}
|
|
|
|
entry, err := onviftesting.CreateCameraEntryFromCapture(archivePath)
|
|
if err != nil {
|
|
log.Printf("Warning: Failed to create registry entry: %v", err)
|
|
return
|
|
}
|
|
|
|
// Set the test file path (relative to registry directory)
|
|
if testFile != "" {
|
|
regDir := filepath.Dir(regPath)
|
|
if absTest, err := filepath.Abs(testFile); err == nil {
|
|
if absRegDir, err := filepath.Abs(regDir); err == nil {
|
|
if rel, err := filepath.Rel(absRegDir, absTest); err == nil {
|
|
entry.TestFile = rel
|
|
}
|
|
}
|
|
}
|
|
if entry.TestFile == "" {
|
|
entry.TestFile = filepath.Base(testFile)
|
|
}
|
|
}
|
|
|
|
// Add or update the camera entry
|
|
registry.AddCamera(*entry)
|
|
|
|
// Update coverage statistics
|
|
updateRegistryCoverage(registry, archivePath)
|
|
|
|
// Save registry
|
|
if err := onviftesting.SaveRegistry(registry, regPath); err != nil {
|
|
log.Printf("Warning: Failed to save registry: %v", err)
|
|
return
|
|
}
|
|
|
|
fmt.Printf("✓ Registry updated: %s\n", regPath)
|
|
fmt.Printf(" Camera ID: %s\n", entry.ID)
|
|
fmt.Printf(" Total cameras in registry: %d\n", len(registry.Cameras))
|
|
}
|
|
|
|
// updateRegistryCoverage calculates coverage from captured operations.
|
|
func updateRegistryCoverage(registry *onviftesting.Registry, archivePath string) {
|
|
capture, _, err := onviftesting.LoadCaptureFromArchiveV2(archivePath)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// Count unique operations per service
|
|
serviceCounts := make(map[string]map[string]bool)
|
|
for _, ex := range capture.Exchanges {
|
|
service := string(ex.ServiceType)
|
|
if service == "" || service == "Unknown" {
|
|
continue
|
|
}
|
|
if serviceCounts[service] == nil {
|
|
serviceCounts[service] = make(map[string]bool)
|
|
}
|
|
serviceCounts[service][ex.OperationName] = true
|
|
}
|
|
|
|
// Get totals from operations registry
|
|
opCounts := onviftesting.GetOperationCount()
|
|
|
|
// Update coverage
|
|
registry.Coverage = make(map[string]onviftesting.Coverage)
|
|
for service, ops := range serviceCounts {
|
|
total := 0
|
|
switch service {
|
|
case "Device":
|
|
total = opCounts.Device
|
|
case "Media":
|
|
total = opCounts.Media
|
|
case "PTZ":
|
|
total = opCounts.PTZ
|
|
case "Imaging":
|
|
total = opCounts.Imaging
|
|
case "Event":
|
|
total = opCounts.Event
|
|
case "DeviceIO":
|
|
total = opCounts.DeviceIO
|
|
}
|
|
|
|
registry.Coverage[service] = onviftesting.Coverage{
|
|
Total: total,
|
|
Captured: len(ops),
|
|
}
|
|
}
|
|
}
|
|
|
|
// generateCoverageReport generates a coverage report from the registry.
|
|
func generateCoverageReport(regPath string) {
|
|
registry, err := onviftesting.LoadRegistry(regPath)
|
|
if err != nil {
|
|
log.Fatalf("Failed to load registry: %v", err)
|
|
}
|
|
|
|
// Generate markdown report
|
|
report := generateCoverageMarkdown(registry)
|
|
|
|
// Output to file or stdout
|
|
if *coverageOutput != "" {
|
|
if err := os.WriteFile(*coverageOutput, []byte(report), 0600); err != nil { //nolint:mnd
|
|
log.Fatalf("Failed to write coverage report: %v", err)
|
|
}
|
|
fmt.Printf("✓ Coverage report written to: %s\n", *coverageOutput)
|
|
} else {
|
|
fmt.Println(report)
|
|
}
|
|
}
|
|
|
|
// generateCoverageMarkdown creates a markdown coverage report.
|
|
func generateCoverageMarkdown(registry *onviftesting.Registry) string {
|
|
var sb strings.Builder
|
|
|
|
sb.WriteString("# ONVIF Operation Coverage Report\n\n")
|
|
sb.WriteString(fmt.Sprintf("Generated: %s\n\n", time.Now().Format("2006-01-02 15:04:05")))
|
|
|
|
// Summary
|
|
sb.WriteString("## Summary\n\n")
|
|
sb.WriteString(fmt.Sprintf("- **Total Cameras**: %d\n", len(registry.Cameras)))
|
|
|
|
total, captured := registry.GetTotalCoverage()
|
|
if total > 0 {
|
|
sb.WriteString(fmt.Sprintf("- **Overall Coverage**: %.1f%% (%d/%d operations)\n\n",
|
|
float64(captured)/float64(total)*100, captured, total))
|
|
}
|
|
|
|
// Cameras
|
|
if len(registry.Cameras) > 0 {
|
|
sb.WriteString("## Registered Cameras\n\n")
|
|
sb.WriteString("| Manufacturer | Model | Firmware | Operations | Capabilities |\n")
|
|
sb.WriteString("|--------------|-------|----------|------------|---------------|\n")
|
|
|
|
for _, cam := range registry.Cameras {
|
|
caps := strings.Join(cam.Capabilities, ", ")
|
|
sb.WriteString(fmt.Sprintf("| %s | %s | %s | %d | %s |\n",
|
|
cam.Manufacturer, cam.Model, cam.Firmware, cam.OperationsCaptured, caps))
|
|
}
|
|
sb.WriteString("\n")
|
|
}
|
|
|
|
// Coverage by service
|
|
if len(registry.Coverage) > 0 {
|
|
sb.WriteString("## Coverage by Service\n\n")
|
|
sb.WriteString("| Service | Total | Captured | Coverage |\n")
|
|
sb.WriteString("|---------|-------|----------|----------|\n")
|
|
|
|
services := []string{"Device", "Media", "PTZ", "Imaging", "Event", "DeviceIO"}
|
|
for _, service := range services {
|
|
if cov, ok := registry.Coverage[service]; ok {
|
|
pct := 0.0
|
|
if cov.Total > 0 {
|
|
pct = float64(cov.Captured) / float64(cov.Total) * 100
|
|
}
|
|
sb.WriteString(fmt.Sprintf("| %s | %d | %d | %.1f%% |\n",
|
|
service, cov.Total, cov.Captured, pct))
|
|
}
|
|
}
|
|
sb.WriteString("\n")
|
|
}
|
|
|
|
// Missing operations
|
|
sb.WriteString("## Operation Specifications\n\n")
|
|
opCounts := onviftesting.GetOperationCount()
|
|
sb.WriteString(fmt.Sprintf("- Device: %d operations defined\n", opCounts.Device))
|
|
sb.WriteString(fmt.Sprintf("- Media: %d operations defined\n", opCounts.Media))
|
|
sb.WriteString(fmt.Sprintf("- PTZ: %d operations defined\n", opCounts.PTZ))
|
|
sb.WriteString(fmt.Sprintf("- Imaging: %d operations defined\n", opCounts.Imaging))
|
|
sb.WriteString(fmt.Sprintf("- Event: %d operations defined\n", opCounts.Event))
|
|
sb.WriteString(fmt.Sprintf("- DeviceIO: %d operations defined\n", opCounts.DeviceIO))
|
|
sb.WriteString(fmt.Sprintf("\n**Total**: %d read-only operations tracked\n", opCounts.Total))
|
|
|
|
return sb.String()
|
|
}
|