Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package goldpinger for openSUSE:Factory 
checked in at 2026-04-21 12:43:28
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/goldpinger (Old)
 and      /work/SRC/openSUSE:Factory/.goldpinger.new.11940 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "goldpinger"

Tue Apr 21 12:43:28 2026 rev:4 rq:1348285 version:3.11.1

Changes:
--------
--- /work/SRC/openSUSE:Factory/goldpinger/goldpinger.changes    2026-04-04 
19:09:46.722933877 +0200
+++ /work/SRC/openSUSE:Factory/.goldpinger.new.11940/goldpinger.changes 
2026-04-21 12:43:49.167931569 +0200
@@ -1,0 +2,9 @@
+Mon Apr 20 17:22:05 UTC 2026 - Johannes Kastl 
<[email protected]>
+
+- Update to version 3.11.1:
+  * Increment the version
+  * Clean up duplicate and out-of-order counters on pinger teardown
+  * Add UDP sequence number tracking for duplicate and out-of-order
+    detection
+
+-------------------------------------------------------------------

Old:
----
  goldpinger-3.11.0.obscpio

New:
----
  goldpinger-3.11.1.obscpio

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ goldpinger.spec ++++++
--- /var/tmp/diff_new_pack.JXFGmY/_old  2026-04-21 12:43:50.335980099 +0200
+++ /var/tmp/diff_new_pack.JXFGmY/_new  2026-04-21 12:43:50.339980265 +0200
@@ -17,7 +17,7 @@
 
 
 Name:           goldpinger
-Version:        3.11.0
+Version:        3.11.1
 Release:        0
 Summary:        Tests and displays connectivity between nodes in a Kubernetes 
cluster
 License:        Apache-2.0

++++++ _service ++++++
--- /var/tmp/diff_new_pack.JXFGmY/_old  2026-04-21 12:43:50.375981761 +0200
+++ /var/tmp/diff_new_pack.JXFGmY/_new  2026-04-21 12:43:50.375981761 +0200
@@ -3,7 +3,7 @@
     <param name="url">https://github.com/bloomberg/goldpinger</param>
     <param name="scm">git</param>
     <param name="exclude">.git</param>
-    <param name="revision">v3.11.0</param>
+    <param name="revision">v3.11.1</param>
     <param name="match-tag">v*</param>
     <param name="versionformat">@PARENT_TAG@</param>
     <param name="versionrewrite-pattern">v(.*)</param>

++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.JXFGmY/_old  2026-04-21 12:43:50.403982924 +0200
+++ /var/tmp/diff_new_pack.JXFGmY/_new  2026-04-21 12:43:50.411983257 +0200
@@ -1,6 +1,6 @@
 <servicedata>
 <service name="tar_scm">
                 <param 
name="url">https://github.com/bloomberg/goldpinger</param>
-              <param 
name="changesrevision">7935a11f9de2cbfb0d2b7cbc0348d9bc6b39b53e</param></service></servicedata>
+              <param 
name="changesrevision">fdb70968b886d341de2ea9300058ab308186717f</param></service></servicedata>
 (No newline at EOF)
 

++++++ goldpinger-3.11.0.obscpio -> goldpinger-3.11.1.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/goldpinger-3.11.0/Makefile 
new/goldpinger-3.11.1/Makefile
--- old/goldpinger-3.11.0/Makefile      2026-04-03 19:04:31.000000000 +0200
+++ new/goldpinger-3.11.1/Makefile      2026-04-20 16:03:22.000000000 +0200
@@ -1,5 +1,5 @@
 name ?= goldpinger
-version ?= v3.11.0
+version ?= v3.11.1
 bin ?= goldpinger
 pkg ?= "github.com/bloomberg/goldpinger"
 tag = $(name):$(version)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/goldpinger-3.11.0/README.md 
new/goldpinger-3.11.1/README.md
--- old/goldpinger-3.11.0/README.md     2026-04-03 19:04:31.000000000 +0200
+++ new/goldpinger-3.11.1/README.md     2026-04-20 16:03:22.000000000 +0200
@@ -328,6 +328,8 @@
 goldpinger_peers_hop_count         # gauge: estimated hop count
 goldpinger_peers_udp_rtt_s         # histogram: UDP round-trip time in seconds
 goldpinger_udp_errors_total        # counter: UDP probe errors
+goldpinger_udp_duplicates_total    # counter: duplicate UDP reply packets
+goldpinger_udp_out_of_order_total  # counter: out-of-order UDP reply packets
 ```
 
 Links with partial loss are shown as yellow edges in the graph UI, and edge 
labels display the UDP RTT instead of HTTP latency when available.
@@ -370,6 +372,8 @@
 goldpinger_peers_loss_pct           # (UDP probe, when enabled)
 goldpinger_peers_hop_count          # (UDP probe, when enabled)
 goldpinger_peers_udp_rtt_s_*        # (UDP probe, when enabled)
+goldpinger_udp_duplicates_total     # (UDP probe, when enabled)
+goldpinger_udp_out_of_order_total   # (UDP probe, when enabled)
 ```
 
 ### Grafana
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/goldpinger-3.11.0/charts/goldpinger/Chart.yaml 
new/goldpinger-3.11.1/charts/goldpinger/Chart.yaml
--- old/goldpinger-3.11.0/charts/goldpinger/Chart.yaml  2026-04-03 
19:04:31.000000000 +0200
+++ new/goldpinger-3.11.1/charts/goldpinger/Chart.yaml  2026-04-20 
16:03:22.000000000 +0200
@@ -1,7 +1,7 @@
 apiVersion: v1
 name: goldpinger
-appVersion: "3.11.0"
-version: 1.1.0
+appVersion: "3.11.1"
+version: 1.1.1
 description: Goldpinger is a tool to help debug, troubleshoot and visualize 
network connectivity and slowness issues.
 home: https://github.com/bloomberg/goldpinger
 sources:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/goldpinger-3.11.0/pkg/goldpinger/pinger.go 
new/goldpinger-3.11.1/pkg/goldpinger/pinger.go
--- old/goldpinger-3.11.0/pkg/goldpinger/pinger.go      2026-04-03 
19:04:31.000000000 +0200
+++ new/goldpinger-3.11.1/pkg/goldpinger/pinger.go      2026-04-20 
16:03:22.000000000 +0200
@@ -165,6 +165,12 @@
                if udpResult.AvgRttS > 0 {
                        ObservePeerUDPRtt(p.pod.HostIP, p.pod.PodIP, 
udpResult.AvgRttS)
                }
+               if udpResult.Duplicates > 0 {
+                       CountUDPDuplicates(p.pod.HostIP, p.pod.PodIP, 
udpResult.Duplicates)
+               }
+               if udpResult.OutOfOrder > 0 {
+                       CountUDPOutOfOrder(p.pod.HostIP, p.pod.PodIP, 
udpResult.OutOfOrder)
+               }
        }
 
        if OK {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/goldpinger-3.11.0/pkg/goldpinger/stats.go 
new/goldpinger-3.11.1/pkg/goldpinger/stats.go
--- old/goldpinger-3.11.0/pkg/goldpinger/stats.go       2026-04-03 
19:04:31.000000000 +0200
+++ new/goldpinger-3.11.1/pkg/goldpinger/stats.go       2026-04-20 
16:03:22.000000000 +0200
@@ -167,6 +167,28 @@
                        "host",
                },
        )
+       goldpingerUDPDuplicatesCounter = prometheus.NewCounterVec(
+               prometheus.CounterOpts{
+                       Name: "goldpinger_udp_duplicates_total",
+                       Help: "Count of duplicate UDP reply packets received",
+               },
+               []string{
+                       "goldpinger_instance",
+                       "host_ip",
+                       "pod_ip",
+               },
+       )
+       goldpingerUDPOutOfOrderCounter = prometheus.NewCounterVec(
+               prometheus.CounterOpts{
+                       Name: "goldpinger_udp_out_of_order_total",
+                       Help: "Count of out-of-order UDP reply packets 
received",
+               },
+               []string{
+                       "goldpinger_instance",
+                       "host_ip",
+                       "pod_ip",
+               },
+       )
        bootTime = time.Now()
 )
 
@@ -184,6 +206,8 @@
        prometheus.MustRegister(goldpingerPeersHopCount)
        prometheus.MustRegister(goldpingerPeersUDPRtt)
        prometheus.MustRegister(goldpingerUDPErrorsCounter)
+       prometheus.MustRegister(goldpingerUDPDuplicatesCounter)
+       prometheus.MustRegister(goldpingerUDPOutOfOrderCounter)
        zap.L().Info("Metrics setup - see /metrics")
 }
 
@@ -276,11 +300,15 @@
        ).Set(float64(hopCount))
 }
 
-// DeletePeerUDPMetrics removes stale UDP metric labels for a destroyed peer
+// DeletePeerUDPMetrics removes stale UDP metric labels for a destroyed peer.
+// This must be kept in sync with all per-peer UDP metrics to avoid stale
+// label sets lingering in /metrics after a pod rolls.
 func DeletePeerUDPMetrics(hostIP, podIP string) {
        goldpingerPeersLossPct.DeleteLabelValues(GoldpingerConfig.Hostname, 
hostIP, podIP)
        goldpingerPeersHopCount.DeleteLabelValues(GoldpingerConfig.Hostname, 
hostIP, podIP)
        goldpingerPeersUDPRtt.DeleteLabelValues(GoldpingerConfig.Hostname, 
hostIP, podIP)
+       
goldpingerUDPDuplicatesCounter.DeleteLabelValues(GoldpingerConfig.Hostname, 
hostIP, podIP)
+       
goldpingerUDPOutOfOrderCounter.DeleteLabelValues(GoldpingerConfig.Hostname, 
hostIP, podIP)
 }
 
 // ObservePeerUDPRtt records a UDP RTT observation in seconds
@@ -300,6 +328,24 @@
        ).Inc()
 }
 
+// CountUDPDuplicates adds to the duplicate packet counter for a peer
+func CountUDPDuplicates(hostIP, podIP string, n int) {
+       goldpingerUDPDuplicatesCounter.WithLabelValues(
+               GoldpingerConfig.Hostname,
+               hostIP,
+               podIP,
+       ).Add(float64(n))
+}
+
+// CountUDPOutOfOrder adds to the out-of-order packet counter for a peer
+func CountUDPOutOfOrder(hostIP, podIP string, n int) {
+       goldpingerUDPOutOfOrderCounter.WithLabelValues(
+               GoldpingerConfig.Hostname,
+               hostIP,
+               podIP,
+       ).Add(float64(n))
+}
+
 // returns a timer for easy observing of the durations of calls to kubernetes 
API
 func GetLabeledKubernetesCallsTimer() *prometheus.Timer {
        return prometheus.NewTimer(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/goldpinger-3.11.0/pkg/goldpinger/stats_test.go 
new/goldpinger-3.11.1/pkg/goldpinger/stats_test.go
--- old/goldpinger-3.11.0/pkg/goldpinger/stats_test.go  1970-01-01 
01:00:00.000000000 +0100
+++ new/goldpinger-3.11.1/pkg/goldpinger/stats_test.go  2026-04-20 
16:03:22.000000000 +0200
@@ -0,0 +1,69 @@
+package goldpinger
+
+import (
+       "testing"
+
+       "github.com/prometheus/client_golang/prometheus"
+       dto "github.com/prometheus/client_model/go"
+)
+
+// TestDeletePeerUDPMetrics_CleansAllPerPeerMetrics verifies that
+// DeletePeerUDPMetrics removes label sets from every per-peer UDP metric.
+// If a new per-peer metric is added but not cleaned up in
+// DeletePeerUDPMetrics, this test will fail.
+func TestDeletePeerUDPMetrics_CleansAllPerPeerMetrics(t *testing.T) {
+       // Save and restore hostname since we set it for the test
+       origHostname := GoldpingerConfig.Hostname
+       GoldpingerConfig.Hostname = "test-instance"
+       defer func() { GoldpingerConfig.Hostname = origHostname }()
+
+       hostIP := "10.0.0.1"
+       podIP := "10.0.0.2"
+
+       // Populate all per-peer UDP metrics so they have label values
+       SetPeerLossPct(hostIP, podIP, 5.0)
+       SetPeerHopCount(hostIP, podIP, 2)
+       ObservePeerUDPRtt(hostIP, podIP, 0.001)
+       CountUDPDuplicates(hostIP, podIP, 1)
+       CountUDPOutOfOrder(hostIP, podIP, 1)
+
+       // Verify they exist before cleanup
+       perPeerCollectors := map[string]prometheus.Collector{
+               "goldpinger_peers_loss_pct":         goldpingerPeersLossPct,
+               "goldpinger_peers_hop_count":        goldpingerPeersHopCount,
+               "goldpinger_peers_udp_rtt_s":        goldpingerPeersUDPRtt,
+               "goldpinger_udp_duplicates_total":   
goldpingerUDPDuplicatesCounter,
+               "goldpinger_udp_out_of_order_total": 
goldpingerUDPOutOfOrderCounter,
+       }
+
+       for name, collector := range perPeerCollectors {
+               if countMetrics(collector) == 0 {
+                       t.Fatalf("metric %s has no label values before cleanup 
— test setup is broken", name)
+               }
+       }
+
+       // Run cleanup
+       DeletePeerUDPMetrics(hostIP, podIP)
+
+       // Verify all per-peer metrics are cleaned up
+       for name, collector := range perPeerCollectors {
+               if n := countMetrics(collector); n != 0 {
+                       t.Errorf("metric %s still has %d label set(s) after 
DeletePeerUDPMetrics — add it to the cleanup function", name, n)
+               }
+       }
+}
+
+// countMetrics returns the number of metric families (label sets) for a 
collector.
+func countMetrics(c prometheus.Collector) int {
+       ch := make(chan prometheus.Metric, 100)
+       c.Collect(ch)
+       close(ch)
+       count := 0
+       for m := range ch {
+               var d dto.Metric
+               if err := m.Write(&d); err == nil {
+                       count++
+               }
+       }
+       return count
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/goldpinger-3.11.0/pkg/goldpinger/udp_probe.go 
new/goldpinger-3.11.1/pkg/goldpinger/udp_probe.go
--- old/goldpinger-3.11.0/pkg/goldpinger/udp_probe.go   2026-04-03 
19:04:31.000000000 +0200
+++ new/goldpinger-3.11.1/pkg/goldpinger/udp_probe.go   2026-04-20 
16:03:22.000000000 +0200
@@ -40,10 +40,12 @@
 
 // UDPProbeResult holds the results of a UDP probe to a peer
 type UDPProbeResult struct {
-       LossPct  float64
-       HopCount int32
-       AvgRttS  float64
-       Err      error
+       LossPct    float64
+       HopCount   int32
+       AvgRttS    float64
+       Duplicates int
+       OutOfOrder int
+       Err        error
 }
 
 // StartUDPListener starts a UDP echo listener on the given port.
@@ -80,7 +82,51 @@
        }
 }
 
-// ProbeUDP sends count UDP packets to the target and measures loss and hop 
count.
+// recvState tracks state accumulated while receiving UDP probe replies.
+type recvState struct {
+       received   int
+       totalRttNs int64
+       ttlValue   int
+       ttlFound   bool
+       seen       map[uint32]bool // sequence numbers already received
+       highestSeq int             // highest sequence number seen so far (-1 = 
none)
+       duplicates int
+       outOfOrder int
+}
+
+// processPacket inspects a received packet and updates the receive state.
+// Returns true if the packet was a valid, non-duplicate GPNG reply.
+func (s *recvState) processPacket(buf []byte, n int, now time.Time) bool {
+       if n < udpHeaderSize {
+               return false
+       }
+       magic := binary.BigEndian.Uint32(buf[0:4])
+       if magic != udpMagic {
+               return false
+       }
+       seq := binary.BigEndian.Uint32(buf[4:8])
+       if s.seen[seq] {
+               s.duplicates++
+               return false
+       }
+       s.seen[seq] = true
+
+       seqInt := int(seq)
+       if s.highestSeq >= 0 && seqInt < s.highestSeq {
+               s.outOfOrder++
+       }
+       if seqInt > s.highestSeq {
+               s.highestSeq = seqInt
+       }
+
+       sentNs := int64(binary.BigEndian.Uint64(buf[8:16]))
+       s.totalRttNs += now.UnixNano() - sentNs
+       s.received++
+       return true
+}
+
+// ProbeUDP sends count UDP packets to the target and measures loss, hop count,
+// RTT, and detects duplicate or out-of-order replies via sequence numbers.
 func ProbeUDP(targetIP string, port, count, size int, timeout time.Duration) 
UDPProbeResult {
        if count <= 0 {
                return UDPProbeResult{Err: fmt.Errorf("packet count must be > 
0, got %d", count)}
@@ -102,8 +148,6 @@
 
        // Determine if this is IPv4 or IPv6 and set up TTL/HopLimit reading
        isIPv6 := net.ParseIP(targetIP).To4() == nil
-       var ttlValue int
-       ttlFound := false
 
        if isIPv6 {
                p := ipv6.NewPacketConn(conn.(*net.UDPConn))
@@ -132,78 +176,67 @@
                }
        }
 
-       // Receive replies
-       received := 0
-       var totalRttNs int64
+       // Receive replies, tracking sequence numbers for duplicates/reordering
+       state := recvState{
+               seen:       make(map[uint32]bool, count),
+               highestSeq: -1,
+       }
        deadline := time.Now().Add(timeout)
        conn.SetReadDeadline(deadline)
-
        recvBuf := make([]byte, udpMaxPacketSize)
 
+       // We keep receiving until we have count unique replies or timeout.
+       // Duplicates don't count toward the received total, so we allow
+       // more iterations than count to handle them.
+       maxIter := count * 2
        if isIPv6 {
                p := ipv6.NewPacketConn(conn.(*net.UDPConn))
-               for received < count {
+               for i := 0; i < maxIter && state.received < count; i++ {
                        n, cm, _, err := p.ReadFrom(recvBuf)
                        now := time.Now()
                        if err != nil {
                                break
                        }
-                       if n < udpHeaderSize {
-                               continue
-                       }
-                       magic := binary.BigEndian.Uint32(recvBuf[0:4])
-                       if magic != udpMagic {
-                               continue
-                       }
-                       sentNs := int64(binary.BigEndian.Uint64(recvBuf[8:16]))
-                       totalRttNs += now.UnixNano() - sentNs
-                       received++
-                       if cm != nil && cm.HopLimit > 0 && !ttlFound {
-                               ttlValue = cm.HopLimit
-                               ttlFound = true
+                       state.processPacket(recvBuf[:n], n, now)
+                       if cm != nil && cm.HopLimit > 0 && !state.ttlFound {
+                               state.ttlValue = cm.HopLimit
+                               state.ttlFound = true
                        }
                }
        } else {
                p := ipv4.NewPacketConn(conn.(*net.UDPConn))
-               for received < count {
+               for i := 0; i < maxIter && state.received < count; i++ {
                        n, cm, _, err := p.ReadFrom(recvBuf)
                        now := time.Now()
                        if err != nil {
                                break
                        }
-                       if n < udpHeaderSize {
-                               continue
-                       }
-                       magic := binary.BigEndian.Uint32(recvBuf[0:4])
-                       if magic != udpMagic {
-                               continue
-                       }
-                       sentNs := int64(binary.BigEndian.Uint64(recvBuf[8:16]))
-                       totalRttNs += now.UnixNano() - sentNs
-                       received++
-                       if cm != nil && cm.TTL > 0 && !ttlFound {
-                               ttlValue = cm.TTL
-                               ttlFound = true
+                       state.processPacket(recvBuf[:n], n, now)
+                       if cm != nil && cm.TTL > 0 && !state.ttlFound {
+                               state.ttlValue = cm.TTL
+                               state.ttlFound = true
                        }
                }
        }
 
-       lossPct := float64(count-received) / float64(count) * 100.0
+       lossPct := float64(count-state.received) / float64(count) * 100.0
 
        var hopCount int32
-       if ttlFound {
-               hopCount = estimateHops(ttlValue)
+       if state.ttlFound {
+               hopCount = estimateHops(state.ttlValue)
        }
 
        var avgRttS float64
-       if received > 0 {
-               avgRttS = float64(totalRttNs) / float64(received) / 1e9
+       if state.received > 0 {
+               avgRttS = float64(state.totalRttNs) / float64(state.received) / 
1e9
        }
 
        return UDPProbeResult{
-               LossPct:  lossPct,
-               HopCount: hopCount,
-               AvgRttS:  avgRttS,
+               LossPct:    lossPct,
+               HopCount:   hopCount,
+               AvgRttS:    avgRttS,
+               Duplicates: state.duplicates,
+               OutOfOrder: state.outOfOrder,
        }
 }
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/goldpinger-3.11.0/pkg/goldpinger/udp_probe_test.go 
new/goldpinger-3.11.1/pkg/goldpinger/udp_probe_test.go
--- old/goldpinger-3.11.0/pkg/goldpinger/udp_probe_test.go      2026-04-03 
19:04:31.000000000 +0200
+++ new/goldpinger-3.11.1/pkg/goldpinger/udp_probe_test.go      2026-04-20 
16:03:22.000000000 +0200
@@ -71,6 +71,76 @@
        return port, func() { pc.Close() }
 }
 
+// startDuplicatingEchoListener echoes every packet twice, producing 
duplicates.
+func startDuplicatingEchoListener(t *testing.T) (int, func()) {
+       t.Helper()
+       pc, err := net.ListenPacket("udp", "127.0.0.1:0")
+       if err != nil {
+               t.Fatal(err)
+       }
+       port := pc.LocalAddr().(*net.UDPAddr).Port
+
+       go func() {
+               buf := make([]byte, udpMaxPacketSize)
+               for {
+                       n, addr, err := pc.ReadFrom(buf)
+                       if err != nil {
+                               return
+                       }
+                       if n >= udpHeaderSize {
+                               magic := binary.BigEndian.Uint32(buf[0:4])
+                               if magic == udpMagic {
+                                       pc.WriteTo(buf[:n], addr)
+                                       pc.WriteTo(buf[:n], addr) // duplicate
+                               }
+                       }
+               }
+       }()
+
+       return port, func() { pc.Close() }
+}
+
+// startReorderingEchoListener buffers two packets at a time and sends
+// them back in reverse order, producing out-of-order replies.
+func startReorderingEchoListener(t *testing.T) (int, func()) {
+       t.Helper()
+       pc, err := net.ListenPacket("udp", "127.0.0.1:0")
+       if err != nil {
+               t.Fatal(err)
+       }
+       port := pc.LocalAddr().(*net.UDPAddr).Port
+
+       go func() {
+               buf1 := make([]byte, udpMaxPacketSize)
+               buf2 := make([]byte, udpMaxPacketSize)
+               for {
+                       // Read first packet
+                       n1, addr1, err := pc.ReadFrom(buf1)
+                       if err != nil {
+                               return
+                       }
+                       // Read second packet
+                       n2, addr2, err := pc.ReadFrom(buf2)
+                       if err != nil {
+                               // Got one but not two — send the first anyway
+                               if n1 >= udpHeaderSize && 
binary.BigEndian.Uint32(buf1[0:4]) == udpMagic {
+                                       pc.WriteTo(buf1[:n1], addr1)
+                               }
+                               return
+                       }
+                       // Send them in reverse order
+                       if n2 >= udpHeaderSize && 
binary.BigEndian.Uint32(buf2[0:4]) == udpMagic {
+                               pc.WriteTo(buf2[:n2], addr2)
+                       }
+                       if n1 >= udpHeaderSize && 
binary.BigEndian.Uint32(buf1[0:4]) == udpMagic {
+                               pc.WriteTo(buf1[:n1], addr1)
+                       }
+               }
+       }()
+
+       return port, func() { pc.Close() }
+}
+
 func TestProbeUDP_NoLoss(t *testing.T) {
        port, cleanup := startTestEchoListener(t)
        defer cleanup()
@@ -159,6 +229,40 @@
        }
 }
 
+func TestProbeUDP_Duplicates(t *testing.T) {
+       port, cleanup := startDuplicatingEchoListener(t)
+       defer cleanup()
+
+       result := ProbeUDP("127.0.0.1", port, 5, 64, 2*time.Second)
+       if result.Err != nil {
+               t.Fatalf("unexpected error: %v", result.Err)
+       }
+       if result.LossPct != 0 {
+               t.Errorf("expected 0%% loss, got %.1f%%", result.LossPct)
+       }
+       if result.Duplicates == 0 {
+               t.Error("expected duplicates > 0, got 0")
+       }
+       t.Logf("duplicates detected: %d", result.Duplicates)
+}
+
+func TestProbeUDP_OutOfOrder(t *testing.T) {
+       port, cleanup := startReorderingEchoListener(t)
+       defer cleanup()
+
+       result := ProbeUDP("127.0.0.1", port, 10, 64, 2*time.Second)
+       if result.Err != nil {
+               t.Fatalf("unexpected error: %v", result.Err)
+       }
+       if result.LossPct != 0 {
+               t.Errorf("expected 0%% loss, got %.1f%%", result.LossPct)
+       }
+       if result.OutOfOrder == 0 {
+               t.Error("expected out-of-order > 0, got 0")
+       }
+       t.Logf("out-of-order detected: %d, duplicates: %d", result.OutOfOrder, 
result.Duplicates)
+}
+
 func TestEstimateHops(t *testing.T) {
        tests := []struct {
                ttl  int

++++++ goldpinger.obsinfo ++++++
--- /var/tmp/diff_new_pack.JXFGmY/_old  2026-04-21 12:43:50.916004198 +0200
+++ /var/tmp/diff_new_pack.JXFGmY/_new  2026-04-21 12:43:50.920004364 +0200
@@ -1,5 +1,5 @@
 name: goldpinger
-version: 3.11.0
-mtime: 1775235871
-commit: 7935a11f9de2cbfb0d2b7cbc0348d9bc6b39b53e
+version: 3.11.1
+mtime: 1776693802
+commit: fdb70968b886d341de2ea9300058ab308186717f
 

++++++ vendor.tar.gz ++++++
/work/SRC/openSUSE:Factory/goldpinger/vendor.tar.gz 
/work/SRC/openSUSE:Factory/.goldpinger.new.11940/vendor.tar.gz differ: char 14, 
line 1

Reply via email to