fix: update .gitignore to preserve cmd/ source directories and add missing CLI tools
This commit is contained in:
@@ -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])
|
||||
}
|
||||
Reference in New Issue
Block a user