mirror of https://github.com/go-gost/gost.git
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 <noreply@anthropic.com>master
parent
94dcc9c045
commit
a8bd4a5f16
|
|
@ -35,7 +35,7 @@ tests/e2e/
|
||||||
│ └── udp_echo.py # UDP echo server (reflects payloads)
|
│ └── udp_echo.py # UDP echo server (reflects payloads)
|
||||||
├── testdata/ # config files or data files for running cases
|
├── testdata/ # config files or data files for running cases
|
||||||
├── shadowsocks_test.go # Shadowsocks protocol tests
|
├── 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
|
### How it works
|
||||||
|
|
|
||||||
|
|
@ -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))
|
|
||||||
}
|
|
||||||
|
|
@ -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))
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
Loading…
Reference in New Issue