Merge pull request #51 from 0x524a/update-codecov-copy-yml
Update codecov copy yml
This commit is contained in:
@@ -1,34 +0,0 @@
|
|||||||
{
|
|
||||||
"permissions": {
|
|
||||||
"allow": [
|
|
||||||
"Bash(wc:*)",
|
|
||||||
"Bash(grep:*)",
|
|
||||||
"Bash(ls:*)",
|
|
||||||
"Bash(find:*)",
|
|
||||||
"Bash(go build:*)",
|
|
||||||
"Bash(go test:*)",
|
|
||||||
"Bash(GOPROXY=direct go build:*)",
|
|
||||||
"Bash(GOPROXY=https://proxy.golang.org,direct go mod download:*)",
|
|
||||||
"Bash(go mod download:*)",
|
|
||||||
"Bash(./bin/onvif-cli discover:*)",
|
|
||||||
"Bash(./bin/onvif-cli:*)",
|
|
||||||
"Bash(./bin/onvif-diagnostics:*)",
|
|
||||||
"Bash(./bin/discover:*)",
|
|
||||||
"Bash(tee:*)",
|
|
||||||
"Bash(nc:*)",
|
|
||||||
"Bash(tree:*)",
|
|
||||||
"Bash(du -sh:*)",
|
|
||||||
"Bash(xargs:*)",
|
|
||||||
"Bash(gofmt:*)",
|
|
||||||
"Bash(make lint:*)",
|
|
||||||
"Bash(go install:*)",
|
|
||||||
"Bash(go vet:*)",
|
|
||||||
"Bash(~/go/bin/govulncheck ./...)",
|
|
||||||
"Bash(go version:*)",
|
|
||||||
"Bash(~/go/bin/staticcheck:*)",
|
|
||||||
"Bash(make build:*)",
|
|
||||||
"Bash(make clean:*)",
|
|
||||||
"Bash(git check-ignore:*)"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -11,14 +11,13 @@ import (
|
|||||||
"github.com/0x524a/onvif-go/discovery"
|
"github.com/0x524a/onvif-go/discovery"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const defaultDiscoveryTimeout = 10 * time.Second
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
iface := flag.String("interface", "", "Network interface to use (e.g., en0, en11)")
|
iface := flag.String("interface", "", "Network interface to use (e.g., en0, en11)")
|
||||||
timeout := flag.Duration("timeout", 10*time.Second, "Discovery timeout")
|
timeout := flag.Duration("timeout", defaultDiscoveryTimeout, "Discovery timeout")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
opts := &discovery.DiscoverOptions{
|
opts := &discovery.DiscoverOptions{
|
||||||
NetworkInterface: *iface,
|
NetworkInterface: *iface,
|
||||||
}
|
}
|
||||||
@@ -29,10 +28,13 @@ func main() {
|
|||||||
}
|
}
|
||||||
fmt.Println("...")
|
fmt.Println("...")
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
devices, err := discovery.DiscoverWithOptions(ctx, *timeout, opts)
|
devices, err := discovery.DiscoverWithOptions(ctx, *timeout, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Discovery error: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Discovery error: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1) //nolint:gocritic // defer cancel() is still executed by runtime on exit
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(devices) == 0 {
|
if len(devices) == 0 {
|
||||||
|
|||||||
+35
-17
@@ -14,6 +14,11 @@ import (
|
|||||||
onviftesting "github.com/0x524a/onvif-go/testing"
|
onviftesting "github.com/0x524a/onvif-go/testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
maxTokenLength = 20
|
||||||
|
percentScale = 100
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
captureArchive = flag.String("capture", "", "Path to XML capture archive (.tar.gz)")
|
captureArchive = flag.String("capture", "", "Path to XML capture archive (.tar.gz)")
|
||||||
outputDir = flag.String("output", "./", "Output directory for generated test file")
|
outputDir = flag.String("output", "./", "Output directory for generated test file")
|
||||||
@@ -128,7 +133,7 @@ type GeneratedTest struct {
|
|||||||
Code string
|
Code string
|
||||||
}
|
}
|
||||||
|
|
||||||
// operationInfo holds info about captured operations
|
// operationInfo holds info about captured operations.
|
||||||
type operationInfo struct {
|
type operationInfo struct {
|
||||||
OperationName string
|
OperationName string
|
||||||
ServiceType onviftesting.ServiceType
|
ServiceType onviftesting.ServiceType
|
||||||
@@ -148,6 +153,7 @@ func main() {
|
|||||||
// Handle coverage report mode
|
// Handle coverage report mode
|
||||||
if *coverageReport {
|
if *coverageReport {
|
||||||
generateCoverageReport(regPath)
|
generateCoverageReport(regPath)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,7 +205,8 @@ func generateTests() string {
|
|||||||
metadata.CameraInfo.FirmwareVersion)
|
metadata.CameraInfo.FirmwareVersion)
|
||||||
} else {
|
} else {
|
||||||
// Try to extract from GetDeviceInformation response
|
// Try to extract from GetDeviceInformation response
|
||||||
for _, ex := range capture.Exchanges {
|
for i := range capture.Exchanges {
|
||||||
|
ex := &capture.Exchanges[i]
|
||||||
if ex.OperationName == "GetDeviceInformation" && ex.Success {
|
if ex.OperationName == "GetDeviceInformation" && ex.Success {
|
||||||
manufacturer := extractXMLValue(ex.ResponseBody, "Manufacturer")
|
manufacturer := extractXMLValue(ex.ResponseBody, "Manufacturer")
|
||||||
model := extractXMLValue(ex.ResponseBody, "Model")
|
model := extractXMLValue(ex.ResponseBody, "Model")
|
||||||
@@ -207,6 +214,7 @@ func generateTests() string {
|
|||||||
if manufacturer != "" && model != "" {
|
if manufacturer != "" && model != "" {
|
||||||
cameraDesc = fmt.Sprintf("%s %s (Firmware: %s)", manufacturer, model, firmware)
|
cameraDesc = fmt.Sprintf("%s %s (Firmware: %s)", manufacturer, model, firmware)
|
||||||
}
|
}
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -241,13 +249,12 @@ func generateTests() string {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to create output file: %v", err)
|
log.Fatalf("Failed to create output file: %v", err)
|
||||||
}
|
}
|
||||||
defer func() {
|
defer f.Close()
|
||||||
_ = f.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
if err := tmpl.Execute(f, testData); err != nil {
|
if err := tmpl.Execute(f, testData); err != nil {
|
||||||
_ = f.Close()
|
log.Printf("Failed to execute template: %v", err)
|
||||||
log.Fatalf("Failed to execute template: %v", err)
|
|
||||||
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("✓ Generated test file: %s\n", outputFile)
|
fmt.Printf("✓ Generated test file: %s\n", outputFile)
|
||||||
@@ -264,10 +271,11 @@ func generateTests() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func analyzeOperations(capture *onviftesting.CameraCaptureV2) []operationInfo {
|
func analyzeOperations(capture *onviftesting.CameraCaptureV2) []operationInfo {
|
||||||
var ops []operationInfo
|
ops := make([]operationInfo, 0, len(capture.Exchanges))
|
||||||
seen := make(map[string]bool)
|
seen := make(map[string]bool)
|
||||||
|
|
||||||
for _, ex := range capture.Exchanges {
|
for i := range capture.Exchanges {
|
||||||
|
ex := &capture.Exchanges[i]
|
||||||
// Create unique key for deduplication
|
// Create unique key for deduplication
|
||||||
key := ex.OperationName
|
key := ex.OperationName
|
||||||
if token := ex.GetProfileToken(); token != "" {
|
if token := ex.GetProfileToken(); token != "" {
|
||||||
@@ -297,10 +305,12 @@ func analyzeOperations(capture *onviftesting.CameraCaptureV2) []operationInfo {
|
|||||||
func hasNonDeviceOperations(ops []operationInfo) bool {
|
func hasNonDeviceOperations(ops []operationInfo) bool {
|
||||||
for _, op := range ops {
|
for _, op := range ops {
|
||||||
switch op.ServiceType {
|
switch op.ServiceType {
|
||||||
case onviftesting.ServiceMedia, onviftesting.ServicePTZ, onviftesting.ServiceImaging:
|
case onviftesting.ServiceMedia, onviftesting.ServicePTZ, onviftesting.ServiceImaging, onviftesting.ServiceEvent, onviftesting.ServiceDeviceIO:
|
||||||
return true
|
return true
|
||||||
|
case onviftesting.ServiceDevice, onviftesting.ServiceUnknown:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -574,6 +584,7 @@ func generatePTZTests(ops []operationInfo) []GeneratedTest {
|
|||||||
Code: code,
|
Code: code,
|
||||||
})
|
})
|
||||||
delete(ptzOps, op.OperationName)
|
delete(ptzOps, op.OperationName)
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if token, ok := op.Parameters["ProfileToken"].(string); ok && token != "" {
|
if token, ok := op.Parameters["ProfileToken"].(string); ok && token != "" {
|
||||||
@@ -699,9 +710,10 @@ func sanitizeToken(token string) string {
|
|||||||
token = strings.ReplaceAll(token, ".", "_")
|
token = strings.ReplaceAll(token, ".", "_")
|
||||||
token = strings.ReplaceAll(token, " ", "_")
|
token = strings.ReplaceAll(token, " ", "_")
|
||||||
// Truncate if too long
|
// Truncate if too long
|
||||||
if len(token) > 20 {
|
if len(token) > maxTokenLength {
|
||||||
token = token[:20]
|
token = token[:maxTokenLength]
|
||||||
}
|
}
|
||||||
|
|
||||||
return token
|
return token
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -713,6 +725,7 @@ func makeRelativePath(archivePath, outputDir string) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return archivePath
|
return archivePath
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -749,12 +762,14 @@ func updateCameraRegistry(regPath, archivePath, testFile string) {
|
|||||||
registry, err := onviftesting.LoadRegistry(regPath)
|
registry, err := onviftesting.LoadRegistry(regPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Warning: Failed to load registry: %v", err)
|
log.Printf("Warning: Failed to load registry: %v", err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
entry, err := onviftesting.CreateCameraEntryFromCapture(archivePath)
|
entry, err := onviftesting.CreateCameraEntryFromCapture(archivePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Warning: Failed to create registry entry: %v", err)
|
log.Printf("Warning: Failed to create registry entry: %v", err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -774,7 +789,7 @@ func updateCameraRegistry(regPath, archivePath, testFile string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add or update the camera entry
|
// Add or update the camera entry
|
||||||
registry.AddCamera(*entry)
|
registry.AddCamera(entry)
|
||||||
|
|
||||||
// Update coverage statistics
|
// Update coverage statistics
|
||||||
updateRegistryCoverage(registry, archivePath)
|
updateRegistryCoverage(registry, archivePath)
|
||||||
@@ -782,6 +797,7 @@ func updateCameraRegistry(regPath, archivePath, testFile string) {
|
|||||||
// Save registry
|
// Save registry
|
||||||
if err := onviftesting.SaveRegistry(registry, regPath); err != nil {
|
if err := onviftesting.SaveRegistry(registry, regPath); err != nil {
|
||||||
log.Printf("Warning: Failed to save registry: %v", err)
|
log.Printf("Warning: Failed to save registry: %v", err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -799,7 +815,8 @@ func updateRegistryCoverage(registry *onviftesting.Registry, archivePath string)
|
|||||||
|
|
||||||
// Count unique operations per service
|
// Count unique operations per service
|
||||||
serviceCounts := make(map[string]map[string]bool)
|
serviceCounts := make(map[string]map[string]bool)
|
||||||
for _, ex := range capture.Exchanges {
|
for i := range capture.Exchanges {
|
||||||
|
ex := &capture.Exchanges[i]
|
||||||
service := string(ex.ServiceType)
|
service := string(ex.ServiceType)
|
||||||
if service == "" || service == "Unknown" {
|
if service == "" || service == "Unknown" {
|
||||||
continue
|
continue
|
||||||
@@ -874,7 +891,7 @@ func generateCoverageMarkdown(registry *onviftesting.Registry) string {
|
|||||||
total, captured := registry.GetTotalCoverage()
|
total, captured := registry.GetTotalCoverage()
|
||||||
if total > 0 {
|
if total > 0 {
|
||||||
sb.WriteString(fmt.Sprintf("- **Overall Coverage**: %.1f%% (%d/%d operations)\n\n",
|
sb.WriteString(fmt.Sprintf("- **Overall Coverage**: %.1f%% (%d/%d operations)\n\n",
|
||||||
float64(captured)/float64(total)*100, captured, total))
|
float64(captured)/float64(total)*percentScale, captured, total))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cameras
|
// Cameras
|
||||||
@@ -883,7 +900,8 @@ func generateCoverageMarkdown(registry *onviftesting.Registry) string {
|
|||||||
sb.WriteString("| Manufacturer | Model | Firmware | Operations | Capabilities |\n")
|
sb.WriteString("| Manufacturer | Model | Firmware | Operations | Capabilities |\n")
|
||||||
sb.WriteString("|--------------|-------|----------|------------|---------------|\n")
|
sb.WriteString("|--------------|-------|----------|------------|---------------|\n")
|
||||||
|
|
||||||
for _, cam := range registry.Cameras {
|
for i := range registry.Cameras {
|
||||||
|
cam := ®istry.Cameras[i]
|
||||||
caps := strings.Join(cam.Capabilities, ", ")
|
caps := strings.Join(cam.Capabilities, ", ")
|
||||||
sb.WriteString(fmt.Sprintf("| %s | %s | %s | %d | %s |\n",
|
sb.WriteString(fmt.Sprintf("| %s | %s | %s | %d | %s |\n",
|
||||||
cam.Manufacturer, cam.Model, cam.Firmware, cam.OperationsCaptured, caps))
|
cam.Manufacturer, cam.Model, cam.Firmware, cam.OperationsCaptured, caps))
|
||||||
@@ -902,7 +920,7 @@ func generateCoverageMarkdown(registry *onviftesting.Registry) string {
|
|||||||
if cov, ok := registry.Coverage[service]; ok {
|
if cov, ok := registry.Coverage[service]; ok {
|
||||||
pct := 0.0
|
pct := 0.0
|
||||||
if cov.Total > 0 {
|
if cov.Total > 0 {
|
||||||
pct = float64(cov.Captured) / float64(cov.Total) * 100
|
pct = float64(cov.Captured) / float64(cov.Total) * percentScale
|
||||||
}
|
}
|
||||||
sb.WriteString(fmt.Sprintf("| %s | %d | %d | %.1f%% |\n",
|
sb.WriteString(fmt.Sprintf("| %s | %d | %d | %.1f%% |\n",
|
||||||
service, cov.Total, cov.Captured, pct))
|
service, cov.Total, cov.Captured, pct))
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ const (
|
|||||||
retryDelaySec = 5
|
retryDelaySec = 5
|
||||||
maxIdleTimeoutSec = 90
|
maxIdleTimeoutSec = 90
|
||||||
unknownStatus = "Unknown"
|
unknownStatus = "Unknown"
|
||||||
|
percentScale = 100
|
||||||
)
|
)
|
||||||
|
|
||||||
type CameraReport struct {
|
type CameraReport struct {
|
||||||
@@ -997,12 +998,24 @@ func runComprehensiveCapture(ctx context.Context, client *onvif.Client, report *
|
|||||||
name string
|
name string
|
||||||
fn func() error
|
fn func() error
|
||||||
}{
|
}{
|
||||||
{"GetHostname", func() error { _, err := client.GetHostname(ctx); return err }},
|
{"GetHostname", func() error { _, err := client.GetHostname(ctx); return fmt.Errorf("GetHostname: %w", err) }}, //nolint:nlreturn
|
||||||
{"GetDNS", func() error { _, err := client.GetDNS(ctx); return err }},
|
{"GetDNS", func() error { _, err := client.GetDNS(ctx); return fmt.Errorf("GetDNS: %w", err) }}, //nolint:nlreturn
|
||||||
{"GetNTP", func() error { _, err := client.GetNTP(ctx); return err }},
|
{"GetNTP", func() error { _, err := client.GetNTP(ctx); return fmt.Errorf("GetNTP: %w", err) }}, //nolint:nlreturn
|
||||||
{"GetNetworkInterfaces", func() error { _, err := client.GetNetworkInterfaces(ctx); return err }},
|
{"GetNetworkInterfaces", func() error {
|
||||||
{"GetNetworkProtocols", func() error { _, err := client.GetNetworkProtocols(ctx); return err }},
|
_, err := client.GetNetworkInterfaces(ctx)
|
||||||
{"GetNetworkDefaultGateway", func() error { _, err := client.GetNetworkDefaultGateway(ctx); return err }},
|
|
||||||
|
return fmt.Errorf("GetNetworkInterfaces: %w", err)
|
||||||
|
}},
|
||||||
|
{"GetNetworkProtocols", func() error {
|
||||||
|
_, err := client.GetNetworkProtocols(ctx)
|
||||||
|
|
||||||
|
return fmt.Errorf("GetNetworkProtocols: %w", err)
|
||||||
|
}},
|
||||||
|
{"GetNetworkDefaultGateway", func() error {
|
||||||
|
_, err := client.GetNetworkDefaultGateway(ctx)
|
||||||
|
|
||||||
|
return fmt.Errorf("GetNetworkDefaultGateway: %w", err)
|
||||||
|
}},
|
||||||
{"GetScopes", func() error { _, err := client.GetScopes(ctx); return err }},
|
{"GetScopes", func() error { _, err := client.GetScopes(ctx); return err }},
|
||||||
{"GetUsers", func() error { _, err := client.GetUsers(ctx); return err }},
|
{"GetUsers", func() error { _, err := client.GetUsers(ctx); return err }},
|
||||||
{"GetDiscoveryMode", func() error { _, err := client.GetDiscoveryMode(ctx); return err }},
|
{"GetDiscoveryMode", func() error { _, err := client.GetDiscoveryMode(ctx); return err }},
|
||||||
@@ -1332,7 +1345,7 @@ func runComprehensiveCapture(ctx context.Context, client *onvif.Client, report *
|
|||||||
fmt.Printf(" Total operations: %d\n", totalOps)
|
fmt.Printf(" Total operations: %d\n", totalOps)
|
||||||
fmt.Printf(" Successful: %d\n", successCount)
|
fmt.Printf(" Successful: %d\n", successCount)
|
||||||
fmt.Printf(" Failed: %d\n", failCount)
|
fmt.Printf(" Failed: %d\n", failCount)
|
||||||
fmt.Printf(" Success rate: %.1f%%\n", float64(successCount)/float64(totalOps)*100)
|
fmt.Printf(" Success rate: %.1f%%\n", float64(successCount)/float64(totalOps)*percentScale)
|
||||||
fmt.Println("========================================")
|
fmt.Println("========================================")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1633,6 +1646,73 @@ func extractSOAPOperation(soapBody string) string {
|
|||||||
return "Unknown"
|
return "Unknown"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// compareFileOrder determines sort order for tar archive entries.
|
||||||
|
// Returns true if file i should come before file j.
|
||||||
|
func compareFileOrder(i, j int, files []string) 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeTarEntry writes a single file to the tar archive.
|
||||||
|
func writeTarEntry(tarWriter *tar.Writer, sourceDir, path string) error {
|
||||||
|
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, "")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create tar header: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set name to relative path
|
||||||
|
relPath, err := filepath.Rel(sourceDir, path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get relative path: %w", err)
|
||||||
|
}
|
||||||
|
header.Name = relPath
|
||||||
|
|
||||||
|
// Write header
|
||||||
|
if err := tarWriter.WriteHeader(header); err != nil {
|
||||||
|
return fmt.Errorf("failed to write tar header: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write file content
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open file: %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
|
||||||
|
}
|
||||||
|
|
||||||
// createTarGzV2 creates a V2 tar.gz archive with metadata.json first.
|
// createTarGzV2 creates a V2 tar.gz archive with metadata.json first.
|
||||||
func createTarGzV2(sourceDir, archivePath string) error {
|
func createTarGzV2(sourceDir, archivePath string) error {
|
||||||
// Create archive file
|
// Create archive file
|
||||||
@@ -1673,67 +1753,14 @@ func createTarGzV2(sourceDir, archivePath string) error {
|
|||||||
|
|
||||||
// Sort files: metadata.json first, then capture JSON files in order, then XML files
|
// Sort files: metadata.json first, then capture JSON files in order, then XML files
|
||||||
sort.Slice(files, func(i, j int) bool {
|
sort.Slice(files, func(i, j int) bool {
|
||||||
nameI := filepath.Base(files[i])
|
return compareFileOrder(i, j, files)
|
||||||
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
|
// Write files in sorted order
|
||||||
for _, path := range files {
|
for _, path := range files {
|
||||||
info, err := os.Stat(path)
|
if err := writeTarEntry(tarWriter, sourceDir, path); err != nil {
|
||||||
if err != nil {
|
return err
|
||||||
return fmt.Errorf("failed to stat file: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create tar header
|
|
||||||
header, err := tar.FileInfoHeader(info, "")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create tar header: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set name to relative path
|
|
||||||
relPath, err := filepath.Rel(sourceDir, path)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get relative path: %w", err)
|
|
||||||
}
|
|
||||||
header.Name = relPath
|
|
||||||
|
|
||||||
// Write header
|
|
||||||
if err := tarWriter.WriteHeader(header); err != nil {
|
|
||||||
return fmt.Errorf("failed to write tar header: %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)
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
return nil
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ func buildIPAddressFilterRequest(filter *IPAddressFilter) ipAddressFilterRequest
|
|||||||
// newSOAPClient creates a SOAP client with the current client credentials.
|
// newSOAPClient creates a SOAP client with the current client credentials.
|
||||||
func (c *Client) newSOAPClient() *soap.Client {
|
func (c *Client) newSOAPClient() *soap.Client {
|
||||||
username, password := c.GetCredentials()
|
username, password := c.GetCredentials()
|
||||||
|
|
||||||
return soap.NewClient(c.httpClient, username, password)
|
return soap.NewClient(c.httpClient, username, password)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ require github.com/0x524A/rtspeek v0.0.1
|
|||||||
require (
|
require (
|
||||||
github.com/bluenviron/gortsplib/v4 v4.16.2 // indirect
|
github.com/bluenviron/gortsplib/v4 v4.16.2 // indirect
|
||||||
github.com/bluenviron/mediacommon/v2 v2.4.1 // indirect
|
github.com/bluenviron/mediacommon/v2 v2.4.1 // indirect
|
||||||
|
github.com/fzipp/gocyclo v0.6.0 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ github.com/bluenviron/mediacommon/v2 v2.4.1/go.mod h1:a6MbPmXtYda9mKibKVMZlW20GY
|
|||||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo=
|
||||||
|
github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA=
|
||||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ func convertToPTZVectorXML(v *PTZVector) *ptzVectorXML {
|
|||||||
if v.Zoom != nil {
|
if v.Zoom != nil {
|
||||||
result.Zoom = &ptzZoomXML{X: v.Zoom.X, Space: v.Zoom.Space}
|
result.Zoom = &ptzZoomXML{X: v.Zoom.X, Space: v.Zoom.Space}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,6 +64,7 @@ func convertToPTZSpeedXML(s *PTZSpeed) *ptzSpeedXML {
|
|||||||
if s.Zoom != nil {
|
if s.Zoom != nil {
|
||||||
result.Zoom = &ptzZoomXML{X: s.Zoom.X, Space: s.Zoom.Space}
|
result.Zoom = &ptzZoomXML{X: s.Zoom.X, Space: s.Zoom.Space}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+18
-22
@@ -273,9 +273,19 @@ func BuildMatchKeyFromExchange(exchange *CapturedExchangeV2) MatchKey {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// addTokenScore adds tokenScoreBonus points to score if token matches between two MatchKeys.
|
||||||
|
const tokenScoreBonus = 10
|
||||||
|
|
||||||
|
func addTokenScore(score int, token1, token2 string) int {
|
||||||
|
if token1 != "" && token1 == token2 {
|
||||||
|
return score + tokenScoreBonus
|
||||||
|
}
|
||||||
|
return score
|
||||||
|
}
|
||||||
|
|
||||||
// MatchScore returns how well two MatchKeys match (higher is better).
|
// MatchScore returns how well two MatchKeys match (higher is better).
|
||||||
// Returns -1 if operation names don't match.
|
// Returns -1 if operation names don't match.
|
||||||
func (k MatchKey) MatchScore(other MatchKey) int {
|
func (k *MatchKey) MatchScore(other *MatchKey) int {
|
||||||
if k.OperationName != other.OperationName {
|
if k.OperationName != other.OperationName {
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
@@ -283,27 +293,13 @@ func (k MatchKey) MatchScore(other MatchKey) int {
|
|||||||
score := 1 // Base score for matching operation
|
score := 1 // Base score for matching operation
|
||||||
|
|
||||||
// Bonus points for matching parameters
|
// Bonus points for matching parameters
|
||||||
if k.ProfileToken != "" && k.ProfileToken == other.ProfileToken {
|
score = addTokenScore(score, k.ProfileToken, other.ProfileToken)
|
||||||
score += 10
|
score = addTokenScore(score, k.ConfigurationToken, other.ConfigurationToken)
|
||||||
}
|
score = addTokenScore(score, k.VideoSourceToken, other.VideoSourceToken)
|
||||||
if k.ConfigurationToken != "" && k.ConfigurationToken == other.ConfigurationToken {
|
score = addTokenScore(score, k.AudioSourceToken, other.AudioSourceToken)
|
||||||
score += 10
|
score = addTokenScore(score, k.PresetToken, other.PresetToken)
|
||||||
}
|
score = addTokenScore(score, k.NodeToken, other.NodeToken)
|
||||||
if k.VideoSourceToken != "" && k.VideoSourceToken == other.VideoSourceToken {
|
score = addTokenScore(score, k.OSDToken, other.OSDToken)
|
||||||
score += 10
|
|
||||||
}
|
|
||||||
if k.AudioSourceToken != "" && k.AudioSourceToken == other.AudioSourceToken {
|
|
||||||
score += 10
|
|
||||||
}
|
|
||||||
if k.PresetToken != "" && k.PresetToken == other.PresetToken {
|
|
||||||
score += 10
|
|
||||||
}
|
|
||||||
if k.NodeToken != "" && k.NodeToken == other.NodeToken {
|
|
||||||
score += 10
|
|
||||||
}
|
|
||||||
if k.OSDToken != "" && k.OSDToken == other.OSDToken {
|
|
||||||
score += 10
|
|
||||||
}
|
|
||||||
|
|
||||||
return score
|
return score
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ func TestMatchKey_MatchScore(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
if result := tt.key1.MatchScore(tt.key2); result != tt.expected {
|
if result := tt.key1.MatchScore(&tt.key2); result != tt.expected {
|
||||||
t.Errorf("MatchScore() = %v, want %v", result, tt.expected)
|
t.Errorf("MatchScore() = %v, want %v", result, tt.expected)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
+8
-6
@@ -2,6 +2,7 @@
|
|||||||
package onviftesting
|
package onviftesting
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
@@ -40,7 +41,7 @@ type GoldenFileSet struct {
|
|||||||
// LoadGoldenManifest loads a manifest.json from a golden directory.
|
// LoadGoldenManifest loads a manifest.json from a golden directory.
|
||||||
func LoadGoldenManifest(goldenDir string) (*GoldenManifest, error) {
|
func LoadGoldenManifest(goldenDir string) (*GoldenManifest, error) {
|
||||||
manifestPath := filepath.Join(goldenDir, "manifest.json")
|
manifestPath := filepath.Join(goldenDir, "manifest.json")
|
||||||
data, err := os.ReadFile(manifestPath)
|
data, err := os.ReadFile(manifestPath) //nolint:gosec // Path is from test data directory, safe
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to read manifest: %w", err)
|
return nil, fmt.Errorf("failed to read manifest: %w", err)
|
||||||
}
|
}
|
||||||
@@ -82,7 +83,7 @@ func LoadGoldenFiles(goldenDir string) (*GoldenFileSet, error) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := os.ReadFile(path)
|
data, err := os.ReadFile(path) //nolint:gosec // Path is from filepath.Walk, safe
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to read %s: %w", path, err)
|
return fmt.Errorf("failed to read %s: %w", path, err)
|
||||||
}
|
}
|
||||||
@@ -100,7 +101,7 @@ func LoadGoldenFiles(goldenDir string) (*GoldenFileSet, error) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("failed to load golden files: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return set, nil
|
return set, nil
|
||||||
@@ -171,6 +172,7 @@ func ValidateResponse(response interface{}, golden *GoldenFile) []string {
|
|||||||
actual, ok := responseData[field]
|
actual, ok := responseData[field]
|
||||||
if !ok {
|
if !ok {
|
||||||
errors = append(errors, fmt.Sprintf("missing field: %s", field))
|
errors = append(errors, fmt.Sprintf("missing field: %s", field))
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,12 +194,12 @@ func ValidateResponse(response interface{}, golden *GoldenFile) []string {
|
|||||||
func toMap(v interface{}) (map[string]interface{}, error) {
|
func toMap(v interface{}) (map[string]interface{}, error) {
|
||||||
data, err := json.Marshal(v)
|
data, err := json.Marshal(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("failed to marshal value: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var result map[string]interface{}
|
var result map[string]interface{}
|
||||||
if err := json.Unmarshal(data, &result); err != nil {
|
if err := json.Unmarshal(data, &result); err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("failed to unmarshal to map: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
@@ -230,7 +232,7 @@ func valuesEqual(expected, actual interface{}) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return string(e) == string(a)
|
return bytes.Equal(e, a)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SaveGoldenFile saves a golden file to disk.
|
// SaveGoldenFile saves a golden file to disk.
|
||||||
|
|||||||
+60
-34
@@ -263,6 +263,58 @@ func NewMockSOAPServerV2(archivePath string) (*MockSOAPServerV2, error) {
|
|||||||
return mock, nil
|
return mock, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// processArchiveEntry processes a single tar archive entry (JSON file) and adds it to the capture.
|
||||||
|
// Returns (isMetadata, error).
|
||||||
|
func processArchiveEntry(header *tar.Header, data []byte, capture *CameraCaptureV2) (*CaptureMetadata, error) {
|
||||||
|
// Check for metadata.json (V2 archives)
|
||||||
|
if header.Name == "metadata.json" || strings.HasSuffix(header.Name, "/metadata.json") {
|
||||||
|
var meta CaptureMetadata
|
||||||
|
if err := json.Unmarshal(data, &meta); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal metadata: %w", err)
|
||||||
|
}
|
||||||
|
return &meta, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip files that look like request/response XML stored as JSON
|
||||||
|
if strings.Contains(header.Name, "_request") || strings.Contains(header.Name, "_response") {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse exchange from JSON
|
||||||
|
exchange, err := parseExchange(header.Name, data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if exchange != nil {
|
||||||
|
capture.Exchanges = append(capture.Exchanges, *exchange)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseExchange parses a JSON exchange entry, supporting both V1 and V2 formats.
|
||||||
|
func parseExchange(fileName string, data []byte) (*CapturedExchangeV2, error) {
|
||||||
|
version := DetectCaptureVersion(data)
|
||||||
|
if version >= "2.0" {
|
||||||
|
var exchange CapturedExchangeV2
|
||||||
|
if err := json.Unmarshal(data, &exchange); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal V2 %s: %w", fileName, err)
|
||||||
|
}
|
||||||
|
return &exchange, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// V1 format - convert to V2
|
||||||
|
var v1Exchange CapturedExchange
|
||||||
|
if err := json.Unmarshal(data, &v1Exchange); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal V1 %s: %w", fileName, err)
|
||||||
|
}
|
||||||
|
v2Exchange := ConvertV1ToV2(&v1Exchange)
|
||||||
|
// Extract parameters from V1 request body
|
||||||
|
v2Exchange.Parameters = ExtractParameters(v2Exchange.OperationName, v2Exchange.RequestBody)
|
||||||
|
v2Exchange.ServiceType = DetermineServiceType(v2Exchange.RequestBody)
|
||||||
|
return v2Exchange, nil
|
||||||
|
}
|
||||||
|
|
||||||
// LoadCaptureFromArchiveV2 loads captures from archive, supporting both V1 and V2 formats.
|
// LoadCaptureFromArchiveV2 loads captures from archive, supporting both V1 and V2 formats.
|
||||||
func LoadCaptureFromArchiveV2(archivePath string) (*CameraCaptureV2, *CaptureMetadata, error) {
|
func LoadCaptureFromArchiveV2(archivePath string) (*CameraCaptureV2, *CaptureMetadata, error) {
|
||||||
file, err := os.Open(archivePath) //nolint:gosec // File path is from test data, safe
|
file, err := os.Open(archivePath) //nolint:gosec // File path is from test data, safe
|
||||||
@@ -308,40 +360,13 @@ func LoadCaptureFromArchiveV2(archivePath string) (*CameraCaptureV2, *CaptureMet
|
|||||||
return nil, nil, fmt.Errorf("failed to read file %s: %w", header.Name, err)
|
return nil, nil, fmt.Errorf("failed to read file %s: %w", header.Name, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for metadata.json (V2 archives)
|
// Process the archive entry
|
||||||
if header.Name == "metadata.json" || strings.HasSuffix(header.Name, "/metadata.json") {
|
meta, err := processArchiveEntry(header, data, capture)
|
||||||
var meta CaptureMetadata
|
if err != nil {
|
||||||
if err := json.Unmarshal(data, &meta); err != nil {
|
return nil, nil, err
|
||||||
return nil, nil, fmt.Errorf("failed to unmarshal metadata: %w", err)
|
|
||||||
}
|
|
||||||
metadata = &meta
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
if meta != nil {
|
||||||
// Skip files that look like request/response XML stored as JSON
|
metadata = meta
|
||||||
if strings.Contains(header.Name, "_request") || strings.Contains(header.Name, "_response") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Detect version and unmarshal accordingly
|
|
||||||
version := DetectCaptureVersion(data)
|
|
||||||
if version >= "2.0" {
|
|
||||||
var exchange CapturedExchangeV2
|
|
||||||
if err := json.Unmarshal(data, &exchange); err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("failed to unmarshal V2 %s: %w", header.Name, err)
|
|
||||||
}
|
|
||||||
capture.Exchanges = append(capture.Exchanges, exchange)
|
|
||||||
} else {
|
|
||||||
// V1 format - convert to V2
|
|
||||||
var v1Exchange CapturedExchange
|
|
||||||
if err := json.Unmarshal(data, &v1Exchange); err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("failed to unmarshal V1 %s: %w", header.Name, err)
|
|
||||||
}
|
|
||||||
v2Exchange := ConvertV1ToV2(&v1Exchange)
|
|
||||||
// Extract parameters from V1 request body
|
|
||||||
v2Exchange.Parameters = ExtractParameters(v2Exchange.OperationName, v2Exchange.RequestBody)
|
|
||||||
v2Exchange.ServiceType = DetermineServiceType(v2Exchange.RequestBody)
|
|
||||||
capture.Exchanges = append(capture.Exchanges, *v2Exchange)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -380,7 +405,7 @@ func (m *MockSOAPServerV2) handleRequest(w http.ResponseWriter, r *http.Request)
|
|||||||
|
|
||||||
for _, ex := range exchanges {
|
for _, ex := range exchanges {
|
||||||
exchangeKey := BuildMatchKeyFromExchange(ex)
|
exchangeKey := BuildMatchKeyFromExchange(ex)
|
||||||
score := requestKey.MatchScore(exchangeKey)
|
score := requestKey.MatchScore(&exchangeKey)
|
||||||
if score > bestScore {
|
if score > bestScore {
|
||||||
bestScore = score
|
bestScore = score
|
||||||
bestMatch = ex
|
bestMatch = ex
|
||||||
@@ -496,6 +521,7 @@ func ExtractParameters(operationName, soapBody string) map[string]interface{} {
|
|||||||
for i := 1; i < len(matches); i++ {
|
for i := 1; i < len(matches); i++ {
|
||||||
if matches[i] != "" {
|
if matches[i] != "" {
|
||||||
params[paramName] = strings.TrimSpace(matches[i])
|
params[paramName] = strings.TrimSpace(matches[i])
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -417,9 +417,10 @@ func ReadOperationsByService(service ServiceType) []OperationSpec {
|
|||||||
return EventReadOperations
|
return EventReadOperations
|
||||||
case ServiceDeviceIO:
|
case ServiceDeviceIO:
|
||||||
return DeviceIOReadOperations
|
return DeviceIOReadOperations
|
||||||
default:
|
case ServiceUnknown:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// IndependentOperations returns operations that don't depend on other operations.
|
// IndependentOperations returns operations that don't depend on other operations.
|
||||||
|
|||||||
+29
-19
@@ -9,6 +9,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const percentScale = 100
|
||||||
|
|
||||||
// Registry holds information about all available camera captures.
|
// Registry holds information about all available camera captures.
|
||||||
type Registry struct {
|
type Registry struct {
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
@@ -47,7 +49,7 @@ const DefaultRegistryPath = "testdata/captures/registry.json"
|
|||||||
|
|
||||||
// LoadRegistry loads the capture registry from a file.
|
// LoadRegistry loads the capture registry from a file.
|
||||||
func LoadRegistry(path string) (*Registry, error) {
|
func LoadRegistry(path string) (*Registry, error) {
|
||||||
data, err := os.ReadFile(path)
|
data, err := os.ReadFile(path) //nolint:gosec // Registry path is from constant or test data, safe
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
// Return empty registry if file doesn't exist
|
// Return empty registry if file doesn't exist
|
||||||
@@ -92,12 +94,13 @@ func SaveRegistry(registry *Registry, path string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AddCamera adds a new camera to the registry.
|
// AddCamera adds a new camera to the registry.
|
||||||
func (r *Registry) AddCamera(entry CameraEntry) {
|
func (r *Registry) AddCamera(entry *CameraEntry) {
|
||||||
// Check if camera already exists
|
// Check if camera already exists
|
||||||
for i, cam := range r.Cameras {
|
for i := range r.Cameras {
|
||||||
|
cam := &r.Cameras[i]
|
||||||
if cam.ID == entry.ID {
|
if cam.ID == entry.ID {
|
||||||
// Update existing entry
|
// Update existing entry
|
||||||
r.Cameras[i] = entry
|
r.Cameras[i] = *entry
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -106,7 +109,7 @@ func (r *Registry) AddCamera(entry CameraEntry) {
|
|||||||
if entry.AddedDate == "" {
|
if entry.AddedDate == "" {
|
||||||
entry.AddedDate = time.Now().Format("2006-01-02")
|
entry.AddedDate = time.Now().Format("2006-01-02")
|
||||||
}
|
}
|
||||||
r.Cameras = append(r.Cameras, entry)
|
r.Cameras = append(r.Cameras, *entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCamera retrieves a camera entry by ID.
|
// GetCamera retrieves a camera entry by ID.
|
||||||
@@ -121,21 +124,23 @@ func (r *Registry) GetCamera(id string) *CameraEntry {
|
|||||||
|
|
||||||
// RemoveCamera removes a camera from the registry.
|
// RemoveCamera removes a camera from the registry.
|
||||||
func (r *Registry) RemoveCamera(id string) bool {
|
func (r *Registry) RemoveCamera(id string) bool {
|
||||||
for i, cam := range r.Cameras {
|
for i := range r.Cameras {
|
||||||
|
cam := &r.Cameras[i]
|
||||||
if cam.ID == id {
|
if cam.ID == id {
|
||||||
r.Cameras = append(r.Cameras[:i], r.Cameras[i+1:]...)
|
r.Cameras = append(r.Cameras[:i], r.Cameras[i+1:]...)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCamerasByManufacturer returns all cameras from a specific manufacturer.
|
// GetCamerasByManufacturer returns all cameras from a specific manufacturer.
|
||||||
func (r *Registry) GetCamerasByManufacturer(manufacturer string) []CameraEntry {
|
func (r *Registry) GetCamerasByManufacturer(manufacturer string) []*CameraEntry {
|
||||||
var cameras []CameraEntry
|
var cameras []*CameraEntry
|
||||||
for _, cam := range r.Cameras {
|
for i := range r.Cameras {
|
||||||
if cam.Manufacturer == manufacturer {
|
if r.Cameras[i].Manufacturer == manufacturer {
|
||||||
cameras = append(cameras, cam)
|
cameras = append(cameras, &r.Cameras[i])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return cameras
|
return cameras
|
||||||
@@ -164,7 +169,7 @@ func (r *Registry) UpdateCoverage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetTotalCoverage returns the total coverage across all services.
|
// GetTotalCoverage returns the total coverage across all services.
|
||||||
func (r *Registry) GetTotalCoverage() (total int, captured int) {
|
func (r *Registry) GetTotalCoverage() (total, captured int) {
|
||||||
for _, cov := range r.Coverage {
|
for _, cov := range r.Coverage {
|
||||||
total += cov.Total
|
total += cov.Total
|
||||||
captured += cov.Captured
|
captured += cov.Captured
|
||||||
@@ -203,7 +208,8 @@ func sanitizeID(s string) string {
|
|||||||
func ValidateRegistry(registry *Registry, basePath string) []string {
|
func ValidateRegistry(registry *Registry, basePath string) []string {
|
||||||
var errors []string
|
var errors []string
|
||||||
|
|
||||||
for _, cam := range registry.Cameras {
|
for i := range registry.Cameras {
|
||||||
|
cam := ®istry.Cameras[i]
|
||||||
capturePath := filepath.Join(basePath, cam.CaptureFile)
|
capturePath := filepath.Join(basePath, cam.CaptureFile)
|
||||||
if _, err := os.Stat(capturePath); os.IsNotExist(err) {
|
if _, err := os.Stat(capturePath); os.IsNotExist(err) {
|
||||||
errors = append(errors, fmt.Sprintf("camera %s: capture file not found: %s", cam.ID, cam.CaptureFile))
|
errors = append(errors, fmt.Sprintf("camera %s: capture file not found: %s", cam.ID, cam.CaptureFile))
|
||||||
@@ -233,11 +239,13 @@ func CreateCameraEntryFromCapture(archivePath string) (*CameraEntry, error) {
|
|||||||
cameraInfo = metadata.CameraInfo
|
cameraInfo = metadata.CameraInfo
|
||||||
} else {
|
} else {
|
||||||
// Try to extract from GetDeviceInformation response
|
// Try to extract from GetDeviceInformation response
|
||||||
for _, ex := range capture.Exchanges {
|
for i := range capture.Exchanges {
|
||||||
|
ex := &capture.Exchanges[i]
|
||||||
if ex.OperationName == "GetDeviceInformation" {
|
if ex.OperationName == "GetDeviceInformation" {
|
||||||
cameraInfo.Manufacturer = ExtractXMLElement(ex.ResponseBody, "Manufacturer")
|
cameraInfo.Manufacturer = ExtractXMLElement(ex.ResponseBody, "Manufacturer")
|
||||||
cameraInfo.Model = ExtractXMLElement(ex.ResponseBody, "Model")
|
cameraInfo.Model = ExtractXMLElement(ex.ResponseBody, "Model")
|
||||||
cameraInfo.FirmwareVersion = ExtractXMLElement(ex.ResponseBody, "FirmwareVersion")
|
cameraInfo.FirmwareVersion = ExtractXMLElement(ex.ResponseBody, "FirmwareVersion")
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -268,7 +276,8 @@ func CreateCameraEntryFromCapture(archivePath string) (*CameraEntry, error) {
|
|||||||
func detectCapabilities(capture *CameraCaptureV2) []string {
|
func detectCapabilities(capture *CameraCaptureV2) []string {
|
||||||
services := make(map[string]bool)
|
services := make(map[string]bool)
|
||||||
|
|
||||||
for _, ex := range capture.Exchanges {
|
for i := range capture.Exchanges {
|
||||||
|
ex := &capture.Exchanges[i]
|
||||||
if ex.ServiceType != "" {
|
if ex.ServiceType != "" {
|
||||||
services[string(ex.ServiceType)] = true
|
services[string(ex.ServiceType)] = true
|
||||||
} else {
|
} else {
|
||||||
@@ -280,10 +289,11 @@ func detectCapabilities(capture *CameraCaptureV2) []string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var result []string
|
result := make([]string, 0, len(services))
|
||||||
for svc := range services {
|
for svc := range services {
|
||||||
result = append(result, svc)
|
result = append(result, svc)
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,8 +359,8 @@ func (r *Registry) GetSummary() RegistrySummary {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Count by manufacturer
|
// Count by manufacturer
|
||||||
for _, cam := range r.Cameras {
|
for i := range r.Cameras {
|
||||||
summary.ManufacturerCount[cam.Manufacturer]++
|
summary.ManufacturerCount[r.Cameras[i].Manufacturer]++
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate coverage percentages
|
// Calculate coverage percentages
|
||||||
@@ -358,7 +368,7 @@ func (r *Registry) GetSummary() RegistrySummary {
|
|||||||
summary.TotalOperations += cov.Total
|
summary.TotalOperations += cov.Total
|
||||||
summary.CapturedOperations += cov.Captured
|
summary.CapturedOperations += cov.Captured
|
||||||
if cov.Total > 0 {
|
if cov.Total > 0 {
|
||||||
summary.ServiceCoverage[service] = float64(cov.Captured) / float64(cov.Total) * 100
|
summary.ServiceCoverage[service] = float64(cov.Captured) / float64(cov.Total) * percentScale
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user