fix: update .gitignore to preserve cmd/ source directories and add missing CLI tools
This commit is contained in:
+7
-5
@@ -26,14 +26,16 @@ go.work
|
|||||||
*~
|
*~
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
# Binaries
|
# Binaries (in root, bin, or dist directories)
|
||||||
bin/
|
bin/
|
||||||
dist/
|
dist/
|
||||||
releases/
|
releases/
|
||||||
onvif-diagnostics
|
/onvif-diagnostics
|
||||||
onvif-server
|
/onvif-server
|
||||||
onvif-server-example
|
/onvif-server-example
|
||||||
generate-tests
|
/generate-tests
|
||||||
|
/onvif-cli
|
||||||
|
/onvif-quick
|
||||||
|
|
||||||
# Temporary files
|
# Temporary files
|
||||||
tmp/
|
tmp/
|
||||||
|
|||||||
@@ -0,0 +1,236 @@
|
|||||||
|
# Test Generator
|
||||||
|
|
||||||
|
Automatically generate Go tests from captured ONVIF camera XML traffic.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This tool reads XML capture archives (created by `onvif-diagnostics -capture-xml`) and generates complete Go test files that replay the captured SOAP traffic through a mock server.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Basic Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./generate-tests \
|
||||||
|
-capture camera-logs/Camera_Model_xmlcapture_timestamp.tar.gz \
|
||||||
|
-output testdata/captures/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
```
|
||||||
|
-capture string
|
||||||
|
Path to XML capture archive (.tar.gz) (required)
|
||||||
|
|
||||||
|
-output string
|
||||||
|
Output directory for generated test file (default: "./")
|
||||||
|
|
||||||
|
-package string
|
||||||
|
Package name for generated test (default: "onvif_test")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Generate test from Bosch camera capture
|
||||||
|
./generate-tests \
|
||||||
|
-capture camera-logs/Bosch_FLEXIDOME_indoor_5100i_IR_8.71.0066_xmlcapture_20251110-120000.tar.gz \
|
||||||
|
-output testdata/captures/
|
||||||
|
|
||||||
|
# Output:
|
||||||
|
# ✓ Generated test file: testdata/captures/bosch_flexidome_indoor_5100i_ir_8.71.0066_test.go
|
||||||
|
# Camera: Bosch FLEXIDOME indoor 5100i IR (Firmware: 8.71.0066)
|
||||||
|
# Captured operations: 18
|
||||||
|
```
|
||||||
|
|
||||||
|
## Generated Test Structure
|
||||||
|
|
||||||
|
The tool creates a complete test file with:
|
||||||
|
|
||||||
|
### Test Function
|
||||||
|
|
||||||
|
```go
|
||||||
|
func Test<CameraName>(t *testing.T)
|
||||||
|
```
|
||||||
|
|
||||||
|
Named based on camera manufacturer, model, and firmware.
|
||||||
|
|
||||||
|
### Subtests
|
||||||
|
|
||||||
|
- `GetDeviceInformation` - Validates device info parsing
|
||||||
|
- `GetSystemDateAndTime` - Tests date/time operation
|
||||||
|
- `GetCapabilities` - Verifies capability discovery
|
||||||
|
- `GetProfiles` - Tests media profile enumeration
|
||||||
|
|
||||||
|
### Assertions
|
||||||
|
|
||||||
|
Each subtest includes:
|
||||||
|
- Error checking
|
||||||
|
- Nil validation
|
||||||
|
- Basic field validation
|
||||||
|
- Informative logging
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
1. **Load Capture** - Reads all SOAP exchanges from tar.gz archive
|
||||||
|
2. **Extract Metadata** - Gets camera manufacturer, model, firmware from responses
|
||||||
|
3. **Generate Name** - Creates valid Go identifier from camera info
|
||||||
|
4. **Render Template** - Fills in test template with camera-specific data
|
||||||
|
5. **Write File** - Saves test to output directory
|
||||||
|
|
||||||
|
## Template
|
||||||
|
|
||||||
|
The generator uses an embedded Go template that creates:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package onvif_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/0x524a/onvif-go"
|
||||||
|
onviftesting "github.com/0x524a/onvif-go/testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test<CameraName>(t *testing.T) {
|
||||||
|
captureArchive := "<archive-file>.tar.gz"
|
||||||
|
|
||||||
|
mockServer, err := onviftesting.NewMockSOAPServer(captureArchive)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create mock server: %v", err)
|
||||||
|
}
|
||||||
|
defer mockServer.Close()
|
||||||
|
|
||||||
|
client, err := onvif.NewClient(
|
||||||
|
mockServer.URL()+"/onvif/device_service",
|
||||||
|
onvif.WithCredentials("testuser", "testpass"),
|
||||||
|
)
|
||||||
|
// ... test operations
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
### 1. Capture from Camera
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./onvif-diagnostics \
|
||||||
|
-endpoint "http://camera/onvif/device_service" \
|
||||||
|
-username "user" \
|
||||||
|
-password "pass" \
|
||||||
|
-capture-xml
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Generate Test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./generate-tests \
|
||||||
|
-capture camera-logs/Camera_*_xmlcapture_*.tar.gz \
|
||||||
|
-output testdata/captures/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Run Test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go test -v ./testdata/captures/ -run TestCamera
|
||||||
|
```
|
||||||
|
|
||||||
|
## Customization
|
||||||
|
|
||||||
|
After generation, you can customize the test:
|
||||||
|
|
||||||
|
### Add Camera-Specific Tests
|
||||||
|
|
||||||
|
```go
|
||||||
|
t.Run("CustomFeature", func(t *testing.T) {
|
||||||
|
// Add custom test for camera-specific features
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Add Detailed Assertions
|
||||||
|
|
||||||
|
```go
|
||||||
|
t.Run("GetDeviceInformation", func(t *testing.T) {
|
||||||
|
info, err := client.GetDeviceInformation(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("GetDeviceInformation failed: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add specific assertions
|
||||||
|
if info.Manufacturer != "ExpectedManufacturer" {
|
||||||
|
t.Errorf("Expected manufacturer X, got %s", info.Manufacturer)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go build -o generate-tests ./cmd/generate-tests/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- `github.com/0x524a/onvif-go/testing` - Mock server and capture loader
|
||||||
|
|
||||||
|
## Output File Naming
|
||||||
|
|
||||||
|
Generated test files are named:
|
||||||
|
|
||||||
|
```
|
||||||
|
<manufacturer>_<model>_<firmware>_test.go
|
||||||
|
```
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
- `bosch_flexidome_indoor_5100i_ir_8.71.0066_test.go`
|
||||||
|
- `axis_q3626-ve_12.6.104_test.go`
|
||||||
|
- `reolink_e1_zoom_v3.1.0.2649_test.go`
|
||||||
|
|
||||||
|
All special characters converted to underscores or removed.
|
||||||
|
|
||||||
|
## Archive Path Handling
|
||||||
|
|
||||||
|
The generator automatically handles archive paths:
|
||||||
|
|
||||||
|
- If archive is in output directory, uses filename only
|
||||||
|
- Otherwise uses relative path from output directory
|
||||||
|
- Tests can find archives when run with `go test ./testdata/captures/`
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "Failed to load capture"
|
||||||
|
|
||||||
|
Archive file not found or corrupted.
|
||||||
|
|
||||||
|
**Solution**: Verify archive path and ensure it's a valid tar.gz file.
|
||||||
|
|
||||||
|
### "Failed to extract device info"
|
||||||
|
|
||||||
|
Archive doesn't contain GetDeviceInformation response.
|
||||||
|
|
||||||
|
**Solution**: Re-capture from camera, ensuring diagnostic runs fully.
|
||||||
|
|
||||||
|
### Generated test won't compile
|
||||||
|
|
||||||
|
Usually due to invalid characters in camera names.
|
||||||
|
|
||||||
|
**Solution**: The generator should handle this, but you can manually edit the test function name.
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
Potential improvements:
|
||||||
|
|
||||||
|
- [ ] Detect camera-specific operations (PTZ, audio, etc.)
|
||||||
|
- [ ] Generate profile-specific tests
|
||||||
|
- [ ] Add benchmarking subtests
|
||||||
|
- [ ] Support custom test templates
|
||||||
|
- [ ] Batch generation from multiple captures
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- `testdata/captures/README.md` - Using generated tests
|
||||||
|
- `testing/mock_server.go` - Mock server implementation
|
||||||
|
- `cmd/onvif-diagnostics/` - Capturing tool
|
||||||
@@ -0,0 +1,263 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
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")
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
func Test{{.CameraName}}(t *testing.T) {
|
||||||
|
// Load capture archive (relative to project root)
|
||||||
|
captureArchive := "{{.CaptureArchiveRelPath}}"
|
||||||
|
|
||||||
|
mockServer, err := onviftesting.NewMockSOAPServer(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()
|
||||||
|
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetSystemDateAndTime", func(t *testing.T) {
|
||||||
|
_, err := client.GetSystemDateAndTime(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("GetSystemDateAndTime failed: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
{{range .AdditionalTests}}
|
||||||
|
t.Run("{{.Name}}", func(t *testing.T) {
|
||||||
|
{{.Code}}
|
||||||
|
})
|
||||||
|
{{end}}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
type TestData struct {
|
||||||
|
PackageName string
|
||||||
|
CameraName string
|
||||||
|
CameraDescription string
|
||||||
|
CaptureArchiveRelPath string
|
||||||
|
AdditionalTests []AdditionalTest
|
||||||
|
}
|
||||||
|
|
||||||
|
type AdditionalTest struct {
|
||||||
|
Name string
|
||||||
|
Code string
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
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")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load capture to get camera info
|
||||||
|
capture, err := onviftesting.LoadCaptureFromArchive(*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]
|
||||||
|
|
||||||
|
// Convert to valid Go identifier
|
||||||
|
cameraName := strings.ReplaceAll(cameraID, "-", "")
|
||||||
|
cameraName = strings.ReplaceAll(cameraName, ".", "")
|
||||||
|
cameraName = strings.ReplaceAll(cameraName, " ", "")
|
||||||
|
|
||||||
|
// Get device info from first exchange (GetDeviceInformation)
|
||||||
|
cameraDesc := cameraID
|
||||||
|
if len(capture.Exchanges) > 0 {
|
||||||
|
// Try to parse device info from response
|
||||||
|
for _, ex := range capture.Exchanges {
|
||||||
|
if strings.Contains(ex.RequestBody, "GetDeviceInformation") {
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
testData := TestData{
|
||||||
|
PackageName: *packageName,
|
||||||
|
CameraName: cameraName,
|
||||||
|
CameraDescription: cameraDesc,
|
||||||
|
CaptureArchiveRelPath: relArchivePath,
|
||||||
|
AdditionalTests: []AdditionalTest{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate test file
|
||||||
|
tmpl, err := template.New("test").Parse(testTemplate)
|
||||||
|
if err != nil {
|
||||||
|
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)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to create output file: %v", err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
if err := tmpl.Execute(f, testData); err != nil {
|
||||||
|
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.Println()
|
||||||
|
fmt.Println("Run tests with:")
|
||||||
|
fmt.Printf(" go test -v %s\n", outputFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
startIdx += len(start)
|
||||||
|
} else {
|
||||||
|
startIdx += len(start)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.TrimSpace(xmlStr[startIdx : startIdx+endIdx])
|
||||||
|
}
|
||||||
@@ -0,0 +1,365 @@
|
|||||||
|
# ONVIF Camera Diagnostic Utility
|
||||||
|
|
||||||
|
A comprehensive diagnostic tool for collecting detailed information from ONVIF cameras. This utility helps analyze camera capabilities, troubleshoot issues, and generate reports for creating camera-specific tests.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
✅ **Comprehensive Testing** - Tests all major ONVIF operations:
|
||||||
|
- Device information and capabilities
|
||||||
|
- Media profiles and streaming
|
||||||
|
- Video encoder configurations
|
||||||
|
- Imaging settings
|
||||||
|
- PTZ status and presets (if available)
|
||||||
|
- System date/time
|
||||||
|
|
||||||
|
✅ **Detailed Reporting** - Generates JSON reports with:
|
||||||
|
- All successful operations with response data
|
||||||
|
- Failed operations with error details
|
||||||
|
- Response times for performance analysis
|
||||||
|
- Structured data ready for test generation
|
||||||
|
|
||||||
|
✅ **Easy to Use** - Simple command-line interface with minimal requirements
|
||||||
|
|
||||||
|
✅ **XML Debugging** - For detailed debugging, see the companion `onvif-xml-capture` utility that captures raw SOAP XML
|
||||||
|
|
||||||
|
✅ **Helpful for**:
|
||||||
|
- Creating camera-specific integration tests
|
||||||
|
- Troubleshooting ONVIF compatibility issues
|
||||||
|
- Analyzing camera capabilities
|
||||||
|
- Debugging connection problems
|
||||||
|
- Documenting camera configurations
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Option 1: Build from source
|
||||||
|
```bash
|
||||||
|
cd /path/to/go-onvif
|
||||||
|
go build -o onvif-diagnostics ./cmd/onvif-diagnostics/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 2: Install globally
|
||||||
|
```bash
|
||||||
|
go install ./cmd/onvif-diagnostics
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Basic Usage
|
||||||
|
```bash
|
||||||
|
./onvif-diagnostics \
|
||||||
|
-endpoint "http://192.168.1.201/onvif/device_service" \
|
||||||
|
-username "service" \
|
||||||
|
-password "Service.1234"
|
||||||
|
```
|
||||||
|
|
||||||
|
### With XML Capture (for debugging)
|
||||||
|
```bash
|
||||||
|
./onvif-diagnostics \
|
||||||
|
-endpoint "http://192.168.1.201/onvif/device_service" \
|
||||||
|
-username "service" \
|
||||||
|
-password "Service.1234" \
|
||||||
|
-capture-xml \
|
||||||
|
-verbose
|
||||||
|
```
|
||||||
|
|
||||||
|
This creates two files:
|
||||||
|
- `Manufacturer_Model_Firmware_timestamp.json` - Diagnostic report
|
||||||
|
- `Manufacturer_Model_Firmware_xmlcapture_timestamp.tar.gz` - Raw SOAP XML archive
|
||||||
|
|
||||||
|
### Verbose Output
|
||||||
|
```bash
|
||||||
|
./onvif-diagnostics \
|
||||||
|
-endpoint "http://192.168.1.201/onvif/device_service" \
|
||||||
|
-username "service" \
|
||||||
|
-password "Service.1234" \
|
||||||
|
-verbose
|
||||||
|
```
|
||||||
|
|
||||||
|
### Capture Raw SOAP XML
|
||||||
|
```bash
|
||||||
|
./onvif-diagnostics \
|
||||||
|
-endpoint "http://192.168.1.201/onvif/device_service" \
|
||||||
|
-username "service" \
|
||||||
|
-password "Service.1234" \
|
||||||
|
-capture-xml
|
||||||
|
```
|
||||||
|
|
||||||
|
Enables XML traffic capture and creates a compressed tar.gz archive containing all SOAP request/response pairs. Useful for debugging XML parsing issues or analyzing camera behavior.
|
||||||
|
|
||||||
|
The archive contains:
|
||||||
|
- `capture_001_GetDeviceInformation.json` - Request/response metadata with operation name
|
||||||
|
- `capture_001_GetDeviceInformation_request.xml` - Formatted SOAP request
|
||||||
|
- `capture_001_GetDeviceInformation_response.xml` - Formatted SOAP response
|
||||||
|
- `capture_002_GetSystemDateAndTime.json` - Next operation metadata
|
||||||
|
- ... (one set per SOAP operation, named by operation type)
|
||||||
|
|
||||||
|
Each file is named with the SOAP operation (e.g., GetDeviceInformation, GetProfiles) for easy identification.
|
||||||
|
|
||||||
|
Extract the archive:
|
||||||
|
```bash
|
||||||
|
tar -xzf camera-logs/Camera_Model_xmlcapture_timestamp.tar.gz
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Output Directory
|
||||||
|
```bash
|
||||||
|
./onvif-diagnostics \
|
||||||
|
-endpoint "http://192.168.1.201/onvif/device_service" \
|
||||||
|
-username "service" \
|
||||||
|
-password "Service.1234" \
|
||||||
|
-output ./my-camera-reports
|
||||||
|
```
|
||||||
|
|
||||||
|
### All Options
|
||||||
|
```
|
||||||
|
Usage of ./onvif-diagnostics:
|
||||||
|
-endpoint string
|
||||||
|
ONVIF device endpoint (e.g., http://192.168.1.201/onvif/device_service)
|
||||||
|
-username string
|
||||||
|
ONVIF username
|
||||||
|
-password string
|
||||||
|
ONVIF password
|
||||||
|
-output string
|
||||||
|
Output directory for logs (default "./camera-logs")
|
||||||
|
-timeout int
|
||||||
|
Request timeout in seconds (default 30)
|
||||||
|
-verbose
|
||||||
|
Verbose output
|
||||||
|
-include-raw
|
||||||
|
Include raw SOAP responses (increases file size)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example Output
|
||||||
|
|
||||||
|
```
|
||||||
|
ONVIF Camera Diagnostic Utility v1.0.0
|
||||||
|
========================================
|
||||||
|
|
||||||
|
Starting diagnostic collection...
|
||||||
|
|
||||||
|
→ 1. Getting device information...
|
||||||
|
✓ Manufacturer: Bosch, Model: FLEXIDOME indoor 5100i IR
|
||||||
|
→ 2. Getting system date and time...
|
||||||
|
✓ Retrieved
|
||||||
|
→ 3. Getting capabilities...
|
||||||
|
✓ Services: Device, Media, Imaging, Events, Analytics
|
||||||
|
→ 4. Discovering service endpoints...
|
||||||
|
✓ Service endpoints discovered
|
||||||
|
→ 5. Getting media profiles...
|
||||||
|
✓ Found 4 profile(s)
|
||||||
|
→ 6. Getting stream URIs for all profiles...
|
||||||
|
✓ Retrieved 4/4 stream URIs
|
||||||
|
→ 7. Getting snapshot URIs for all profiles...
|
||||||
|
✓ Retrieved 4/4 snapshot URIs
|
||||||
|
→ 8. Getting video encoder configurations...
|
||||||
|
✓ Retrieved 4/4 video encoder configs
|
||||||
|
→ 9. Getting imaging settings...
|
||||||
|
✓ Retrieved 1/1 imaging settings
|
||||||
|
→ 10. Getting PTZ status...
|
||||||
|
ℹ No PTZ configurations found
|
||||||
|
→ 11. Getting PTZ presets...
|
||||||
|
ℹ No PTZ configurations found
|
||||||
|
→ Saving diagnostic report...
|
||||||
|
|
||||||
|
========================================
|
||||||
|
✓ Diagnostic collection complete!
|
||||||
|
Report saved to: camera-logs/Bosch_FLEXIDOME_indoor_5100i_IR_8.71.0066_20251107-193656.json
|
||||||
|
Total errors: 0
|
||||||
|
|
||||||
|
Device: Bosch FLEXIDOME indoor 5100i IR
|
||||||
|
Firmware: 8.71.0066
|
||||||
|
Profiles: 4
|
||||||
|
|
||||||
|
Please share this file for analysis and test creation.
|
||||||
|
========================================
|
||||||
|
```
|
||||||
|
|
||||||
|
## Report Structure
|
||||||
|
|
||||||
|
The generated JSON report includes:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"timestamp": "2025-11-07T19:36:56Z",
|
||||||
|
"utility_version": "1.0.0",
|
||||||
|
"connection_info": {
|
||||||
|
"endpoint": "http://192.168.1.201/onvif/device_service",
|
||||||
|
"username": "service",
|
||||||
|
"test_date": "2025-11-07"
|
||||||
|
},
|
||||||
|
"device_info": {
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"manufacturer": "Bosch",
|
||||||
|
"model": "FLEXIDOME indoor 5100i IR",
|
||||||
|
"firmware_version": "8.71.0066",
|
||||||
|
"serial_number": "404754734001050102",
|
||||||
|
"hardware_id": "F000B543"
|
||||||
|
},
|
||||||
|
"response_time": "21.5ms"
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"success": true,
|
||||||
|
"count": 4,
|
||||||
|
"data": [ /* profile details */ ]
|
||||||
|
},
|
||||||
|
"stream_uris": [ /* stream URI results for each profile */ ],
|
||||||
|
"errors": [ /* any errors encountered */ ]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Use Cases
|
||||||
|
|
||||||
|
### 1. Creating Camera-Specific Tests
|
||||||
|
Run the diagnostic on your camera and share the JSON file. The report contains all the information needed to create comprehensive integration tests.
|
||||||
|
|
||||||
|
### 2. Troubleshooting Connection Issues
|
||||||
|
If your camera isn't working, run diagnostics to see exactly which operations fail and what error messages are returned.
|
||||||
|
|
||||||
|
### 3. Comparing Cameras
|
||||||
|
Run diagnostics on multiple cameras to compare capabilities, response times, and compatibility.
|
||||||
|
|
||||||
|
### 4. Documentation
|
||||||
|
Generate detailed reports of camera configurations for documentation purposes.
|
||||||
|
|
||||||
|
## Interpreting Results
|
||||||
|
|
||||||
|
### Success Indicators
|
||||||
|
- ✓ Green checkmarks indicate successful operations
|
||||||
|
- Response times help identify performance issues
|
||||||
|
- High success rates indicate good compatibility
|
||||||
|
|
||||||
|
### Error Indicators
|
||||||
|
- ✗ Red X marks indicate failed operations
|
||||||
|
- ℹ Info symbols indicate optional features not available
|
||||||
|
- Check the `errors` array in JSON for detailed error messages
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
**All operations fail:**
|
||||||
|
- Check network connectivity
|
||||||
|
- Verify endpoint URL is correct
|
||||||
|
- Ensure camera is powered on
|
||||||
|
|
||||||
|
**Authentication errors:**
|
||||||
|
- Verify username and password
|
||||||
|
- Check user permissions on camera
|
||||||
|
|
||||||
|
**Some profiles fail:**
|
||||||
|
- Camera may have different capabilities per profile
|
||||||
|
- Some operations may not be supported by all profiles
|
||||||
|
|
||||||
|
**Timeout errors:**
|
||||||
|
- Increase timeout with `-timeout 60`
|
||||||
|
- Check network latency
|
||||||
|
- Verify camera is responding
|
||||||
|
|
||||||
|
## Sharing Reports
|
||||||
|
|
||||||
|
When sharing diagnostic reports:
|
||||||
|
|
||||||
|
1. **Anonymize if needed** - The report includes:
|
||||||
|
- IP addresses (in endpoint)
|
||||||
|
- Usernames (not passwords)
|
||||||
|
- Serial numbers
|
||||||
|
|
||||||
|
2. **What to share**:
|
||||||
|
- The complete JSON file
|
||||||
|
- Any console output showing errors
|
||||||
|
- Camera model and firmware version
|
||||||
|
|
||||||
|
3. **Where to share**:
|
||||||
|
- GitHub Issues
|
||||||
|
- Email for analysis
|
||||||
|
- Pull request descriptions
|
||||||
|
|
||||||
|
## Advanced Usage
|
||||||
|
|
||||||
|
### Batch Testing Multiple Cameras
|
||||||
|
Create a script to test multiple cameras:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
cameras=(
|
||||||
|
"192.168.1.201:service:password1"
|
||||||
|
"192.168.1.202:admin:password2"
|
||||||
|
"192.168.1.203:user:password3"
|
||||||
|
)
|
||||||
|
|
||||||
|
for camera in "${cameras[@]}"; do
|
||||||
|
IFS=':' read -r ip user pass <<< "$camera"
|
||||||
|
echo "Testing camera at $ip..."
|
||||||
|
./onvif-diagnostics \
|
||||||
|
-endpoint "http://$ip/onvif/device_service" \
|
||||||
|
-username "$user" \
|
||||||
|
-password "$pass"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
### Automated Testing
|
||||||
|
Include in CI/CD pipelines:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- name: Run ONVIF Diagnostics
|
||||||
|
run: |
|
||||||
|
./onvif-diagnostics \
|
||||||
|
-endpoint "${{ secrets.CAMERA_ENDPOINT }}" \
|
||||||
|
-username "${{ secrets.CAMERA_USERNAME }}" \
|
||||||
|
-password "${{ secrets.CAMERA_PASSWORD }}" \
|
||||||
|
-output ./reports
|
||||||
|
|
||||||
|
- name: Upload Diagnostic Reports
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: camera-diagnostics
|
||||||
|
path: ./reports/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Adding New Tests
|
||||||
|
|
||||||
|
To add new diagnostic tests, edit `cmd/onvif-diagnostics/main.go`:
|
||||||
|
|
||||||
|
1. Create a new test function following the pattern:
|
||||||
|
```go
|
||||||
|
func testNewOperation(ctx context.Context, client *onvif.Client, report *CameraReport) *NewOperationResult {
|
||||||
|
// Implementation
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Add result struct to store data
|
||||||
|
3. Call the test in main()
|
||||||
|
4. Update report structure
|
||||||
|
|
||||||
|
### Building for Different Platforms
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Linux
|
||||||
|
GOOS=linux GOARCH=amd64 go build -o onvif-diagnostics-linux ./cmd/onvif-diagnostics/
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
GOOS=windows GOARCH=amd64 go build -o onvif-diagnostics.exe ./cmd/onvif-diagnostics/
|
||||||
|
|
||||||
|
# macOS ARM
|
||||||
|
GOOS=darwin GOARCH=arm64 go build -o onvif-diagnostics-mac-arm ./cmd/onvif-diagnostics/
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Same as parent project.
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues or questions:
|
||||||
|
1. Run diagnostics with `-verbose` flag
|
||||||
|
2. Share the generated JSON report
|
||||||
|
3. **For XML parsing issues**: Use `onvif-xml-capture` utility to capture raw SOAP XML
|
||||||
|
4. Open a GitHub issue with the report attached
|
||||||
|
|
||||||
|
## Related Tools
|
||||||
|
|
||||||
|
- **onvif-xml-capture** - Captures raw SOAP XML requests/responses for detailed debugging
|
||||||
|
- Location: `cmd/onvif-xml-capture/`
|
||||||
|
- Use when: Diagnostic report shows errors and you need to see raw XML
|
||||||
|
- See: `XML_DEBUGGING_SOLUTION.md` for complete guide
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user