cleanup
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
// Command discover performs ONVIF camera discovery on the local network.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/0x524a/onvif-go/discovery"
|
||||
)
|
||||
|
||||
func main() {
|
||||
iface := flag.String("interface", "", "Network interface to use (e.g., en0, en11)")
|
||||
timeout := flag.Duration("timeout", 10*time.Second, "Discovery timeout")
|
||||
flag.Parse()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
|
||||
defer cancel()
|
||||
|
||||
opts := &discovery.DiscoverOptions{
|
||||
NetworkInterface: *iface,
|
||||
}
|
||||
|
||||
fmt.Printf("Discovering ONVIF cameras on the network")
|
||||
if *iface != "" {
|
||||
fmt.Printf(" (interface: %s)", *iface)
|
||||
}
|
||||
fmt.Println("...")
|
||||
|
||||
devices, err := discovery.DiscoverWithOptions(ctx, *timeout, opts)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Discovery error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if len(devices) == 0 {
|
||||
fmt.Println("No cameras found.")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
fmt.Printf("\nFound %d camera(s):\n\n", len(devices))
|
||||
for i, d := range devices {
|
||||
fmt.Printf("Camera %d:\n", i+1)
|
||||
fmt.Printf(" Endpoint: %s\n", d.EndpointRef)
|
||||
for _, addr := range d.XAddrs {
|
||||
fmt.Printf(" XAddr: %s\n", addr)
|
||||
}
|
||||
if len(d.Scopes) > 0 {
|
||||
fmt.Printf(" Scopes:\n")
|
||||
for _, s := range d.Scopes {
|
||||
fmt.Printf(" - %s\n", s)
|
||||
}
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
+758
-101
@@ -6,8 +6,10 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
onviftesting "github.com/0x524a/onvif-go/testing"
|
||||
)
|
||||
@@ -16,6 +18,10 @@ 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}}
|
||||
@@ -29,12 +35,14 @@ import (
|
||||
onviftesting "github.com/0x524a/onvif-go/testing"
|
||||
)
|
||||
|
||||
// Test{{.CameraName}} tests ONVIF client against {{.CameraDescription}} captured responses
|
||||
// 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.NewMockSOAPServer(captureArchive)
|
||||
|
||||
mockServer, err := onviftesting.NewMockSOAPServerV2(captureArchive)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create mock server: %v", err)
|
||||
}
|
||||
@@ -52,69 +60,48 @@ func Test{{.CameraName}}(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
t.Run("GetDeviceInformation", func(t *testing.T) {
|
||||
info, err := client.GetDeviceInformation(ctx)
|
||||
if err != nil {
|
||||
t.Errorf("GetDeviceInformation failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate expected values
|
||||
if info.Manufacturer == "" {
|
||||
t.Error("Manufacturer is empty")
|
||||
}
|
||||
if info.Model == "" {
|
||||
t.Error("Model is empty")
|
||||
}
|
||||
if info.FirmwareVersion == "" {
|
||||
t.Error("FirmwareVersion is empty")
|
||||
}
|
||||
|
||||
t.Logf("Device: %s %s (Firmware: %s)", info.Manufacturer, info.Model, info.FirmwareVersion)
|
||||
// =========================================================================
|
||||
// Device Service Operations
|
||||
// =========================================================================
|
||||
{{range .DeviceTests}}
|
||||
t.Run("{{.Name}}", func(t *testing.T) {
|
||||
{{.Code}}
|
||||
})
|
||||
|
||||
t.Run("GetSystemDateAndTime", func(t *testing.T) {
|
||||
_, err := client.GetSystemDateAndTime(ctx)
|
||||
if err != nil {
|
||||
t.Errorf("GetSystemDateAndTime failed: %v", err)
|
||||
}
|
||||
{{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}}
|
||||
})
|
||||
|
||||
t.Run("GetCapabilities", func(t *testing.T) {
|
||||
caps, err := client.GetCapabilities(ctx)
|
||||
if err != nil {
|
||||
t.Errorf("GetCapabilities failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if caps.Device == nil {
|
||||
t.Error("Device capabilities is nil")
|
||||
}
|
||||
if caps.Media == nil {
|
||||
t.Error("Media capabilities is nil")
|
||||
}
|
||||
|
||||
t.Logf("Capabilities: Device=%v, Media=%v, Imaging=%v, PTZ=%v",
|
||||
caps.Device != nil, caps.Media != nil, caps.Imaging != nil, caps.PTZ != nil)
|
||||
{{end}}
|
||||
// =========================================================================
|
||||
// Profile-Dependent Operations
|
||||
// =========================================================================
|
||||
{{range .ProfileTests}}
|
||||
t.Run("{{.Name}}", func(t *testing.T) {
|
||||
{{.Code}}
|
||||
})
|
||||
|
||||
t.Run("GetProfiles", func(t *testing.T) {
|
||||
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))
|
||||
for i, profile := range profiles {
|
||||
t.Logf(" Profile %d: %s (Token: %s)", i+1, profile.Name, profile.Token)
|
||||
}
|
||||
{{end}}
|
||||
// =========================================================================
|
||||
// PTZ Operations
|
||||
// =========================================================================
|
||||
{{range .PTZTests}}
|
||||
t.Run("{{.Name}}", func(t *testing.T) {
|
||||
{{.Code}}
|
||||
})
|
||||
{{range .AdditionalTests}}
|
||||
{{end}}
|
||||
// =========================================================================
|
||||
// Imaging Operations
|
||||
// =========================================================================
|
||||
{{range .ImagingTests}}
|
||||
t.Run("{{.Name}}", func(t *testing.T) {
|
||||
{{.Code}}
|
||||
})
|
||||
@@ -127,17 +114,43 @@ type TestData struct {
|
||||
CameraName string
|
||||
CameraDescription string
|
||||
CaptureArchiveRelPath string
|
||||
AdditionalTests []AdditionalTest
|
||||
TotalExchanges int
|
||||
NeedsInit bool
|
||||
DeviceTests []GeneratedTest
|
||||
MediaTests []GeneratedTest
|
||||
ProfileTests []GeneratedTest
|
||||
PTZTests []GeneratedTest
|
||||
ImagingTests []GeneratedTest
|
||||
}
|
||||
|
||||
type AdditionalTest struct {
|
||||
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()
|
||||
@@ -146,18 +159,29 @@ func main() {
|
||||
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)
|
||||
}
|
||||
|
||||
// Load capture to get camera info
|
||||
capture, err := onviftesting.LoadCaptureFromArchive(*captureArchive)
|
||||
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)
|
||||
// Remove _xmlcapture_timestamp.tar.gz suffix
|
||||
parts := strings.Split(baseName, "_xmlcapture_")
|
||||
cameraID := parts[0]
|
||||
|
||||
@@ -166,45 +190,44 @@ func main() {
|
||||
cameraName = strings.ReplaceAll(cameraName, ".", "")
|
||||
cameraName = strings.ReplaceAll(cameraName, " ", "")
|
||||
|
||||
// Get device info from first exchange (GetDeviceInformation)
|
||||
// Get camera description from metadata or extract from captures
|
||||
cameraDesc := cameraID
|
||||
if len(capture.Exchanges) > 0 {
|
||||
// Try to parse device info from response
|
||||
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 !strings.Contains(ex.RequestBody, "GetDeviceInformation") {
|
||||
continue
|
||||
}
|
||||
// Extract manufacturer and model from response
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare test data
|
||||
// Make archive path relative if inside output directory
|
||||
relArchivePath := *captureArchive
|
||||
|
||||
// If archive is in a sibling directory to output, make it relative
|
||||
if absOutput, err := filepath.Abs(*outputDir); err == nil {
|
||||
if absArchive, err := filepath.Abs(*captureArchive); err == nil {
|
||||
if rel, err := filepath.Rel(filepath.Dir(absOutput), absArchive); err == nil {
|
||||
relArchivePath = rel
|
||||
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: relArchivePath,
|
||||
AdditionalTests: []AdditionalTest{},
|
||||
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
|
||||
@@ -213,7 +236,6 @@ func main() {
|
||||
log.Fatalf("Failed to parse template: %v", err)
|
||||
}
|
||||
|
||||
// Create output file
|
||||
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 {
|
||||
@@ -225,26 +247,481 @@ func main() {
|
||||
|
||||
if err := tmpl.Execute(f, testData); err != nil {
|
||||
_ = f.Close()
|
||||
//nolint:gocritic // Fatalf exits, defer won't run - this is acceptable
|
||||
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 {
|
||||
// Simple extraction for basic tags
|
||||
start := fmt.Sprintf("<%s>", tagName)
|
||||
end := fmt.Sprintf("</%s>", tagName)
|
||||
|
||||
startIdx := strings.Index(xmlStr, start)
|
||||
if startIdx == -1 {
|
||||
// Try with namespace prefix
|
||||
start = fmt.Sprintf(":%s>", tagName)
|
||||
startIdx = strings.Index(xmlStr, start)
|
||||
if startIdx == -1 {
|
||||
@@ -257,7 +734,6 @@ func extractXMLValue(xmlStr, tagName string) string {
|
||||
|
||||
endIdx := strings.Index(xmlStr[startIdx:], end)
|
||||
if endIdx == -1 {
|
||||
// Try with namespace prefix
|
||||
end = fmt.Sprintf(":/%s>", tagName)
|
||||
endIdx = strings.Index(xmlStr[startIdx:], end)
|
||||
if endIdx == -1 {
|
||||
@@ -267,3 +743,184 @@ func extractXMLValue(xmlStr, tagName string) string {
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
+665
-95
@@ -14,10 +14,13 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/0x524a/onvif-go"
|
||||
onviftesting "github.com/0x524a/onvif-go/testing"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -150,6 +153,7 @@ var (
|
||||
timeout = flag.Int("timeout", 30, "Request timeout in seconds") //nolint:mnd // Default timeout value
|
||||
verbose = flag.Bool("verbose", false, "Verbose output")
|
||||
captureXML = flag.Bool("capture-xml", false, "Capture raw SOAP XML traffic and create tar.gz archive")
|
||||
captureAll = flag.Bool("capture-all", false, "Capture all READ operations (comprehensive mode, implies -capture-xml)")
|
||||
)
|
||||
|
||||
//nolint:funlen,gocognit,gocyclo // Main function has high complexity due to multiple diagnostic operations
|
||||
@@ -192,6 +196,11 @@ func main() {
|
||||
RawResponses: make(map[string]interface{}),
|
||||
}
|
||||
|
||||
// If capture-all is set, enable capture-xml automatically
|
||||
if *captureAll {
|
||||
*captureXML = true
|
||||
}
|
||||
|
||||
// Setup XML capture if requested
|
||||
var loggingTransport *LoggingTransport
|
||||
var xmlCaptureDir string
|
||||
@@ -246,72 +255,79 @@ func main() {
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
fmt.Println("Starting diagnostic collection...")
|
||||
fmt.Println()
|
||||
|
||||
// Test 1: Get Device Information
|
||||
logStepf("1. Getting device information...")
|
||||
report.DeviceInfo = testGetDeviceInformation(ctx, client, report)
|
||||
|
||||
// Test 2: Get System Date and Time
|
||||
logStepf("2. Getting system date and time...")
|
||||
report.SystemDateTime = testGetSystemDateTime(ctx, client, report)
|
||||
|
||||
// Test 3: Get Capabilities
|
||||
logStepf("3. Getting capabilities...")
|
||||
report.Capabilities = testGetCapabilities(ctx, client, report)
|
||||
|
||||
// Test 4: Initialize (discover services)
|
||||
logStepf("4. Discovering service endpoints...")
|
||||
if err := client.Initialize(ctx); err != nil {
|
||||
logErrorf("Service discovery failed: %v", err)
|
||||
report.Errors = append(report.Errors, ErrorLog{
|
||||
Operation: "Initialize",
|
||||
Error: err.Error(),
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
})
|
||||
if *captureAll {
|
||||
fmt.Println("Starting COMPREHENSIVE diagnostic collection...")
|
||||
fmt.Println("This will capture all READ operations for testing.")
|
||||
fmt.Println()
|
||||
runComprehensiveCapture(ctx, client, report)
|
||||
} else {
|
||||
logSuccessf("Service endpoints discovered")
|
||||
}
|
||||
fmt.Println("Starting diagnostic collection...")
|
||||
fmt.Println()
|
||||
|
||||
// Test 5: Get Profiles
|
||||
logStepf("5. Getting media profiles...")
|
||||
report.Profiles = testGetProfiles(ctx, client, report)
|
||||
// Test 1: Get Device Information
|
||||
logStepf("1. Getting device information...")
|
||||
report.DeviceInfo = testGetDeviceInformation(ctx, client, report)
|
||||
|
||||
// Test 6: Get Stream URIs (for each profile)
|
||||
if report.Profiles != nil && report.Profiles.Success {
|
||||
logStepf("6. Getting stream URIs for all profiles...")
|
||||
report.StreamURIs = testGetStreamURIs(ctx, client, report.Profiles.Data, report)
|
||||
}
|
||||
// Test 2: Get System Date and Time
|
||||
logStepf("2. Getting system date and time...")
|
||||
report.SystemDateTime = testGetSystemDateTime(ctx, client, report)
|
||||
|
||||
// Test 7: Get Snapshot URIs (for each profile)
|
||||
if report.Profiles != nil && report.Profiles.Success {
|
||||
logStepf("7. Getting snapshot URIs for all profiles...")
|
||||
report.SnapshotURIs = testGetSnapshotURIs(ctx, client, report.Profiles.Data, report)
|
||||
}
|
||||
// Test 3: Get Capabilities
|
||||
logStepf("3. Getting capabilities...")
|
||||
report.Capabilities = testGetCapabilities(ctx, client, report)
|
||||
|
||||
// Test 8: Get Video Encoder Configurations
|
||||
if report.Profiles != nil && report.Profiles.Success {
|
||||
logStepf("8. Getting video encoder configurations...")
|
||||
report.VideoEncoders = testGetVideoEncoders(ctx, client, report.Profiles.Data, report)
|
||||
}
|
||||
// Test 4: Initialize (discover services)
|
||||
logStepf("4. Discovering service endpoints...")
|
||||
if err := client.Initialize(ctx); err != nil {
|
||||
logErrorf("Service discovery failed: %v", err)
|
||||
report.Errors = append(report.Errors, ErrorLog{
|
||||
Operation: "Initialize",
|
||||
Error: err.Error(),
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
})
|
||||
} else {
|
||||
logSuccessf("Service endpoints discovered")
|
||||
}
|
||||
|
||||
// Test 9: Get Imaging Settings
|
||||
if report.Profiles != nil && report.Profiles.Success {
|
||||
logStepf("9. Getting imaging settings...")
|
||||
report.ImagingSettings = testGetImagingSettings(ctx, client, report.Profiles.Data, report)
|
||||
}
|
||||
// Test 5: Get Profiles
|
||||
logStepf("5. Getting media profiles...")
|
||||
report.Profiles = testGetProfiles(ctx, client, report)
|
||||
|
||||
// Test 10: Get PTZ Status (if PTZ is available)
|
||||
if report.Profiles != nil && report.Profiles.Success {
|
||||
logStepf("10. Getting PTZ status...")
|
||||
report.PTZStatus = testGetPTZStatus(ctx, client, report.Profiles.Data, report)
|
||||
}
|
||||
// Test 6: Get Stream URIs (for each profile)
|
||||
if report.Profiles != nil && report.Profiles.Success {
|
||||
logStepf("6. Getting stream URIs for all profiles...")
|
||||
report.StreamURIs = testGetStreamURIs(ctx, client, report.Profiles.Data, report)
|
||||
}
|
||||
|
||||
// Test 11: Get PTZ Presets (if PTZ is available)
|
||||
if report.Profiles != nil && report.Profiles.Success {
|
||||
logStepf("11. Getting PTZ presets...")
|
||||
report.PTZPresets = testGetPTZPresets(ctx, client, report.Profiles.Data, report)
|
||||
// Test 7: Get Snapshot URIs (for each profile)
|
||||
if report.Profiles != nil && report.Profiles.Success {
|
||||
logStepf("7. Getting snapshot URIs for all profiles...")
|
||||
report.SnapshotURIs = testGetSnapshotURIs(ctx, client, report.Profiles.Data, report)
|
||||
}
|
||||
|
||||
// Test 8: Get Video Encoder Configurations
|
||||
if report.Profiles != nil && report.Profiles.Success {
|
||||
logStepf("8. Getting video encoder configurations...")
|
||||
report.VideoEncoders = testGetVideoEncoders(ctx, client, report.Profiles.Data, report)
|
||||
}
|
||||
|
||||
// Test 9: Get Imaging Settings
|
||||
if report.Profiles != nil && report.Profiles.Success {
|
||||
logStepf("9. Getting imaging settings...")
|
||||
report.ImagingSettings = testGetImagingSettings(ctx, client, report.Profiles.Data, report)
|
||||
}
|
||||
|
||||
// Test 10: Get PTZ Status (if PTZ is available)
|
||||
if report.Profiles != nil && report.Profiles.Success {
|
||||
logStepf("10. Getting PTZ status...")
|
||||
report.PTZStatus = testGetPTZStatus(ctx, client, report.Profiles.Data, report)
|
||||
}
|
||||
|
||||
// Test 11: Get PTZ Presets (if PTZ is available)
|
||||
if report.Profiles != nil && report.Profiles.Success {
|
||||
logStepf("11. Getting PTZ presets...")
|
||||
report.PTZPresets = testGetPTZPresets(ctx, client, report.Profiles.Data, report)
|
||||
}
|
||||
}
|
||||
|
||||
// Generate output filename based on device info
|
||||
@@ -327,7 +343,14 @@ func main() {
|
||||
// Create XML archive if capture was enabled
|
||||
if *captureXML && loggingTransport != nil {
|
||||
fmt.Println()
|
||||
logStepf("Creating XML capture archive...")
|
||||
logStepf("Creating V2 XML capture archive...")
|
||||
|
||||
// V2: Save metadata.json before creating archive
|
||||
if err := loggingTransport.SaveMetadata(report); err != nil {
|
||||
logErrorf("Failed to save metadata: %v", err)
|
||||
} else {
|
||||
logSuccessf("V2 metadata.json generated")
|
||||
}
|
||||
|
||||
// Generate archive name based on device info
|
||||
var archiveName string
|
||||
@@ -344,10 +367,10 @@ func main() {
|
||||
|
||||
archivePath := filepath.Join(*outputDir, archiveName)
|
||||
|
||||
if err := createTarGz(xmlCaptureDir, archivePath); err != nil {
|
||||
if err := createTarGzV2(xmlCaptureDir, archivePath); err != nil {
|
||||
logErrorf("Failed to create XML archive: %v", err)
|
||||
} else {
|
||||
logSuccessf("XML archive created: %s", archiveName)
|
||||
logSuccessf("V2 XML archive created: %s", archiveName)
|
||||
logSuccessf("Total SOAP calls captured: %d", loggingTransport.Counter)
|
||||
|
||||
// Remove temporary directory
|
||||
@@ -912,18 +935,452 @@ func logInfof(format string, args ...interface{}) {
|
||||
fmt.Printf(" ℹ %s\n", fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Comprehensive Capture Mode
|
||||
// =============================================================================
|
||||
|
||||
// runComprehensiveCapture captures all READ operations from the camera.
|
||||
// This function exercises the full API to create a comprehensive test fixture.
|
||||
//
|
||||
//nolint:funlen,gocognit,gocyclo // Comprehensive capture requires many operations
|
||||
func runComprehensiveCapture(ctx context.Context, client *onvif.Client, report *CameraReport) {
|
||||
successCount := 0
|
||||
failCount := 0
|
||||
totalOps := 0
|
||||
|
||||
// Phase 1: Get device information first (needed for report)
|
||||
logStepf("Phase 1: Core device information...")
|
||||
|
||||
report.DeviceInfo = testGetDeviceInformation(ctx, client, report)
|
||||
if report.DeviceInfo != nil && report.DeviceInfo.Success {
|
||||
successCount++
|
||||
} else {
|
||||
failCount++
|
||||
}
|
||||
totalOps++
|
||||
|
||||
report.SystemDateTime = testGetSystemDateTime(ctx, client, report)
|
||||
if report.SystemDateTime != nil && report.SystemDateTime.Success {
|
||||
successCount++
|
||||
} else {
|
||||
failCount++
|
||||
}
|
||||
totalOps++
|
||||
|
||||
report.Capabilities = testGetCapabilities(ctx, client, report)
|
||||
if report.Capabilities != nil && report.Capabilities.Success {
|
||||
successCount++
|
||||
} else {
|
||||
failCount++
|
||||
}
|
||||
totalOps++
|
||||
|
||||
// Phase 2: Initialize to discover service endpoints
|
||||
logStepf("Phase 2: Service discovery...")
|
||||
if err := client.Initialize(ctx); err != nil {
|
||||
logErrorf("Service discovery failed: %v", err)
|
||||
report.Errors = append(report.Errors, ErrorLog{
|
||||
Operation: "Initialize",
|
||||
Error: err.Error(),
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
})
|
||||
failCount++
|
||||
} else {
|
||||
logSuccessf("Service endpoints discovered")
|
||||
successCount++
|
||||
}
|
||||
totalOps++
|
||||
|
||||
// Phase 3: Device service operations (no dependencies)
|
||||
logStepf("Phase 3: Device service operations...")
|
||||
deviceOps := []struct {
|
||||
name string
|
||||
fn func() error
|
||||
}{
|
||||
{"GetHostname", func() error { _, err := client.GetHostname(ctx); return err }},
|
||||
{"GetDNS", func() error { _, err := client.GetDNS(ctx); return err }},
|
||||
{"GetNTP", func() error { _, err := client.GetNTP(ctx); return err }},
|
||||
{"GetNetworkInterfaces", func() error { _, err := client.GetNetworkInterfaces(ctx); return err }},
|
||||
{"GetNetworkProtocols", func() error { _, err := client.GetNetworkProtocols(ctx); return err }},
|
||||
{"GetNetworkDefaultGateway", func() error { _, err := client.GetNetworkDefaultGateway(ctx); return err }},
|
||||
{"GetScopes", func() error { _, err := client.GetScopes(ctx); return err }},
|
||||
{"GetUsers", func() error { _, err := client.GetUsers(ctx); return err }},
|
||||
{"GetDiscoveryMode", func() error { _, err := client.GetDiscoveryMode(ctx); return err }},
|
||||
{"GetRemoteDiscoveryMode", func() error { _, err := client.GetRemoteDiscoveryMode(ctx); return err }},
|
||||
{"GetEndpointReference", func() error { _, err := client.GetEndpointReference(ctx); return err }},
|
||||
{"GetRelayOutputs", func() error { _, err := client.GetRelayOutputs(ctx); return err }},
|
||||
{"GetRemoteUser", func() error { _, err := client.GetRemoteUser(ctx); return err }},
|
||||
{"GetIPAddressFilter", func() error { _, err := client.GetIPAddressFilter(ctx); return err }},
|
||||
{"GetZeroConfiguration", func() error { _, err := client.GetZeroConfiguration(ctx); return err }},
|
||||
{"GetServices", func() error { _, err := client.GetServices(ctx, true); return err }},
|
||||
{"GetServiceCapabilities", func() error { _, err := client.GetServiceCapabilities(ctx); return err }},
|
||||
{"GetStorageConfigurations", func() error { _, err := client.GetStorageConfigurations(ctx); return err }},
|
||||
{"GetGeoLocation", func() error { _, err := client.GetGeoLocation(ctx); return err }},
|
||||
{"GetDPAddresses", func() error { _, err := client.GetDPAddresses(ctx); return err }},
|
||||
{"GetAccessPolicy", func() error { _, err := client.GetAccessPolicy(ctx); return err }},
|
||||
{"GetWsdlURL", func() error { _, err := client.GetWsdlURL(ctx); return err }},
|
||||
{"GetPasswordComplexityConfiguration", func() error { _, err := client.GetPasswordComplexityConfiguration(ctx); return err }},
|
||||
{"GetPasswordHistoryConfiguration", func() error { _, err := client.GetPasswordHistoryConfiguration(ctx); return err }},
|
||||
{"GetAuthFailureWarningConfiguration", func() error { _, err := client.GetAuthFailureWarningConfiguration(ctx); return err }},
|
||||
}
|
||||
|
||||
for _, op := range deviceOps {
|
||||
if err := op.fn(); err != nil {
|
||||
if *verbose {
|
||||
logErrorf("%s: %v", op.name, err)
|
||||
}
|
||||
failCount++
|
||||
} else {
|
||||
if *verbose {
|
||||
logSuccessf("%s", op.name)
|
||||
}
|
||||
successCount++
|
||||
}
|
||||
totalOps++
|
||||
}
|
||||
logSuccessf("Device operations: %d captured", len(deviceOps))
|
||||
|
||||
// Phase 4: Media service - Get profiles and sources
|
||||
logStepf("Phase 4: Media profiles and sources...")
|
||||
report.Profiles = testGetProfiles(ctx, client, report)
|
||||
totalOps++
|
||||
if report.Profiles != nil && report.Profiles.Success {
|
||||
successCount++
|
||||
} else {
|
||||
failCount++
|
||||
}
|
||||
|
||||
// Get video sources
|
||||
videoSources, err := client.GetVideoSources(ctx)
|
||||
totalOps++
|
||||
if err != nil {
|
||||
if *verbose {
|
||||
logErrorf("GetVideoSources: %v", err)
|
||||
}
|
||||
failCount++
|
||||
} else {
|
||||
if *verbose {
|
||||
logSuccessf("GetVideoSources: %d sources", len(videoSources))
|
||||
}
|
||||
successCount++
|
||||
}
|
||||
|
||||
// Get audio sources
|
||||
audioSources, err := client.GetAudioSources(ctx)
|
||||
totalOps++
|
||||
if err != nil {
|
||||
if *verbose {
|
||||
logErrorf("GetAudioSources: %v", err)
|
||||
}
|
||||
failCount++
|
||||
} else {
|
||||
if *verbose {
|
||||
logSuccessf("GetAudioSources: %d sources", len(audioSources))
|
||||
}
|
||||
successCount++
|
||||
}
|
||||
|
||||
// Get audio outputs
|
||||
_, err = client.GetAudioOutputs(ctx)
|
||||
totalOps++
|
||||
if err != nil {
|
||||
if *verbose {
|
||||
logErrorf("GetAudioOutputs: %v", err)
|
||||
}
|
||||
failCount++
|
||||
} else {
|
||||
if *verbose {
|
||||
logSuccessf("GetAudioOutputs")
|
||||
}
|
||||
successCount++
|
||||
}
|
||||
|
||||
// Phase 5: Profile-dependent operations
|
||||
if report.Profiles != nil && report.Profiles.Success && len(report.Profiles.Data) > 0 {
|
||||
logStepf("Phase 5: Profile-dependent operations...")
|
||||
|
||||
for _, profile := range report.Profiles.Data {
|
||||
// GetProfile
|
||||
_, err := client.GetProfile(ctx, profile.Token)
|
||||
totalOps++
|
||||
if err != nil {
|
||||
failCount++
|
||||
} else {
|
||||
successCount++
|
||||
}
|
||||
|
||||
// GetStreamURI
|
||||
_, err = client.GetStreamURI(ctx, profile.Token)
|
||||
totalOps++
|
||||
if err != nil {
|
||||
failCount++
|
||||
} else {
|
||||
successCount++
|
||||
}
|
||||
|
||||
// GetSnapshotURI
|
||||
_, err = client.GetSnapshotURI(ctx, profile.Token)
|
||||
totalOps++
|
||||
if err != nil {
|
||||
failCount++
|
||||
} else {
|
||||
successCount++
|
||||
}
|
||||
|
||||
// PTZ operations (if PTZ configuration exists)
|
||||
if profile.PTZConfiguration != nil {
|
||||
_, err = client.GetStatus(ctx, profile.Token)
|
||||
totalOps++
|
||||
if err != nil {
|
||||
failCount++
|
||||
} else {
|
||||
successCount++
|
||||
}
|
||||
|
||||
_, err = client.GetPresets(ctx, profile.Token)
|
||||
totalOps++
|
||||
if err != nil {
|
||||
failCount++
|
||||
} else {
|
||||
successCount++
|
||||
}
|
||||
}
|
||||
|
||||
// Video encoder configuration
|
||||
if profile.VideoEncoderConfiguration != nil {
|
||||
_, err = client.GetVideoEncoderConfiguration(ctx, profile.VideoEncoderConfiguration.Token)
|
||||
totalOps++
|
||||
if err != nil {
|
||||
failCount++
|
||||
} else {
|
||||
successCount++
|
||||
}
|
||||
|
||||
_, err = client.GetVideoEncoderConfigurationOptions(ctx, profile.VideoEncoderConfiguration.Token)
|
||||
totalOps++
|
||||
if err != nil {
|
||||
failCount++
|
||||
} else {
|
||||
successCount++
|
||||
}
|
||||
}
|
||||
|
||||
// Audio encoder configuration
|
||||
if profile.AudioEncoderConfiguration != nil {
|
||||
_, err = client.GetAudioEncoderConfiguration(ctx, profile.AudioEncoderConfiguration.Token)
|
||||
totalOps++
|
||||
if err != nil {
|
||||
failCount++
|
||||
} else {
|
||||
successCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
logSuccessf("Profile operations completed for %d profiles", len(report.Profiles.Data))
|
||||
}
|
||||
|
||||
// Phase 6: Video source dependent operations
|
||||
if len(videoSources) > 0 {
|
||||
logStepf("Phase 6: Video source operations...")
|
||||
|
||||
for _, source := range videoSources {
|
||||
// Imaging settings
|
||||
_, err := client.GetImagingSettings(ctx, source.Token)
|
||||
totalOps++
|
||||
if err != nil {
|
||||
failCount++
|
||||
} else {
|
||||
successCount++
|
||||
}
|
||||
|
||||
// Imaging options
|
||||
_, err = client.GetOptions(ctx, source.Token)
|
||||
totalOps++
|
||||
if err != nil {
|
||||
failCount++
|
||||
} else {
|
||||
successCount++
|
||||
}
|
||||
|
||||
// Imaging move options
|
||||
_, err = client.GetMoveOptions(ctx, source.Token)
|
||||
totalOps++
|
||||
if err != nil {
|
||||
failCount++
|
||||
} else {
|
||||
successCount++
|
||||
}
|
||||
}
|
||||
logSuccessf("Video source operations completed for %d sources", len(videoSources))
|
||||
}
|
||||
|
||||
// Phase 7: Configuration listings
|
||||
logStepf("Phase 7: Configuration listings...")
|
||||
configOps := []struct {
|
||||
name string
|
||||
fn func() error
|
||||
}{
|
||||
{"GetVideoSourceConfigurations", func() error { _, err := client.GetVideoSourceConfigurations(ctx); return err }},
|
||||
{"GetVideoEncoderConfigurations", func() error { _, err := client.GetVideoEncoderConfigurations(ctx); return err }},
|
||||
{"GetAudioSourceConfigurations", func() error { _, err := client.GetAudioSourceConfigurations(ctx); return err }},
|
||||
{"GetAudioEncoderConfigurations", func() error { _, err := client.GetAudioEncoderConfigurations(ctx); return err }},
|
||||
{"GetAudioOutputConfigurations", func() error { _, err := client.GetAudioOutputConfigurations(ctx); return err }},
|
||||
{"GetMetadataConfigurations", func() error { _, err := client.GetMetadataConfigurations(ctx); return err }},
|
||||
{"GetMediaServiceCapabilities", func() error { _, err := client.GetMediaServiceCapabilities(ctx); return err }},
|
||||
}
|
||||
|
||||
for _, op := range configOps {
|
||||
if err := op.fn(); err != nil {
|
||||
if *verbose {
|
||||
logErrorf("%s: %v", op.name, err)
|
||||
}
|
||||
failCount++
|
||||
} else {
|
||||
if *verbose {
|
||||
logSuccessf("%s", op.name)
|
||||
}
|
||||
successCount++
|
||||
}
|
||||
totalOps++
|
||||
}
|
||||
logSuccessf("Configuration listings: %d captured", len(configOps))
|
||||
|
||||
// Phase 8: Event service
|
||||
logStepf("Phase 8: Event service...")
|
||||
eventOps := []struct {
|
||||
name string
|
||||
fn func() error
|
||||
}{
|
||||
{"GetEventServiceCapabilities", func() error { _, err := client.GetEventServiceCapabilities(ctx); return err }},
|
||||
{"GetEventProperties", func() error { _, err := client.GetEventProperties(ctx); return err }},
|
||||
}
|
||||
|
||||
for _, op := range eventOps {
|
||||
if err := op.fn(); err != nil {
|
||||
if *verbose {
|
||||
logErrorf("%s: %v", op.name, err)
|
||||
}
|
||||
failCount++
|
||||
} else {
|
||||
if *verbose {
|
||||
logSuccessf("%s", op.name)
|
||||
}
|
||||
successCount++
|
||||
}
|
||||
totalOps++
|
||||
}
|
||||
logSuccessf("Event operations: %d captured", len(eventOps))
|
||||
|
||||
// Phase 9: Certificate operations
|
||||
logStepf("Phase 9: Certificate and security operations...")
|
||||
certOps := []struct {
|
||||
name string
|
||||
fn func() error
|
||||
}{
|
||||
{"GetCertificates", func() error { _, err := client.GetCertificates(ctx); return err }},
|
||||
{"GetCACertificates", func() error { _, err := client.GetCACertificates(ctx); return err }},
|
||||
{"GetCertificatesStatus", func() error { _, err := client.GetCertificatesStatus(ctx); return err }},
|
||||
{"GetClientCertificateMode", func() error { _, err := client.GetClientCertificateMode(ctx); return err }},
|
||||
}
|
||||
|
||||
for _, op := range certOps {
|
||||
if err := op.fn(); err != nil {
|
||||
if *verbose {
|
||||
logErrorf("%s: %v", op.name, err)
|
||||
}
|
||||
failCount++
|
||||
} else {
|
||||
if *verbose {
|
||||
logSuccessf("%s", op.name)
|
||||
}
|
||||
successCount++
|
||||
}
|
||||
totalOps++
|
||||
}
|
||||
logSuccessf("Certificate operations: %d captured", len(certOps))
|
||||
|
||||
// Phase 10: WiFi operations (may not be supported by all cameras)
|
||||
logStepf("Phase 10: WiFi operations...")
|
||||
wifiOps := []struct {
|
||||
name string
|
||||
fn func() error
|
||||
}{
|
||||
{"GetDot11Capabilities", func() error { _, err := client.GetDot11Capabilities(ctx); return err }},
|
||||
{"GetDot1XConfigurations", func() error { _, err := client.GetDot1XConfigurations(ctx); return err }},
|
||||
}
|
||||
|
||||
for _, op := range wifiOps {
|
||||
if err := op.fn(); err != nil {
|
||||
if *verbose {
|
||||
logErrorf("%s: %v", op.name, err)
|
||||
}
|
||||
failCount++
|
||||
} else {
|
||||
if *verbose {
|
||||
logSuccessf("%s", op.name)
|
||||
}
|
||||
successCount++
|
||||
}
|
||||
totalOps++
|
||||
}
|
||||
logSuccessf("WiFi operations: %d captured", len(wifiOps))
|
||||
|
||||
// Summary
|
||||
fmt.Println()
|
||||
fmt.Println("========================================")
|
||||
fmt.Printf("Comprehensive capture complete!\n")
|
||||
fmt.Printf(" Total operations: %d\n", totalOps)
|
||||
fmt.Printf(" Successful: %d\n", successCount)
|
||||
fmt.Printf(" Failed: %d\n", failCount)
|
||||
fmt.Printf(" Success rate: %.1f%%\n", float64(successCount)/float64(totalOps)*100)
|
||||
fmt.Println("========================================")
|
||||
}
|
||||
|
||||
// XML Capture functionality
|
||||
|
||||
// XMLCapture stores a request/response pair.
|
||||
// XMLCapture stores a request/response pair (V2 format with parameter awareness).
|
||||
type XMLCapture struct {
|
||||
Timestamp string `json:"timestamp"`
|
||||
Operation int `json:"operation"`
|
||||
// Version indicates the capture format version ("2.0" for V2)
|
||||
Version string `json:"version"`
|
||||
|
||||
// Timestamp is when the exchange was captured (RFC3339 format)
|
||||
Timestamp string `json:"timestamp"`
|
||||
|
||||
// Sequence is the capture order (1-indexed for V2)
|
||||
Sequence int `json:"sequence"`
|
||||
|
||||
// Operation is deprecated in V2, kept for backward compatibility
|
||||
Operation int `json:"operation,omitempty"`
|
||||
|
||||
// OperationName is the SOAP operation name (e.g., "GetDeviceInformation")
|
||||
OperationName string `json:"operation_name"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
RequestBody string `json:"request_body"`
|
||||
ResponseBody string `json:"response_body"`
|
||||
StatusCode int `json:"status_code"`
|
||||
Error string `json:"error,omitempty"`
|
||||
|
||||
// ServiceType categorizes which ONVIF service handles this operation
|
||||
ServiceType string `json:"service_type,omitempty"`
|
||||
|
||||
// Parameters contains extracted key parameters from the request
|
||||
Parameters map[string]interface{} `json:"parameters,omitempty"`
|
||||
|
||||
// Endpoint is the URL the request was sent to
|
||||
Endpoint string `json:"endpoint"`
|
||||
|
||||
// RequestBody is the full SOAP request XML
|
||||
RequestBody string `json:"request_body"`
|
||||
|
||||
// ResponseBody is the full SOAP response XML
|
||||
ResponseBody string `json:"response_body"`
|
||||
|
||||
// StatusCode is the HTTP response status code
|
||||
StatusCode int `json:"status_code"`
|
||||
|
||||
// DurationNs is the request duration in nanoseconds
|
||||
DurationNs int64 `json:"duration_ns,omitempty"`
|
||||
|
||||
// Success indicates if the operation succeeded (no SOAP fault)
|
||||
Success bool `json:"success"`
|
||||
|
||||
// Error contains error message if the operation failed
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// LoggingTransport wraps http.RoundTripper to log requests and responses.
|
||||
@@ -931,13 +1388,24 @@ type LoggingTransport struct {
|
||||
Transport http.RoundTripper
|
||||
LogDir string
|
||||
Counter int
|
||||
// V2 additions for metadata generation
|
||||
captures []*XMLCapture
|
||||
serviceMap map[string]string // operation -> service type
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (t *LoggingTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
t.mu.Lock()
|
||||
t.Counter++
|
||||
sequence := t.Counter
|
||||
t.mu.Unlock()
|
||||
|
||||
startTime := time.Now()
|
||||
capture := XMLCapture{
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
Operation: t.Counter,
|
||||
Version: onviftesting.CaptureVersion,
|
||||
Timestamp: startTime.Format(time.RFC3339),
|
||||
Sequence: sequence,
|
||||
Operation: sequence, // Keep for backward compatibility
|
||||
Endpoint: req.URL.String(),
|
||||
}
|
||||
|
||||
@@ -948,6 +1416,11 @@ func (t *LoggingTransport) RoundTrip(req *http.Request) (*http.Response, error)
|
||||
capture.RequestBody = string(bodyBytes)
|
||||
// Extract operation name from SOAP body
|
||||
capture.OperationName = extractSOAPOperation(capture.RequestBody)
|
||||
// V2: Extract service type
|
||||
serviceType := onviftesting.DetermineServiceType(capture.RequestBody)
|
||||
capture.ServiceType = string(serviceType)
|
||||
// V2: Extract parameters
|
||||
capture.Parameters = onviftesting.ExtractParameters(capture.OperationName, capture.RequestBody)
|
||||
// Restore the body for the actual request
|
||||
req.Body = io.NopCloser(strings.NewReader(string(bodyBytes)))
|
||||
}
|
||||
@@ -955,8 +1428,13 @@ func (t *LoggingTransport) RoundTrip(req *http.Request) (*http.Response, error)
|
||||
|
||||
// Make the actual request
|
||||
resp, err := t.Transport.RoundTrip(req)
|
||||
|
||||
// V2: Track request duration
|
||||
capture.DurationNs = time.Since(startTime).Nanoseconds()
|
||||
|
||||
if err != nil {
|
||||
capture.Error = err.Error()
|
||||
capture.Success = false
|
||||
t.saveCapture(&capture)
|
||||
|
||||
return nil, fmt.Errorf("round trip failed: %w", err)
|
||||
@@ -973,6 +1451,12 @@ func (t *LoggingTransport) RoundTrip(req *http.Request) (*http.Response, error)
|
||||
}
|
||||
}
|
||||
|
||||
// V2: Determine success (no SOAP fault and 2xx status)
|
||||
capture.Success = resp.StatusCode >= 200 && resp.StatusCode < 300 &&
|
||||
!strings.Contains(capture.ResponseBody, "<soap:Fault>") &&
|
||||
!strings.Contains(capture.ResponseBody, "<Fault>") &&
|
||||
!strings.Contains(capture.ResponseBody, ":Fault>")
|
||||
|
||||
t.saveCapture(&capture)
|
||||
|
||||
return resp, nil
|
||||
@@ -1012,8 +1496,19 @@ func prettyPrintXML(xmlStr string) string {
|
||||
}
|
||||
|
||||
func (t *LoggingTransport) saveCapture(capture *XMLCapture) {
|
||||
// Create filename base using operation name
|
||||
baseFilename := fmt.Sprintf("capture_%03d_%s", capture.Operation, capture.OperationName)
|
||||
// V2: Track capture for metadata generation
|
||||
t.mu.Lock()
|
||||
t.captures = append(t.captures, capture)
|
||||
if t.serviceMap == nil {
|
||||
t.serviceMap = make(map[string]string)
|
||||
}
|
||||
if capture.ServiceType != "" && capture.ServiceType != "Unknown" {
|
||||
t.serviceMap[capture.OperationName] = capture.ServiceType
|
||||
}
|
||||
t.mu.Unlock()
|
||||
|
||||
// Create filename base using sequence and operation name
|
||||
baseFilename := fmt.Sprintf("capture_%03d_%s", capture.Sequence, capture.OperationName)
|
||||
|
||||
// Save as individual JSON file
|
||||
filename := filepath.Join(t.LogDir, baseFilename+".json")
|
||||
@@ -1046,6 +1541,50 @@ func (t *LoggingTransport) saveCapture(capture *XMLCapture) {
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateMetadata creates the V2 metadata.json file from captured exchanges.
|
||||
func (t *LoggingTransport) GenerateMetadata(report *CameraReport) *onviftesting.CaptureMetadata {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
metadata := &onviftesting.CaptureMetadata{
|
||||
Version: onviftesting.CaptureVersion,
|
||||
CreatedAt: time.Now(),
|
||||
ToolVersion: version,
|
||||
TotalExchanges: len(t.captures),
|
||||
ServiceMap: t.serviceMap,
|
||||
}
|
||||
|
||||
// Extract camera info from report
|
||||
if report.DeviceInfo != nil && report.DeviceInfo.Success && report.DeviceInfo.Data != nil {
|
||||
metadata.CameraInfo = onviftesting.CameraInfo{
|
||||
Manufacturer: report.DeviceInfo.Data.Manufacturer,
|
||||
Model: report.DeviceInfo.Data.Model,
|
||||
FirmwareVersion: report.DeviceInfo.Data.FirmwareVersion,
|
||||
SerialNumber: report.DeviceInfo.Data.SerialNumber,
|
||||
HardwareID: report.DeviceInfo.Data.HardwareID,
|
||||
}
|
||||
}
|
||||
|
||||
return metadata
|
||||
}
|
||||
|
||||
// SaveMetadata writes the metadata.json file to the log directory.
|
||||
func (t *LoggingTransport) SaveMetadata(report *CameraReport) error {
|
||||
metadata := t.GenerateMetadata(report)
|
||||
|
||||
data, err := json.MarshalIndent(metadata, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal metadata: %w", err)
|
||||
}
|
||||
|
||||
filename := filepath.Join(t.LogDir, "metadata.json")
|
||||
if err := os.WriteFile(filename, data, 0600); err != nil { //nolint:mnd // 0600 appropriate for diagnostic files
|
||||
return fmt.Errorf("failed to write metadata: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// extractSOAPOperation extracts the operation name from a SOAP request body.
|
||||
func extractSOAPOperation(soapBody string) string {
|
||||
// Look for the operation element in the SOAP Body
|
||||
@@ -1094,8 +1633,8 @@ func extractSOAPOperation(soapBody string) string {
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
// createTarGz creates a tar.gz archive from a directory.
|
||||
func createTarGz(sourceDir, archivePath string) error {
|
||||
// createTarGzV2 creates a V2 tar.gz archive with metadata.json first.
|
||||
func createTarGzV2(sourceDir, archivePath string) error {
|
||||
// Create archive file
|
||||
archiveFile, err := os.Create(archivePath) //nolint:gosec // Archive path is validated before use
|
||||
if err != nil {
|
||||
@@ -1117,16 +1656,54 @@ func createTarGz(sourceDir, archivePath string) error {
|
||||
_ = tarWriter.Close()
|
||||
}()
|
||||
|
||||
// Walk through source directory
|
||||
// V2: Collect all files and sort them with metadata.json first
|
||||
var files []string
|
||||
if err := filepath.Walk(sourceDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Skip the root directory itself
|
||||
if path == sourceDir {
|
||||
if path == sourceDir || info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
files = append(files, path)
|
||||
return nil
|
||||
}); err != nil {
|
||||
return fmt.Errorf("failed to walk source directory: %w", err)
|
||||
}
|
||||
|
||||
// Sort files: metadata.json first, then capture JSON files in order, then XML files
|
||||
sort.Slice(files, func(i, j int) bool {
|
||||
nameI := filepath.Base(files[i])
|
||||
nameJ := filepath.Base(files[j])
|
||||
|
||||
// metadata.json always first
|
||||
if nameI == "metadata.json" {
|
||||
return true
|
||||
}
|
||||
if nameJ == "metadata.json" {
|
||||
return false
|
||||
}
|
||||
|
||||
// JSON files before XML files
|
||||
isJSONi := strings.HasSuffix(nameI, ".json")
|
||||
isJSONj := strings.HasSuffix(nameJ, ".json")
|
||||
if isJSONi && !isJSONj {
|
||||
return true
|
||||
}
|
||||
if !isJSONi && isJSONj {
|
||||
return false
|
||||
}
|
||||
|
||||
// Sort by name
|
||||
return nameI < nameJ
|
||||
})
|
||||
|
||||
// Write files in sorted order
|
||||
for _, path := range files {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to stat file: %w", err)
|
||||
}
|
||||
|
||||
// Create tar header
|
||||
header, err := tar.FileInfoHeader(info, "")
|
||||
@@ -1146,24 +1723,17 @@ func createTarGz(sourceDir, archivePath string) error {
|
||||
return fmt.Errorf("failed to write tar header: %w", err)
|
||||
}
|
||||
|
||||
// If it's a file, write its content
|
||||
if !info.IsDir() {
|
||||
file, err := os.Open(path) //nolint:gosec // File path is from filepath.Walk, safe
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open file: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = file.Close()
|
||||
}()
|
||||
|
||||
if _, err := io.Copy(tarWriter, file); err != nil {
|
||||
return fmt.Errorf("failed to write file to tar: %w", err)
|
||||
}
|
||||
// Write file content
|
||||
file, err := os.Open(path) //nolint:gosec // File path is from filepath.Walk, safe
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return fmt.Errorf("failed to walk source directory: %w", err)
|
||||
if _, err := io.Copy(tarWriter, file); err != nil {
|
||||
_ = file.Close()
|
||||
return fmt.Errorf("failed to write file to tar: %w", err)
|
||||
}
|
||||
_ = file.Close()
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user