From a8bd4a5f169ed8468e4ffbec9b7639817828e254 Mon Sep 17 00:00:00 2001 From: ginuerzh Date: Mon, 13 Jul 2026 16:42:14 +0800 Subject: [PATCH] test(e2e): consolidate selector tests, add round-robin/fifo/backup failover Merge parallel_selector_test.go into selector_test.go and add e2e coverage for the round-robin + fail filter, fifo sticky fallback, and backup filter strategies. Each test drives requests through a hop with one dead node and asserts the selector marks and skips it so traffic converges on the live node. Co-Authored-By: Claude --- tests/e2e/README.md | 2 +- tests/e2e/parallel_selector_test.go | 57 --------- tests/e2e/selector_test.go | 114 ++++++++++++++++++ tests/e2e/testdata/selector/backup.yaml | 37 ++++++ tests/e2e/testdata/selector/fifo.yaml | 34 ++++++ .../server.yaml => selector/parallel.yaml} | 0 tests/e2e/testdata/selector/roundrobin.yaml | 34 ++++++ 7 files changed, 220 insertions(+), 58 deletions(-) delete mode 100644 tests/e2e/parallel_selector_test.go create mode 100644 tests/e2e/selector_test.go create mode 100644 tests/e2e/testdata/selector/backup.yaml create mode 100644 tests/e2e/testdata/selector/fifo.yaml rename tests/e2e/testdata/{parallel_selector/server.yaml => selector/parallel.yaml} (100%) create mode 100644 tests/e2e/testdata/selector/roundrobin.yaml diff --git a/tests/e2e/README.md b/tests/e2e/README.md index 81329d0..f3172ee 100644 --- a/tests/e2e/README.md +++ b/tests/e2e/README.md @@ -35,7 +35,7 @@ tests/e2e/ │ └── udp_echo.py # UDP echo server (reflects payloads) ├── testdata/ # config files or data files for running cases ├── shadowsocks_test.go # Shadowsocks protocol tests -└── parallel_selector_test.go # Parallel node selector tests +└── selector_test.go # Node selector tests (round-robin, fifo, backup, parallel) ``` ### How it works diff --git a/tests/e2e/parallel_selector_test.go b/tests/e2e/parallel_selector_test.go deleted file mode 100644 index e82e0da..0000000 --- a/tests/e2e/parallel_selector_test.go +++ /dev/null @@ -1,57 +0,0 @@ -package e2e - -import ( - "context" - "fmt" - "io" - "testing" - - "github.com/stretchr/testify/suite" - "github.com/testcontainers/testcontainers-go" -) - -type ParallelSelectorSuite struct { - suite.Suite - ctx context.Context - echoC testcontainers.Container - echoIP string -} - -func (s *ParallelSelectorSuite) SetupSuite() { - s.ctx = context.Background() - - echoC, err := RunEchoContainer(s.ctx, SharedNetworkName) - s.Require().NoError(err) - s.echoC = echoC - - echoIP, err := echoC.ContainerIP(s.ctx) - s.Require().NoError(err) - s.echoIP = echoIP -} - -func (s *ParallelSelectorSuite) TearDownSuite() { - if s.echoC != nil { - s.echoC.Terminate(s.ctx) - } -} - -func (s *ParallelSelectorSuite) TestParallelSelector() { - gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName, "testdata/parallel_selector/server.yaml", "8080/tcp") - s.Require().NoError(err) - defer gostC.Terminate(s.ctx) - - // Test the proxy by running curl inside the gost container - cmd := []string{"curl", "-v", "-s", "-x", "http://127.0.0.1:8080", fmt.Sprintf("http://%s:5678", s.echoIP)} - code, out, err := gostC.Exec(s.ctx, cmd) - s.Require().NoError(err) - - body, err := io.ReadAll(out) - s.Require().NoError(err) - s.Require().Equal(0, code) - - s.Require().Contains(string(body), "hello-gost") -} - -func TestParallelSelectorSuite(t *testing.T) { - suite.Run(t, new(ParallelSelectorSuite)) -} diff --git a/tests/e2e/selector_test.go b/tests/e2e/selector_test.go new file mode 100644 index 0000000..6595c24 --- /dev/null +++ b/tests/e2e/selector_test.go @@ -0,0 +1,114 @@ +package e2e + +import ( + "context" + "fmt" + "io" + "strings" + "testing" + + "github.com/stretchr/testify/suite" + "github.com/testcontainers/testcontainers-go" +) + +type SelectorSuite struct { + suite.Suite + ctx context.Context + echoC testcontainers.Container + echoIP string +} + +func (s *SelectorSuite) SetupSuite() { + s.ctx = context.Background() + + echoC, err := RunEchoContainer(s.ctx, SharedNetworkName) + s.Require().NoError(err) + s.echoC = echoC + + echoIP, err := echoC.ContainerIP(s.ctx) + s.Require().NoError(err) + s.echoIP = echoIP +} + +func (s *SelectorSuite) TearDownSuite() { + if s.echoC != nil { + s.echoC.Terminate(s.ctx) + } +} + +// proxyRequest sends one request through the gost proxy and returns the curl +// exit code and response body. +func (s *SelectorSuite) proxyRequest(gostC testcontainers.Container, port string) (int, string) { + cmd := []string{ + "curl", "-s", + "-x", fmt.Sprintf("http://127.0.0.1:%s", port), + fmt.Sprintf("http://%s:5678", s.echoIP), + } + code, out, err := gostC.Exec(s.ctx, cmd) + s.Require().NoError(err) + + body, err := io.ReadAll(out) + s.Require().NoError(err) + return code, string(body) +} + +// runFailover starts a gost container from cfg and verifies that, despite a +// dead node in the hop, requests converge to the live node after the selector +// marks the dead node and skips it. At most the initial marking attempt may +// fail; every request after must reach the echo server. +func (s *SelectorSuite) runFailover(cfg, port string) { + gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName, cfg, port+"/tcp") + s.Require().NoError(err) + defer gostC.Terminate(s.ctx) + + const n = 12 + var failures int + lastOK := false + for range n { + code, body := s.proxyRequest(gostC, port) + ok := code == 0 && strings.Contains(body, "hello-gost") + if !ok { + failures++ + } + lastOK = ok + } + + s.Require().LessOrEqual(failures, 1, + "selector did not filter the dead node; too many requests failed") + s.Require().True(lastOK, "requests did not converge to the live node") +} + +// TestRoundRobinFailover exercises the round-robin strategy combined with the +// fail filter: the dead node is tried once, marked, and then skipped. +func (s *SelectorSuite) TestRoundRobinFailover() { + s.runFailover("testdata/selector/roundrobin.yaml", "8080") +} + +// TestFIFOFailover exercises the fifo (sticky) strategy: it always picks the +// first node until it fails, then falls through to the secondary node. +func (s *SelectorSuite) TestFIFOFailover() { + s.runFailover("testdata/selector/fifo.yaml", "8080") +} + +// TestBackupFailover exercises the backup filter: with the primary node dead, +// the selector falls back to the backup node. +func (s *SelectorSuite) TestBackupFailover() { + s.runFailover("testdata/selector/backup.yaml", "8080") +} + +// TestParallelSelector exercises the parallel strategy: it dials all nodes +// concurrently and uses the first that connects, so a dead node never blocks +// the request. +func (s *SelectorSuite) TestParallelSelector() { + gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName, "testdata/selector/parallel.yaml", "8080/tcp") + s.Require().NoError(err) + defer gostC.Terminate(s.ctx) + + code, body := s.proxyRequest(gostC, "8080") + s.Require().Equal(0, code) + s.Require().Contains(body, "hello-gost") +} + +func TestSelectorSuite(t *testing.T) { + suite.Run(t, new(SelectorSuite)) +} diff --git a/tests/e2e/testdata/selector/backup.yaml b/tests/e2e/testdata/selector/backup.yaml new file mode 100644 index 0000000..29c8aa3 --- /dev/null +++ b/tests/e2e/testdata/selector/backup.yaml @@ -0,0 +1,37 @@ +services: +- name: proxy + addr: :8080 + handler: + type: http + chain: my-chain + listener: + type: tcp + +# Live relay: forwards the request to the echo server (curl target). +- name: relay + addr: 127.0.0.1:18081 + handler: + type: http + listener: + type: tcp + +chains: +- name: my-chain + hops: + - name: hop-1 + selector: + strategy: round + maxFails: 1 + nodes: + # primary: dead, non-backup. + - name: node-1 + addr: 127.0.0.1:18082 + connector: + type: http + # backup: only used once all primary nodes are dead. + - name: node-2 + addr: 127.0.0.1:18081 + connector: + type: http + metadata: + backup: true diff --git a/tests/e2e/testdata/selector/fifo.yaml b/tests/e2e/testdata/selector/fifo.yaml new file mode 100644 index 0000000..d1b0be5 --- /dev/null +++ b/tests/e2e/testdata/selector/fifo.yaml @@ -0,0 +1,34 @@ +services: +- name: proxy + addr: :8080 + handler: + type: http + chain: my-chain + listener: + type: tcp + +# Live relay: forwards the request to the echo server (curl target). +- name: relay + addr: 127.0.0.1:18081 + handler: + type: http + listener: + type: tcp + +chains: +- name: my-chain + hops: + - name: hop-1 + selector: + strategy: fifo + maxFails: 1 + nodes: + # primary: dead, so fifo must fall through to the secondary node. + - name: node-1 + addr: 127.0.0.1:18082 + connector: + type: http + - name: node-2 + addr: 127.0.0.1:18081 + connector: + type: http diff --git a/tests/e2e/testdata/parallel_selector/server.yaml b/tests/e2e/testdata/selector/parallel.yaml similarity index 100% rename from tests/e2e/testdata/parallel_selector/server.yaml rename to tests/e2e/testdata/selector/parallel.yaml diff --git a/tests/e2e/testdata/selector/roundrobin.yaml b/tests/e2e/testdata/selector/roundrobin.yaml new file mode 100644 index 0000000..09b9753 --- /dev/null +++ b/tests/e2e/testdata/selector/roundrobin.yaml @@ -0,0 +1,34 @@ +services: +- name: proxy + addr: :8080 + handler: + type: http + chain: my-chain + listener: + type: tcp + +# Live relay: forwards the request to the echo server (curl target). +- name: relay + addr: 127.0.0.1:18081 + handler: + type: http + listener: + type: tcp + +chains: +- name: my-chain + hops: + - name: hop-1 + selector: + strategy: round + maxFails: 1 + nodes: + - name: node-1 + addr: 127.0.0.1:18081 + connector: + type: http + # dead node: never listens; must be marked and skipped by the selector. + - name: node-2 + addr: 127.0.0.1:18082 + connector: + type: http