Replaces apiserver's hand-written Go parsePeer() with a CGO wrapper around the shared C parser+validator pair (xce_parse_peers then xce_cluster_config_validate, both src/lib/cluster.c) that worker and scheduler already use from their own main.c's. Gives apiserver the same [name=]host:port[,...] syntax (including the name= prefix the old Go parser could not express) and identical malformed-candidate rejection (empty host, port <= 0, non-numeric port) -- round-1 review caught the first pass skipping the validate() half, silently accepting inputs worker/scheduler both reject. A malformed entry now rejects the whole candidate list rather than being skipped with a warning (matches xce_parse_peers()'s own all-or-nothing construction and worker/scheduler's precedent) -- surfaced loudly via RegisterToBackend's error, not silently. Parsing only -- RegisterToBackend()'s connect loop and tryConnect() are unchanged; collapsing them into a single connect call is task 4.2's job. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NNB4zGPCsHyDUCDJyMbiQN
140 lines
5 KiB
Go
140 lines
5 KiB
Go
//go:build cgo
|
|
|
|
package backend
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// candidates_test.go exercises parseCandidates (candidates.go), the CGO
|
|
// wrapper around the shared C parser xce_parse_peers() that replaces
|
|
// apiserver's retired hand-written parsePeer() (Req 6.1, 6.2). Kept
|
|
// //go:build cgo since parseCandidates only exists in the CGO build --
|
|
// this file itself does not `import "C"` (a _test.go file can never do
|
|
// that directly, golang/go#13938): it only calls the plain-Go-signature
|
|
// parseCandidates defined in candidates.go, same split already used by
|
|
// wire_dispatch_test.go/wire_dispatch_support.go in this package.
|
|
|
|
func TestParseCandidates_ValidHostPort(t *testing.T) {
|
|
got, err := parseCandidates("ctrl01:9990")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
want := []Candidate{{Name: "", Host: "ctrl01", Port: 9990}}
|
|
if len(got) != len(want) || got[0] != want[0] {
|
|
t.Errorf("got %+v, want %+v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestParseCandidates_NamePrefixSyntax(t *testing.T) {
|
|
// The old hand-written parsePeer() could not parse this syntax at all
|
|
// -- this is the format-parity case that proves real parity with
|
|
// worker/scheduler's --peers (Req 6.1).
|
|
got, err := parseCandidates("ctrl2=host2:9991")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
want := []Candidate{{Name: "ctrl2", Host: "host2", Port: 9991}}
|
|
if len(got) != len(want) || got[0] != want[0] {
|
|
t.Errorf("got %+v, want %+v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestParseCandidates_MultipleCommaJoined(t *testing.T) {
|
|
got, err := parseCandidates("ctrl01:9990,ctrl2=host2:9991,ctrl03:9992")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
want := []Candidate{
|
|
{Name: "", Host: "ctrl01", Port: 9990},
|
|
{Name: "ctrl2", Host: "host2", Port: 9991},
|
|
{Name: "", Host: "ctrl03", Port: 9992},
|
|
}
|
|
if len(got) != len(want) {
|
|
t.Fatalf("got %d candidates, want %d: %+v", len(got), len(want), got)
|
|
}
|
|
for i := range want {
|
|
if got[i] != want[i] {
|
|
t.Errorf("candidate[%d]: got %+v, want %+v", i, got[i], want[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseCandidates_MalformedEntry_MissingColon(t *testing.T) {
|
|
_, err := parseCandidates("ctrl01")
|
|
if err == nil {
|
|
t.Fatal("expected error for missing colon, got nil")
|
|
}
|
|
// Req 6.2: reject identifying the malformed entry, consistent with how
|
|
// worker/scheduler report it -- since xce_parse_peers() itself cannot
|
|
// identify which token was malformed (same limit worker/scheduler's own
|
|
// --peers parsing has), the whole attempted string is echoed back.
|
|
if !strings.Contains(err.Error(), "ctrl01") {
|
|
t.Errorf("error %q does not identify the malformed input", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestParseCandidates_EmptyString(t *testing.T) {
|
|
_, err := parseCandidates("")
|
|
if err == nil {
|
|
t.Fatal("expected error for empty peers string, got nil")
|
|
}
|
|
}
|
|
|
|
func TestParseCandidates_MalformedEntry_MidList(t *testing.T) {
|
|
// A malformed second entry rejects the WHOLE list -- xce_parse_peers()
|
|
// does not skip just the bad token and keep the rest (Req 6.2, matching
|
|
// worker/scheduler's own all-or-nothing --peers parsing).
|
|
_, err := parseCandidates("ctrl01:9990,badentry,ctrl03:9992")
|
|
if err == nil {
|
|
t.Fatal("expected error for malformed mid-list entry, got nil")
|
|
}
|
|
}
|
|
|
|
func TestParseCandidates_MalformedEntry_ZeroPort(t *testing.T) {
|
|
// xce_parse_peers() alone would happily atoi() "0" into port=0 -- it is
|
|
// xce_cluster_config_validate()'s port<=0 check (src/lib/cluster.c:94)
|
|
// that rejects it, same as worker/scheduler (Req 6.2).
|
|
_, err := parseCandidates("ctrl01:0")
|
|
if err == nil {
|
|
t.Fatal("expected error for port 0, got nil")
|
|
}
|
|
}
|
|
|
|
func TestParseCandidates_MalformedEntry_NonNumericPort(t *testing.T) {
|
|
// atoi("abc") silently returns 0 -- caught by the same port<=0 check as
|
|
// the explicit-zero case above, not by xce_parse_peers() itself.
|
|
_, err := parseCandidates("ctrl01:abc")
|
|
if err == nil {
|
|
t.Fatal("expected error for non-numeric port, got nil")
|
|
}
|
|
}
|
|
|
|
func TestParseCandidates_MalformedEntry_EmptyHost(t *testing.T) {
|
|
// xce_cluster_config_validate()'s hostname[0]=='\0' check
|
|
// (src/lib/cluster.c:92) rejects an empty host, same as worker/scheduler.
|
|
_, err := parseCandidates(":9990")
|
|
if err == nil {
|
|
t.Fatal("expected error for empty host, got nil")
|
|
}
|
|
}
|
|
|
|
func TestParseCandidates_OverCapSilentlyTruncates(t *testing.T) {
|
|
// MAX_HA_CANDIDATES (src/include/config.h) is 4. xce_parse_peers()
|
|
// silently stops reading further tokens once the cap is reached --
|
|
// no error, no warning at this layer (the WARN-on-truncation behavior
|
|
// belongs to xce_cluster_config_to_client_config(), a different
|
|
// function, not exercised by this task). Don't invent stricter
|
|
// behavior than the C function actually has.
|
|
got, err := parseCandidates("c1:1,c2:2,c3:3,c4:4,c5:5")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if len(got) != 4 {
|
|
t.Fatalf("got %d candidates, want 4 (MAX_HA_CANDIDATES cap)", len(got))
|
|
}
|
|
if got[3] != (Candidate{Name: "", Host: "c4", Port: 4}) {
|
|
t.Errorf("candidate[3] = %+v, want the 4th entry (c5 silently dropped)", got[3])
|
|
}
|
|
}
|