diff --git a/cmd/discover/main.go b/cmd/discover/main.go index 9e9ff3a..072dd2e 100644 --- a/cmd/discover/main.go +++ b/cmd/discover/main.go @@ -11,14 +11,13 @@ import ( "github.com/0x524a/onvif-go/discovery" ) +const defaultDiscoveryTimeout = 10 * time.Second + func main() { 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() - ctx, cancel := context.WithTimeout(context.Background(), *timeout) - defer cancel() - opts := &discovery.DiscoverOptions{ NetworkInterface: *iface, } @@ -29,9 +28,13 @@ func main() { } fmt.Println("...") + ctx, cancel := context.WithTimeout(context.Background(), *timeout) + defer cancel() + devices, err := discovery.DiscoverWithOptions(ctx, *timeout, opts) if err != nil { fmt.Fprintf(os.Stderr, "Discovery error: %v\n", err) + cancel() os.Exit(1) } diff --git a/cmd/generate-tests/main.go b/cmd/generate-tests/main.go index 0c2b01d..5c50f90 100644 --- a/cmd/generate-tests/main.go +++ b/cmd/generate-tests/main.go @@ -14,6 +14,11 @@ import ( onviftesting "github.com/0x524a/onvif-go/testing" ) +const ( + maxTokenLength = 20 + percentScale = 100 +) + var ( captureArchive = flag.String("capture", "", "Path to XML capture archive (.tar.gz)") outputDir = flag.String("output", "./", "Output directory for generated test file") @@ -128,7 +133,7 @@ type GeneratedTest struct { Code string } -// operationInfo holds info about captured operations +// operationInfo holds info about captured operations. type operationInfo struct { OperationName string ServiceType onviftesting.ServiceType @@ -199,7 +204,8 @@ func generateTests() string { metadata.CameraInfo.FirmwareVersion) } else { // 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 { manufacturer := extractXMLValue(ex.ResponseBody, "Manufacturer") model := extractXMLValue(ex.ResponseBody, "Model") @@ -207,6 +213,7 @@ func generateTests() string { if manufacturer != "" && model != "" { cameraDesc = fmt.Sprintf("%s %s (Firmware: %s)", manufacturer, model, firmware) } + break } } @@ -241,12 +248,9 @@ func generateTests() string { if err != nil { log.Fatalf("Failed to create output file: %v", err) } - defer func() { - _ = f.Close() - }() + defer f.Close() if err := tmpl.Execute(f, testData); err != nil { - _ = f.Close() log.Fatalf("Failed to execute template: %v", err) } @@ -264,10 +268,11 @@ func generateTests() string { } func analyzeOperations(capture *onviftesting.CameraCaptureV2) []operationInfo { - var ops []operationInfo + ops := make([]operationInfo, 0, len(capture.Exchanges)) seen := make(map[string]bool) - for _, ex := range capture.Exchanges { + for i := range capture.Exchanges { + ex := &capture.Exchanges[i] // Create unique key for deduplication key := ex.OperationName if token := ex.GetProfileToken(); token != "" { @@ -297,10 +302,13 @@ func analyzeOperations(capture *onviftesting.CameraCaptureV2) []operationInfo { func hasNonDeviceOperations(ops []operationInfo) bool { for _, op := range ops { switch op.ServiceType { - case onviftesting.ServiceMedia, onviftesting.ServicePTZ, onviftesting.ServiceImaging: + case onviftesting.ServiceMedia, onviftesting.ServicePTZ, onviftesting.ServiceImaging, onviftesting.ServiceEvent, onviftesting.ServiceDeviceIO: return true + case onviftesting.ServiceDevice, onviftesting.ServiceUnknown: + // continue checking } } + return false } @@ -574,6 +582,7 @@ func generatePTZTests(ops []operationInfo) []GeneratedTest { Code: code, }) delete(ptzOps, op.OperationName) + continue } if token, ok := op.Parameters["ProfileToken"].(string); ok && token != "" { @@ -699,9 +708,10 @@ func sanitizeToken(token string) string { token = strings.ReplaceAll(token, ".", "_") token = strings.ReplaceAll(token, " ", "_") // Truncate if too long - if len(token) > 20 { - token = token[:20] + if len(token) > maxTokenLength { + token = token[:maxTokenLength] } + return token } @@ -774,7 +784,7 @@ func updateCameraRegistry(regPath, archivePath, testFile string) { } // Add or update the camera entry - registry.AddCamera(*entry) + registry.AddCamera(entry) // Update coverage statistics updateRegistryCoverage(registry, archivePath) @@ -874,7 +884,7 @@ func generateCoverageMarkdown(registry *onviftesting.Registry) string { total, captured := registry.GetTotalCoverage() if total > 0 { sb.WriteString(fmt.Sprintf("- **Overall Coverage**: %.1f%% (%d/%d operations)\n\n", - float64(captured)/float64(total)*100, captured, total)) + float64(captured)/float64(total)*percentScale, captured, total)) } // Cameras @@ -883,7 +893,8 @@ func generateCoverageMarkdown(registry *onviftesting.Registry) string { sb.WriteString("| Manufacturer | Model | Firmware | Operations | Capabilities |\n") sb.WriteString("|--------------|-------|----------|------------|---------------|\n") - for _, cam := range registry.Cameras { + for i := range registry.Cameras { + cam := ®istry.Cameras[i] caps := strings.Join(cam.Capabilities, ", ") sb.WriteString(fmt.Sprintf("| %s | %s | %s | %d | %s |\n", cam.Manufacturer, cam.Model, cam.Firmware, cam.OperationsCaptured, caps)) @@ -902,7 +913,7 @@ func generateCoverageMarkdown(registry *onviftesting.Registry) string { if cov, ok := registry.Coverage[service]; ok { pct := 0.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", service, cov.Total, cov.Captured, pct)) diff --git a/cmd/onvif-diagnostics/main.go b/cmd/onvif-diagnostics/main.go index da90911..e1a0873 100644 --- a/cmd/onvif-diagnostics/main.go +++ b/cmd/onvif-diagnostics/main.go @@ -30,6 +30,7 @@ const ( retryDelaySec = 5 maxIdleTimeoutSec = 90 unknownStatus = "Unknown" + percentScale = 100 ) type CameraReport struct { @@ -1332,7 +1333,7 @@ func runComprehensiveCapture(ctx context.Context, client *onvif.Client, report * fmt.Printf(" Total operations: %d\n", totalOps) fmt.Printf(" Successful: %d\n", successCount) fmt.Printf(" Failed: %d\n", failCount) - fmt.Printf(" Success rate: %.1f%%\n", float64(successCount)/float64(totalOps)*100) + fmt.Printf(" Success rate: %.1f%%\n", float64(successCount)/float64(totalOps)*percentScale) fmt.Println("========================================") } @@ -1633,6 +1634,73 @@ func extractSOAPOperation(soapBody string) string { 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) //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 +} + // createTarGzV2 creates a V2 tar.gz archive with metadata.json first. func createTarGzV2(sourceDir, archivePath string) error { // Create archive file @@ -1673,67 +1741,14 @@ func createTarGzV2(sourceDir, archivePath string) error { // Sort files: metadata.json first, then capture JSON files in order, then XML files sort.Slice(files, func(i, j int) bool { - nameI := filepath.Base(files[i]) - nameJ := filepath.Base(files[j]) - - // metadata.json always first - if nameI == "metadata.json" { - return true - } - if nameJ == "metadata.json" { - return false - } - - // JSON files before XML files - isJSONi := strings.HasSuffix(nameI, ".json") - isJSONj := strings.HasSuffix(nameJ, ".json") - if isJSONi && !isJSONj { - return true - } - if !isJSONi && isJSONj { - return false - } - - // Sort by name - return nameI < nameJ + return compareFileOrder(i, j, files) }) // Write files in sorted order for _, path := range files { - info, err := os.Stat(path) - if err != nil { - return fmt.Errorf("failed to stat file: %w", err) + if err := writeTarEntry(tarWriter, sourceDir, path); err != nil { + return 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 diff --git a/go.mod b/go.mod index a0cc30b..2655e58 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require github.com/0x524A/rtspeek v0.0.1 require ( github.com/bluenviron/gortsplib/v4 v4.16.2 // 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/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect diff --git a/go.sum b/go.sum index 6931161..ac49297 100644 --- a/go.sum +++ b/go.sum @@ -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/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/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/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/testing/capture_types.go b/testing/capture_types.go index 01e8110..190a1ee 100644 --- a/testing/capture_types.go +++ b/testing/capture_types.go @@ -273,6 +273,14 @@ func BuildMatchKeyFromExchange(exchange *CapturedExchangeV2) MatchKey { } } +// addTokenScore adds 10 points to score if token matches between two MatchKeys. +func addTokenScore(score int, token1, token2 string) int { + if token1 != "" && token1 == token2 { + return score + 10 + } + return score +} + // MatchScore returns how well two MatchKeys match (higher is better). // Returns -1 if operation names don't match. func (k MatchKey) MatchScore(other MatchKey) int { @@ -283,27 +291,13 @@ func (k MatchKey) MatchScore(other MatchKey) int { score := 1 // Base score for matching operation // Bonus points for matching parameters - if k.ProfileToken != "" && k.ProfileToken == other.ProfileToken { - score += 10 - } - if k.ConfigurationToken != "" && k.ConfigurationToken == other.ConfigurationToken { - score += 10 - } - if k.VideoSourceToken != "" && k.VideoSourceToken == other.VideoSourceToken { - 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 - } + score = addTokenScore(score, k.ProfileToken, other.ProfileToken) + score = addTokenScore(score, k.ConfigurationToken, other.ConfigurationToken) + score = addTokenScore(score, k.VideoSourceToken, other.VideoSourceToken) + score = addTokenScore(score, k.AudioSourceToken, other.AudioSourceToken) + score = addTokenScore(score, k.PresetToken, other.PresetToken) + score = addTokenScore(score, k.NodeToken, other.NodeToken) + score = addTokenScore(score, k.OSDToken, other.OSDToken) return score } diff --git a/testing/golden.go b/testing/golden.go index 69ef307..ccfee59 100644 --- a/testing/golden.go +++ b/testing/golden.go @@ -2,6 +2,7 @@ package onviftesting import ( + "bytes" "encoding/json" "fmt" "os" @@ -40,7 +41,7 @@ type GoldenFileSet struct { // LoadGoldenManifest loads a manifest.json from a golden directory. func LoadGoldenManifest(goldenDir string) (*GoldenManifest, error) { 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 { return nil, fmt.Errorf("failed to read manifest: %w", err) } @@ -82,7 +83,7 @@ func LoadGoldenFiles(goldenDir string) (*GoldenFileSet, error) { return nil } - data, err := os.ReadFile(path) + data, err := os.ReadFile(path) //nolint:gosec // Path is from filepath.Walk, safe if err != nil { return fmt.Errorf("failed to read %s: %w", path, err) } @@ -100,7 +101,7 @@ func LoadGoldenFiles(goldenDir string) (*GoldenFileSet, error) { }) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to load golden files: %w", err) } return set, nil @@ -171,6 +172,7 @@ func ValidateResponse(response interface{}, golden *GoldenFile) []string { actual, ok := responseData[field] if !ok { errors = append(errors, fmt.Sprintf("missing field: %s", field)) + continue } @@ -192,12 +194,12 @@ func ValidateResponse(response interface{}, golden *GoldenFile) []string { func toMap(v interface{}) (map[string]interface{}, error) { data, err := json.Marshal(v) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to marshal value: %w", err) } var result map[string]interface{} 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 @@ -230,7 +232,7 @@ func valuesEqual(expected, actual interface{}) bool { return false } - return string(e) == string(a) + return bytes.Equal(e, a) } // SaveGoldenFile saves a golden file to disk. diff --git a/testing/mock_server.go b/testing/mock_server.go index 9df584a..732022b 100644 --- a/testing/mock_server.go +++ b/testing/mock_server.go @@ -263,6 +263,58 @@ func NewMockSOAPServerV2(archivePath string) (*MockSOAPServerV2, error) { 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. func LoadCaptureFromArchiveV2(archivePath string) (*CameraCaptureV2, *CaptureMetadata, error) { 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) } - // 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, nil, fmt.Errorf("failed to unmarshal metadata: %w", err) - } - metadata = &meta - continue + // Process the archive entry + meta, err := processArchiveEntry(header, data, capture) + if err != nil { + return nil, nil, err } - - // Skip files that look like request/response XML stored as JSON - 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) + if meta != nil { + metadata = meta } } @@ -496,6 +521,7 @@ func ExtractParameters(operationName, soapBody string) map[string]interface{} { for i := 1; i < len(matches); i++ { if matches[i] != "" { params[paramName] = strings.TrimSpace(matches[i]) + break } } diff --git a/testing/operations.go b/testing/operations.go index bf74d98..99604af 100644 --- a/testing/operations.go +++ b/testing/operations.go @@ -417,9 +417,10 @@ func ReadOperationsByService(service ServiceType) []OperationSpec { return EventReadOperations case ServiceDeviceIO: return DeviceIOReadOperations - default: + case ServiceUnknown: return nil } + return nil } // IndependentOperations returns operations that don't depend on other operations. diff --git a/testing/registry.go b/testing/registry.go index 3b6a64c..b5350b5 100644 --- a/testing/registry.go +++ b/testing/registry.go @@ -9,6 +9,8 @@ import ( "time" ) +const percentScale = 100 + // Registry holds information about all available camera captures. type Registry struct { Version string `json:"version"` @@ -47,7 +49,7 @@ const DefaultRegistryPath = "testdata/captures/registry.json" // LoadRegistry loads the capture registry from a file. 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 os.IsNotExist(err) { // 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. -func (r *Registry) AddCamera(entry CameraEntry) { +func (r *Registry) AddCamera(entry *CameraEntry) { // 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 { // Update existing entry - r.Cameras[i] = entry + r.Cameras[i] = *entry return } } @@ -106,7 +109,7 @@ func (r *Registry) AddCamera(entry CameraEntry) { if entry.AddedDate == "" { 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. @@ -121,12 +124,14 @@ func (r *Registry) GetCamera(id string) *CameraEntry { // RemoveCamera removes a camera from the registry. 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 { r.Cameras = append(r.Cameras[:i], r.Cameras[i+1:]...) return true } } + return false } @@ -164,7 +169,7 @@ func (r *Registry) UpdateCoverage() { } // 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 { total += cov.Total captured += cov.Captured @@ -238,6 +243,7 @@ func CreateCameraEntryFromCapture(archivePath string) (*CameraEntry, error) { cameraInfo.Manufacturer = ExtractXMLElement(ex.ResponseBody, "Manufacturer") cameraInfo.Model = ExtractXMLElement(ex.ResponseBody, "Model") cameraInfo.FirmwareVersion = ExtractXMLElement(ex.ResponseBody, "FirmwareVersion") + break } } @@ -280,10 +286,11 @@ func detectCapabilities(capture *CameraCaptureV2) []string { } } - var result []string + result := make([]string, 0, len(services)) for svc := range services { result = append(result, svc) } + return result } @@ -358,7 +365,7 @@ func (r *Registry) GetSummary() RegistrySummary { summary.TotalOperations += cov.Total summary.CapturedOperations += cov.Captured if cov.Total > 0 { - summary.ServiceCoverage[service] = float64(cov.Captured) / float64(cov.Total) * 100 + summary.ServiceCoverage[service] = float64(cov.Captured) / float64(cov.Total) * percentScale } }