Files
uopi/pkg/ca/search_test.go
2026-04-28 00:09:22 +02:00

138 lines
3.8 KiB
Go

package ca
import (
"net"
"testing"
"github.com/uopi/goca/proto"
)
// buildOldSearchReply builds a CA_PROTO_SEARCH UDP reply using the old
// payload format (CA < 4.9): [minor_version(2)][pad(2)][IP(4)].
// When ip is nil, the IP field is 0xFFFFFFFF (use-sender convention).
func buildOldSearchReply(searchID uint32, serverIP net.IP, port int) []byte {
payload := make([]byte, 8)
payload[0] = 0x00
payload[1] = proto.MinorVersion // e.g. 0x0D = 13
// payload[2:4] = 0x00 0x00 (pad)
if serverIP == nil {
payload[4] = 0xFF
payload[5] = 0xFF
payload[6] = 0xFF
payload[7] = 0xFF
} else {
copy(payload[4:], serverIP.To4())
}
h := proto.Header{
Command: proto.CmdSearch,
DataType: uint16(port),
Parameter1: proto.MinorVersion,
Parameter2: searchID,
}
return proto.BuildMessage(h, payload)
}
// TestParseReplyOldFormatAllFF verifies the old format with 0xFFFFFFFF IP.
func TestParseReplyOldFormatAllFF(t *testing.T) {
se := &searchEngine{waiters: make(map[uint32]*searchWaiter)}
const searchID = uint32(42)
result := make(chan searchResult, 1)
se.mu.Lock()
se.waiters[searchID] = &searchWaiter{pvName: "TEST:PV", ch: result}
se.mu.Unlock()
pkt := buildOldSearchReply(searchID, nil, 5064) // nil → 0xFFFFFFFF
src := &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 5064}
se.parseReply(pkt, src)
select {
case r := <-result:
if r.addr != "127.0.0.1:5064" {
t.Errorf("addr = %q, want 127.0.0.1:5064", r.addr)
}
default:
t.Fatal("parseReply did not deliver a result")
}
}
// TestParseReplyOldFormatAllZero verifies the old format with 0.0.0.0 IP
// (used by some EPICS Base versions to also mean "use sender IP").
func TestParseReplyOldFormatAllZero(t *testing.T) {
se := &searchEngine{waiters: make(map[uint32]*searchWaiter)}
const searchID = uint32(43)
result := make(chan searchResult, 1)
se.mu.Lock()
se.waiters[searchID] = &searchWaiter{pvName: "TEST:PV", ch: result}
se.mu.Unlock()
pkt := buildOldSearchReply(searchID, net.IPv4(0, 0, 0, 0).To4(), 5064)
src := &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 5064}
se.parseReply(pkt, src)
select {
case r := <-result:
if r.addr != "127.0.0.1:5064" {
t.Errorf("addr = %q, want 127.0.0.1:5064", r.addr)
}
default:
t.Fatal("parseReply did not deliver a result")
}
}
// TestParseReplyOldFormatExplicitIP verifies that an explicit server IP in
// the old payload format is read from the correct offset (bytes [4:8]).
func TestParseReplyOldFormatExplicitIP(t *testing.T) {
se := &searchEngine{
waiters: make(map[uint32]*searchWaiter),
}
const searchID = uint32(7)
result := make(chan searchResult, 1)
se.mu.Lock()
se.waiters[searchID] = &searchWaiter{pvName: "TEST:PV2", ch: result}
se.mu.Unlock()
explicitIP := net.ParseIP("10.0.0.5").To4()
pkt := buildOldSearchReply(searchID, explicitIP, 5064)
src := &net.UDPAddr{IP: net.ParseIP("10.0.0.5"), Port: 5064}
se.parseReply(pkt, src)
select {
case r := <-result:
if r.addr != "10.0.0.5:5064" {
t.Errorf("addr = %q, want 10.0.0.5:5064", r.addr)
}
default:
t.Fatal("parseReply did not deliver a result")
}
}
// TestParseReplyNewFormat verifies the new payload format (IP at [0:4]).
func TestParseReplyNewFormat(t *testing.T) {
se := &searchEngine{
waiters: make(map[uint32]*searchWaiter),
}
const searchID = uint32(99)
result := make(chan searchResult, 1)
se.mu.Lock()
se.waiters[searchID] = &searchWaiter{pvName: "TEST:PV3", ch: result}
se.mu.Unlock()
// New format: IP = 0xFFFFFFFF at [0:4].
pkt := EncodeSearchReply(searchID, nil, 5064)
src := &net.UDPAddr{IP: net.ParseIP("192.168.1.10"), Port: 5064}
se.parseReply(pkt, src)
select {
case r := <-result:
if r.addr != "192.168.1.10:5064" {
t.Errorf("addr = %q, want 192.168.1.10:5064", r.addr)
}
default:
t.Fatal("parseReply did not deliver a result")
}
}