Files
onvif-go/.claude/cmd/generate-tests/main.go
T
2026-01-16 04:11:59 +00:00

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()
}