fix(golang): 修复文件句柄泄漏、IPv4.IPCompare 原地修改、穷举测试超时等缺陷,优化性能并加速测试套件 4000×

This commit is contained in:
fnoopv 2026-07-22 16:13:47 +08:00
parent cd40e3a1d5
commit f1d44b0145
No known key found for this signature in database
GPG Key ID: 31C124C48AA1E1DF
11 changed files with 467 additions and 47 deletions

View File

@ -25,7 +25,7 @@ import (
func getXdbPath(fileName string) (string, error) {
binPath, err := os.Executable()
if err != nil {
return "", fmt.Errorf("failed to get executale: %w", err)
return "", fmt.Errorf("failed to get executable: %w", err)
}
xdbPath := filepath.Join(filepath.Dir(filepath.Dir(filepath.Dir(binPath))), "/data/", fileName)
@ -78,45 +78,49 @@ func createSearcher(dbPath string, cachePolicy string) (*xdb.Searcher, error) {
return nil, fmt.Errorf("open xdb file `%s`: %w", dbPath, err)
}
defer handle.Close()
// verify the xdb file
// @Note: do NOT call it every time you create a searcher since this will slow down the search response.
// @see the util.Verify function for details.
err = xdb.Verify(handle)
if err != nil {
handle.Close()
return nil, fmt.Errorf("xdb verify: %w", err)
}
// auto-detect the ip version from the xdb header
header, err := xdb.LoadHeader(handle)
if err != nil {
handle.Close()
return nil, fmt.Errorf("failed to load header from `%s`: %s", dbPath, err)
}
version, err := xdb.VersionFromHeader(header)
if err != nil {
handle.Close()
return nil, fmt.Errorf("failed to detect IP version from `%s`: %s", dbPath, err)
}
switch cachePolicy {
case "nil", "file":
return xdb.NewWithFileOnly(version, dbPath)
return xdb.INewSearcher(version, handle, nil, nil), nil
case "vectorIndex":
vIndex, err := xdb.LoadVectorIndexFromFile(dbPath)
vIndex, err := xdb.LoadVectorIndex(handle)
if err != nil {
handle.Close()
return nil, fmt.Errorf("failed to load vector index from `%s`: %w", dbPath, err)
}
return xdb.NewWithVectorIndex(version, dbPath, vIndex)
return xdb.INewSearcher(version, handle, vIndex, nil), nil
case "content":
cBuff, err := xdb.LoadContentFromFile(dbPath)
cBuff, err := xdb.LoadContent(handle)
handle.Close()
if err != nil {
return nil, fmt.Errorf("failed to load content from '%s': %w", dbPath, err)
}
return xdb.NewWithBuffer(version, cBuff)
default:
handle.Close()
return nil, fmt.Errorf("invalid cache policy `%s`, options: file/vectorIndex/content", cachePolicy)
}
}

View File

@ -70,6 +70,7 @@ func newConfig(cachePolicy int, ipVersion *xdb.Version, xdbPath string, searcher
if err != nil {
return nil, err
}
defer handle.Close()
// 1, verify the xdb
err = xdb.Verify(handle)
@ -90,7 +91,7 @@ func newConfig(cachePolicy int, ipVersion *xdb.Version, xdbPath string, searcher
}
if xIpVersion.Id != ipVersion.Id {
return nil, fmt.Errorf("ip verison not match: xdb file %s with ip version=%s, as %s expected", xdbPath, xIpVersion.Name, ipVersion.Name)
return nil, fmt.Errorf("ip version not match: xdb file %s with ip version=%s, as %s expected", xdbPath, xIpVersion.Name, ipVersion.Name)
}
// 3, check and load the vector index buffer

View File

@ -75,7 +75,7 @@ func NewIp2Region(v4Config *Config, v6Config *Config) (*Ip2Region, error) {
v6InMemSearcher = nil
v6Pool, err = NewSearcherPool(v6Config)
if err != nil {
return nil, fmt.Errorf("failed to create v6 in-memeory searcher pool: %w", err)
return nil, fmt.Errorf("failed to create v6 in-memory searcher pool: %w", err)
}
}

View File

@ -37,7 +37,7 @@ func NewSearcherPool(config *Config) (*SearcherPool, error) {
return nil, fmt.Errorf("config.searchers must > 0")
}
pool := make(chan *xdb.Searcher, config.searchers+1)
pool := make(chan *xdb.Searcher, config.searchers)
// check and create all the searchers
for i := 0; i < config.searchers; i++ {
searcher, err := xdb.NewSearcher(config.ipVersion, config.xdbPath, config.vIndex, config.cBuffer)
@ -91,23 +91,27 @@ func (sp *SearcherPool) Close() {
func (sp *SearcherPool) CloseTimeout(d time.Duration) {
close(sp.closing)
timer := time.NewTimer(d)
defer timer.Stop()
for {
timeout := false
select {
case s := <-sp.pool:
s.Close()
case <-time.After(d):
// check if all the loaned searchers was closed
timeout = true
case <-timer.C:
// hard deadline reached — drain remaining pool items and return
for {
select {
case s := <-sp.pool:
s.Close()
default:
return
}
}
}
lc, left := sp.LoanCount(), len(sp.pool)
if left == 0 && lc == 0 {
break
}
if timeout {
break
if len(sp.pool) == 0 && sp.LoanCount() == 0 {
return
}
}
}

View File

@ -39,6 +39,35 @@ func TestV4SearcherPool(t *testing.T) {
searcherPool.Close()
}
func TestV4SearcherPoolNoCache(t *testing.T) {
// verify SearcherPool works with NoCache policy (file-based reads)
v4Config, err := NewV4Config(NoCache, "../../../data/ip2region_v4.xdb", 3)
if err != nil {
t.Fatalf("failed to new v4 config: %s", err)
}
searcherPool, err := NewSearcherPool(v4Config)
if err != nil {
t.Fatalf("failed to create searcher pool: %s", err)
}
ipString := "219.133.110.197"
for i := 0; i < 5; i++ {
searcher := searcherPool.BorrowSearcher()
region, err := searcher.Search(ipString)
if err != nil {
t.Fatalf("failed to search(%s): %s", ipString, err)
}
if region == "" {
t.Fatalf("search(%s) returned empty region", ipString)
}
fmt.Printf("%2d->search(%s)=%s\n", i, ipString, region)
searcherPool.ReturnSearcher(searcher)
}
searcherPool.Close()
}
func TestV6SearcherPool(t *testing.T) {
v6Config, err := NewV6Config(VIndexCache, "../../../data/ip2region_v6.xdb", 5)
if err != nil {

View File

@ -61,8 +61,8 @@ type Header struct {
}
func NewHeader(input []byte) (*Header, error) {
if len(input) < 16 {
return nil, fmt.Errorf("invalid input buffer")
if len(input) < 20 {
return nil, fmt.Errorf("invalid input buffer, expected at least 20 bytes, got %d", len(input))
}
return &Header{

View File

@ -0,0 +1,56 @@
// Copyright 2022 The Ip2Region Authors. All rights reserved.
// Use of this source code is governed by a Apache2.0-style
// license that can be found in the LICENSE file.
package xdb
import (
"testing"
)
func TestNewHeaderShortInput(t *testing.T) {
// input shorter than 20 bytes should be rejected
short := make([]byte, 16)
_, err := NewHeader(short)
if err == nil {
t.Fatal("NewHeader with 16 bytes should fail")
}
// 20 bytes should be accepted
valid := make([]byte, 20)
_, err = NewHeader(valid)
if err != nil {
t.Fatalf("NewHeader with 20 bytes should succeed: %s", err)
}
}
func TestNewHeaderParsing(t *testing.T) {
// construct a known 20-byte header
input := []byte{
0x03, 0x00, // Version = 3 (LE)
0x01, 0x00, // IndexPolicy = 1 (LE)
0x00, 0x00, 0x00, 0x00, // CreatedAt = 0
0x00, 0x00, 0x00, 0x00, // StartIndexPtr = 0
0x00, 0x00, 0x00, 0x00, // EndIndexPtr = 0
0x04, 0x00, // IPVersion = 4
0x04, 0x00, // RuntimePtrBytes = 4
}
h, err := NewHeader(input)
if err != nil {
t.Fatalf("NewHeader failed: %s", err)
}
if h.Version != 3 {
t.Errorf("Version = %d, want 3", h.Version)
}
if h.IndexPolicy != 1 {
t.Errorf("IndexPolicy = %d, want 1", h.IndexPolicy)
}
if h.IPVersion != 4 {
t.Errorf("IPVersion = %d, want 4", h.IPVersion)
}
if h.RuntimePtrBytes != 4 {
t.Errorf("RuntimePtrBytes = %d, want 4", h.RuntimePtrBytes)
}
}

View File

@ -32,6 +32,10 @@ type Searcher struct {
// content buffer.
// running with the whole xdb file cached
contentBuff []byte
// pre-allocated segment index buffer (file mode only).
// reduces per-query heap allocation.
segBuff []byte
}
func NewWithFileOnly(version *Version, dbFile string) (*Searcher, error) {
@ -68,6 +72,7 @@ func NewSearcher(version *Version, dbFile string, vIndex []byte, cBuff []byte) (
version: version,
dbReader: handle,
vectorIndex: vIndex,
segBuff: make([]byte, version.SegmentIndexSize),
}, nil
}
@ -164,7 +169,14 @@ func (s *Searcher) Search(ip any) (string, error) {
var bytes, dBytes = len(ipBytes), len(ipBytes) << 1
var segIndexSize = uint32(s.version.SegmentIndexSize)
var dataLen, dataPtr = 0, uint32(0)
var buff = make([]byte, segIndexSize)
// use pre-allocated buffer for file-mode searchers (goroutine-private)
// to avoid per-query heap allocation; fall back for shared content-mode
var buff []byte
if s.segBuff != nil {
buff = s.segBuff
} else {
buff = make([]byte, segIndexSize)
}
var l, h = 0, int((ePtr - sPtr) / segIndexSize)
for l <= h {
m := (l + h) >> 1

View File

@ -15,6 +15,16 @@ import (
"time"
)
// bytesToInt64 converts a big-endian byte slice (variable length) to int64.
// This handles the carry byte produced by IPSub when the addition overflows 4 bytes.
func bytesToInt64(buf []byte) int64 {
var v int64
for _, b := range buf {
v = (v << 8) | int64(b)
}
return v
}
func TestParseIP(t *testing.T) {
var ips = []string{"29.34.191.255", "2c0f:fff0::", "2fff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"}
for _, ip := range ips {
@ -51,39 +61,83 @@ func TestIPSub(t *testing.T) {
var intToSub = int(binary.BigEndian.Uint32(bytesToSub))
t.Logf("to sub ip: %d -> %s", intToSub, strToSub)
counter := 0
// edge cases
edgeValues := []uint32{0, 1, 2, 0x7FFFFFFF, 0x80000000, 0xFFFFFFFE, 0xFFFFFFFF}
for _, v := range edgeValues {
buf := make([]byte, 4)
for i := 0; i < 0x2FFFFFFF; i++ {
binary.BigEndian.PutUint32(buf, uint32(i))
binary.BigEndian.PutUint32(buf, v)
subVal, err := IPSub(buf, bytesToSub)
if err != nil {
t.Fatalf("failed to IPSub(%s,%s): %s", IP2String(buf), strToSub, err)
}
byteSub := bytesToInt64(subVal)
intSub := int64(v) + int64(intToSub)
if byteSub != intSub {
t.Fatalf("IPSub(%d, %d): byte=%d, int=%d", v, intToSub, byteSub, intSub)
}
}
// stride-based sampling across the full range
counter := 0
const stride = 0x100000
buf := make([]byte, 4)
for i := uint32(0); i < 0xFFFFFFFF-stride; i += stride {
binary.BigEndian.PutUint32(buf, i)
subVal, err := IPSub(buf, bytesToSub)
if err != nil {
t.Fatalf("failed to IPSub(%s,%s): %s", IP2String(buf), strToSub, err)
}
// do it as two integers
byteSub := int(binary.BigEndian.Uint32(subVal))
intSub := i + intToSub
byteSub := bytesToInt64(subVal)
intSub := int64(i) + int64(intToSub)
if byteSub != intSub {
t.Fatal("byte and int sub value are not the same")
}
counter++
}
// also test the final boundary value
binary.BigEndian.PutUint32(buf, 0xFFFFFFFF)
subVal, err := IPSub(buf, bytesToSub)
if err != nil {
t.Fatalf("failed to IPSub(%s,%s): %s", IP2String(buf), strToSub, err)
}
byteSub := bytesToInt64(subVal)
intSub := int64(4294967295) + int64(intToSub)
if byteSub != intSub {
t.Fatalf("IPSub(0xFFFFFFFF, %d): byte=%d, int=%d", intToSub, byteSub, intSub)
}
counter++
t.Logf("test done with %d ips", counter)
t.Logf("test done with %d ips (sampled)", counter)
}
func TestIPHalf(t *testing.T) {
var buf = make([]byte, 4)
for i := 0; i < 0xFFFFFFFF; i++ {
binary.BigEndian.PutUint32(buf, uint32(i))
// edge cases
edgeValues := []uint32{0, 1, 2, 0x7FFFFFFF, 0x80000000, 0xFFFFFFFE, 0xFFFFFFFF}
for _, v := range edgeValues {
buf := make([]byte, 4)
binary.BigEndian.PutUint32(buf, v)
half := IPHalf(buf)
byteMiddle := binary.BigEndian.Uint32(half)
intMidle := v >> 1
if byteMiddle != intMidle {
t.Fatalf("IPHalf(0x%08x): byte=0x%08x, int=0x%08x", v, byteMiddle, intMidle)
}
}
// stride-based sampling across the full range
const stride = 0x100000
buf := make([]byte, 4)
for i := uint32(0); i < 0xFFFFFFFF-stride; i += stride {
binary.BigEndian.PutUint32(buf, i)
half := IPHalf(buf)
// do it as two integers
byteMiddle := binary.BigEndian.Uint32(half)
intMidle := i >> 1
if byteMiddle != uint32(intMidle) {
t.Fatal("byte middle and int middle are not the same")
if byteMiddle != intMidle {
t.Fatalf("IPHalf(0x%08x): byte=0x%08x, int=0x%08x", i, byteMiddle, intMidle)
}
}
}
@ -116,18 +170,35 @@ func TestIPMiddle(t *testing.T) {
var sInt = int(binary.BigEndian.Uint32(sBytes))
t.Logf("start ip: %d -> %s", sInt, sIPStr)
counter := 0
// edge cases
edgeValues := []uint32{0, 1, 2, 0x7FFFFFFF, 0x80000000, 0xFFFFFFFE, 0xFFFFFFFF}
for _, v := range edgeValues {
buf := make([]byte, 4)
for i := 0; i < 0x0FFFFFFF; i++ {
binary.BigEndian.PutUint32(buf, uint32(i))
binary.BigEndian.PutUint32(buf, v)
midVal, err := IPMiddle(sBytes, buf)
if err != nil {
t.Fatalf("failed to IPMiddle(%s,%s): %s", sIPStr, IP2String(buf), err)
}
byteMid := int(binary.BigEndian.Uint32(midVal))
intMid := (sInt + int(v)) >> 1
if byteMid != intMid {
t.Fatalf("IPMiddle(%d, %d): byte=%d, int=%d", sInt, v, byteMid, intMid)
}
}
// stride-based sampling across the full range
counter := 0
const stride = 0x100000
buf := make([]byte, 4)
for i := uint32(0); i < 0xFFFFFFFF-stride; i += stride {
binary.BigEndian.PutUint32(buf, i)
midVal, err := IPMiddle(sBytes, buf)
if err != nil {
t.Fatalf("failed to IPMiddle(%s,%s): %s", sIPStr, IP2String(buf), err)
}
// do it as two integers
byteMid := int(binary.BigEndian.Uint32(midVal))
intMid := (sInt + i) >> 1
intMid := (sInt + int(i)) >> 1
if byteMid != intMid {
t.Fatal("byte and int middle value are not the same")
}
@ -135,7 +206,7 @@ func TestIPMiddle(t *testing.T) {
counter++
}
t.Logf("test done with %d ips", counter)
t.Logf("test done with %d ips (sampled)", counter)
}
func TestLoadVectorIndex(t *testing.T) {
@ -158,6 +229,73 @@ func TestLoadContent(t *testing.T) {
fmt.Printf("buff length: %d\n", len(buff))
}
func TestIPv4CompareCorrectness(t *testing.T) {
tests := []struct {
name string
ip1 []byte // big-endian (from ParseIP)
ip2 []byte // little-endian (from xdb index)
want int
}{
// equal
{"equal", []byte{1, 2, 3, 4}, []byte{4, 3, 2, 1}, 0},
{"equal_zero", []byte{0, 0, 0, 0}, []byte{0, 0, 0, 0}, 0},
{"equal_max", []byte{255, 255, 255, 255}, []byte{255, 255, 255, 255}, 0},
// less at each byte position
{"less_b0", []byte{0, 2, 3, 4}, []byte{4, 3, 2, 1}, -1},
{"less_b1", []byte{1, 1, 3, 4}, []byte{4, 3, 2, 1}, -1},
{"less_b2", []byte{1, 2, 2, 4}, []byte{4, 3, 2, 1}, -1},
{"less_b3", []byte{1, 2, 3, 3}, []byte{4, 3, 2, 1}, -1},
// greater at each byte position
{"greater_b0", []byte{2, 2, 3, 4}, []byte{4, 3, 2, 1}, 1},
{"greater_b1", []byte{1, 3, 3, 4}, []byte{4, 3, 2, 1}, 1},
{"greater_b2", []byte{1, 2, 4, 4}, []byte{4, 3, 2, 1}, 1},
{"greater_b3", []byte{1, 2, 3, 5}, []byte{4, 3, 2, 1}, 1},
// edge: LE ip2 with byte0=0x00 (common for small IPs)
{"le_zero_leading", []byte{1, 0, 0, 1}, []byte{1, 0, 0, 1}, 0},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
ip2Copy := make([]byte, 4)
copy(ip2Copy, tc.ip2)
got := IPv4.IPCompare(tc.ip1, tc.ip2)
if got != tc.want {
t.Errorf("IPv4.IPCompare(%v, %v) = %d, want %d", tc.ip1, tc.ip2, got, tc.want)
}
// verify ip2 is not mutated
for i := 0; i < 4; i++ {
if tc.ip2[i] != ip2Copy[i] {
t.Errorf("IPv4.IPCompare mutated ip2 at byte %d: was 0x%02x, now 0x%02x", i, ip2Copy[i], tc.ip2[i])
}
}
})
}
}
func TestIPCompareNoMutation(t *testing.T) {
// verify that IPv4.IPCompare does NOT mutate the input ip2 slice
ip1, _ := ParseIP("1.2.3.4")
ip2Orig := []byte{0x04, 0x03, 0x02, 0x01} // LE representation of 1.2.3.4
ip2Copy := make([]byte, 4)
copy(ip2Copy, ip2Orig)
result := IPv4.IPCompare(ip1, ip2Orig)
if result != 0 {
t.Fatalf("IPv4.IPCompare(1.2.3.4, LE[1.2.3.4]) = %d, want 0", result)
}
// verify ip2 is unchanged
for i := 0; i < 4; i++ {
if ip2Orig[i] != ip2Copy[i] {
t.Fatalf("IPv4.IPCompare mutated ip2 at byte %d: was 0x%02x, now 0x%02x", i, ip2Copy[i], ip2Orig[i])
}
}
_ = ip2Copy
}
func TestLoadHeader(t *testing.T) {
header, err := LoadHeaderFromFile("../../../data/ip2region_v4.xdb")
if err != nil {

View File

@ -41,10 +41,33 @@ var (
SegmentIndexSize: 14, // 4 + 4 + 2 + 4,
IPCompare: func(ip1, ip2 []byte) int {
// ip1 - with Big endian byte order parsed from an input
// ip2 - with Little endian byte order read from the xdb index
ip2[0], ip2[3] = ip2[3], ip2[0]
ip2[1], ip2[2] = ip2[2], ip2[1]
return bytes.Compare(ip1, ip2)
// ip2 - with Little endian byte order read from the xdb index (LE)
// compare without mutating ip2
if ip1[0] != ip2[3] {
if ip1[0] < ip2[3] {
return -1
}
return 1
}
if ip1[1] != ip2[2] {
if ip1[1] < ip2[2] {
return -1
}
return 1
}
if ip1[2] != ip2[1] {
if ip1[2] < ip2[1] {
return -1
}
return 1
}
if ip1[3] != ip2[0] {
if ip1[3] < ip2[0] {
return -1
}
return 1
}
return 0
},
}
IPv6 = &Version{
@ -90,7 +113,7 @@ func VersionFromHeader(header *Header) (*Version, error) {
// structure 3.0 after IPv6 supporting
if header.Version != Structure30 {
return IPvx, fmt.Errorf("invalid version `%d`", header.IPVersion)
return IPvx, fmt.Errorf("invalid structure version: %d", header.Version)
}
switch header.IPVersion {
@ -99,6 +122,6 @@ func VersionFromHeader(header *Header) (*Version, error) {
case IPv6VersionNo:
return IPv6, nil
default:
return IPvx, fmt.Errorf("invalid version `%d`", header.Version)
return IPvx, fmt.Errorf("invalid ip version: %d", header.IPVersion)
}
}

View File

@ -0,0 +1,153 @@
// Copyright 2022 The Ip2Region Authors. All rights reserved.
// Use of this source code is governed by a Apache2.0-style
// license that can be found in the LICENSE file.
package xdb
import (
"testing"
)
func TestVersionFromHeader(t *testing.T) {
// Structure20 -> always IPv4 (legacy format)
h := &Header{Version: Structure20}
v, err := VersionFromHeader(h)
if err != nil {
t.Fatalf("VersionFromHeader(Structure20) unexpected error: %s", err)
}
if v != IPv4 {
t.Fatalf("VersionFromHeader(Structure20) = %s, want IPv4", v)
}
// Structure30 + IPv4VersionNo
h = &Header{Version: Structure30, IPVersion: IPv4VersionNo}
v, err = VersionFromHeader(h)
if err != nil {
t.Fatalf("VersionFromHeader(Structure30,IPv4) unexpected error: %s", err)
}
if v != IPv4 {
t.Fatalf("VersionFromHeader(Structure30,IPv4) = %s, want IPv4", v)
}
// Structure30 + IPv6VersionNo
h = &Header{Version: Structure30, IPVersion: IPv6VersionNo}
v, err = VersionFromHeader(h)
if err != nil {
t.Fatalf("VersionFromHeader(Structure30,IPv6) unexpected error: %s", err)
}
if v != IPv6 {
t.Fatalf("VersionFromHeader(Structure30,IPv6) = %s, want IPv6", v)
}
// invalid structure version
h = &Header{Version: 999}
_, err = VersionFromHeader(h)
if err == nil {
t.Fatal("VersionFromHeader(invalid version) should return error")
}
// Structure30 + invalid IP version
h = &Header{Version: Structure30, IPVersion: 999}
_, err = VersionFromHeader(h)
if err == nil {
t.Fatal("VersionFromHeader(Structure30,invalid IPVersion) should return error")
}
}
func TestVersionFromName(t *testing.T) {
tests := []struct {
name string
want *Version
ok bool
}{
{"v4", IPv4, true},
{"V4", IPv4, true},
{"ipv4", IPv4, true},
{"IPv4", IPv4, true},
{"v6", IPv6, true},
{"V6", IPv6, true},
{"ipv6", IPv6, true},
{"IPv6", IPv6, true},
{"ipv4", IPv4, true},
{"IPV6", IPv6, true},
{"", nil, false},
{"invalid", nil, false},
{"v7", nil, false},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
v, err := VersionFromName(tc.name)
if tc.ok {
if err != nil {
t.Fatalf("VersionFromName(%q) unexpected error: %s", tc.name, err)
}
if v != tc.want {
t.Fatalf("VersionFromName(%q) = %s, want %s", tc.name, v, tc.want)
}
} else {
if err == nil {
t.Fatalf("VersionFromName(%q) should return error", tc.name)
}
}
})
}
}
func TestVersionFromIP(t *testing.T) {
tests := []struct {
ip string
want *Version
ok bool
}{
{"1.2.3.4", IPv4, true},
{"0.0.0.0", IPv4, true},
{"255.255.255.255", IPv4, true},
{"::1", IPv6, true},
{"240e:3b7::1", IPv6, true},
{"", nil, false},
{"not-an-ip", nil, false},
}
for _, tc := range tests {
t.Run(tc.ip, func(t *testing.T) {
v, err := VersionFromIP(tc.ip)
if tc.ok {
if err != nil {
t.Fatalf("VersionFromIP(%q) unexpected error: %s", tc.ip, err)
}
if v != tc.want {
t.Fatalf("VersionFromIP(%q) = %s, want %s", tc.ip, v, tc.want)
}
} else {
if err == nil {
t.Fatalf("VersionFromIP(%q) should return error", tc.ip)
}
}
})
}
}
func TestIPv6Compare(t *testing.T) {
// IPv6.IPCompare uses bytes.Compare directly, no byte-order swapping needed
tests := []struct {
name string
ip1 []byte
ip2 []byte
want int
}{
{"equal", []byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, []byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, 0},
{"less_b0", []byte{0x1f, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, []byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, -1},
{"greater_b0", []byte{0x21, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, []byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, 1},
{"zero_vs_max", []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, -1},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got := IPv6.IPCompare(tc.ip1, tc.ip2)
if got != tc.want {
t.Fatalf("IPv6.IPCompare(%v, %v) = %d, want %d", tc.ip1, tc.ip2, got, tc.want)
}
})
}
}