Refactor code for improved readability and maintainability; add utility functions for score calculation and archive processing

This commit is contained in:
ProtoTess
2026-01-16 05:55:37 +00:00
parent c701115620
commit 268330840c
10 changed files with 208 additions and 146 deletions
+7 -4
View File
@@ -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)
}
+26 -15
View File
@@ -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 := &registry.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))
+72 -57
View File
@@ -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
+1
View File
@@ -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
+2
View File
@@ -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=
+15 -21
View File
@@ -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
}
+8 -6
View File
@@ -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.
+59 -33
View File
@@ -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
}
}
+2 -1
View File
@@ -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.
+16 -9
View File
@@ -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
}
}