Skip to content

Commit 646fc70

Browse files
committed
feat: add tests for iperf package
Signed-off-by: nabil salah <nabil.salah203@gmail.com>
1 parent db755da commit 646fc70

File tree

8 files changed

+495
-10
lines changed

8 files changed

+495
-10
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ require (
5353
github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f
5454
github.com/whs/nacl-sealed-box v0.0.0-20180930164530-92b9ba845d8d
5555
github.com/yggdrasil-network/yggdrasil-go v0.4.0
56-
go.uber.org/mock v0.5.0
56+
go.uber.org/mock v0.5.2
5757
golang.org/x/crypto v0.33.0
5858
golang.org/x/sys v0.30.0
5959
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20200609130330-bd2cb7843e1b

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -637,6 +637,8 @@ go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
637637
go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
638638
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
639639
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
640+
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
641+
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
640642
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
641643
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
642644
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=

pkg/perf/iperf/exec_wrapper.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package iperf
2+
3+
import (
4+
"context"
5+
"os/exec"
6+
)
7+
8+
// ExecWrapper wraps the exec package functions to allow for mocking
9+
type ExecWrapper interface {
10+
LookPath(file string) (string, error)
11+
CommandContext(ctx context.Context, name string, arg ...string) ExecCmd
12+
}
13+
14+
// ExecCmd represents a command that can be executed
15+
type ExecCmd interface {
16+
CombinedOutput() ([]byte, error)
17+
}
18+
19+
// RealExecWrapper implements ExecWrapper using the real exec package
20+
type RealExecWrapper struct{}
21+
22+
// LookPath searches for an executable named file in the directories named by the PATH environment variable
23+
func (r *RealExecWrapper) LookPath(file string) (string, error) {
24+
return exec.LookPath(file)
25+
}
26+
27+
// CommandContext returns the Cmd struct to execute the named program with the given arguments
28+
func (r *RealExecWrapper) CommandContext(ctx context.Context, name string, arg ...string) ExecCmd {
29+
return &RealExecCmd{cmd: exec.CommandContext(ctx, name, arg...)}
30+
}
31+
32+
// RealExecCmd wraps exec.Cmd to implement ExecCmd interface
33+
type RealExecCmd struct {
34+
cmd *exec.Cmd
35+
}
36+
37+
// CombinedOutput runs the command and returns its combined standard output and standard error
38+
func (r *RealExecCmd) CombinedOutput() ([]byte, error) {
39+
return r.cmd.CombinedOutput()
40+
}
41+
42+
// Global instance that can be overridden in tests
43+
var execWrapper ExecWrapper = &RealExecWrapper{}

pkg/perf/iperf/graphql_wrapper.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package iperf
2+
3+
import (
4+
"context"
5+
6+
"github.com/threefoldtech/zosbase/pkg/perf/graphql"
7+
)
8+
9+
// GraphQLClient interface for mocking GraphQL operations
10+
type GraphQLClient interface {
11+
GetUpNodes(ctx context.Context, nodesNum int, farmID, excludeFarmID uint32, ipv4, ipv6 bool) ([]graphql.Node, error)
12+
}
13+
14+
// GraphQLClientWrapper wraps the real GraphQL client to implement the interface
15+
type GraphQLClientWrapper struct {
16+
client graphql.GraphQl
17+
}
18+
19+
// NewGraphQLClientWrapper creates a new wrapper for the GraphQL client
20+
func NewGraphQLClientWrapper(client graphql.GraphQl) GraphQLClient {
21+
return &GraphQLClientWrapper{client: client}
22+
}
23+
24+
// GetUpNodes calls the underlying GraphQL client's GetUpNodes method
25+
func (g *GraphQLClientWrapper) GetUpNodes(ctx context.Context, nodesNum int, farmID, excludeFarmID uint32, ipv4, ipv6 bool) ([]graphql.Node, error) {
26+
return g.client.GetUpNodes(ctx, nodesNum, farmID, excludeFarmID, ipv4, ipv6)
27+
}

pkg/perf/iperf/iperf_task.go

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,11 @@ const (
2929
)
3030

3131
// IperfTest for iperf tcp/udp tests
32-
type IperfTest struct{}
32+
type IperfTest struct {
33+
// Optional dependencies for testing
34+
graphqlClient GraphQLClient
35+
execWrapper ExecWrapper
36+
}
3337

3438
// IperfResult for iperf test results
3539
type IperfResult struct {
@@ -75,10 +79,19 @@ func (t *IperfTest) Jitter() uint32 {
7579

7680
// Run runs the tcp test and returns the result
7781
func (t *IperfTest) Run(ctx context.Context) (interface{}, error) {
78-
env := environment.MustGet()
79-
g, err := graphql.NewGraphQl(env.GraphQL...)
80-
if err != nil {
81-
return nil, err
82+
var g GraphQLClient
83+
var err error
84+
85+
// Use injected GraphQL client if available, otherwise create a new one
86+
if t.graphqlClient != nil {
87+
g = t.graphqlClient
88+
} else {
89+
env := environment.MustGet()
90+
graphqlClient, err := graphql.NewGraphQl(env.GraphQL...)
91+
if err != nil {
92+
return nil, err
93+
}
94+
g = NewGraphQLClientWrapper(graphqlClient)
8295
}
8396

8497
// get public up nodes
@@ -94,7 +107,13 @@ func (t *IperfTest) Run(ctx context.Context) (interface{}, error) {
94107

95108
nodes = append(nodes, freeFarmNodes...)
96109

97-
_, err = exec.LookPath("iperf")
110+
// Use injected exec wrapper if available
111+
execWrap := execWrapper
112+
if t.execWrapper != nil {
113+
execWrap = t.execWrapper
114+
}
115+
116+
_, err = execWrap.LookPath("iperf")
98117
if err != nil {
99118
return nil, err
100119
}
@@ -150,9 +169,15 @@ func (t *IperfTest) runIperfTest(ctx context.Context, clientIP string, tcp bool)
150169
opts = append(opts, "--length", "16B", "--udp")
151170
}
152171

172+
// Use injected exec wrapper if available
173+
execWrap := execWrapper
174+
if t.execWrapper != nil {
175+
execWrap = t.execWrapper
176+
}
177+
153178
var report iperfCommandOutput
154179
operation := func() error {
155-
res := runIperfCommand(ctx, opts)
180+
res := runIperfCommand(ctx, opts, execWrap)
156181
if res.Error == errServerBusy {
157182
return errors.New(errServerBusy)
158183
}
@@ -196,8 +221,8 @@ func (t *IperfTest) runIperfTest(ctx context.Context, clientIP string, tcp bool)
196221
return iperfResult
197222
}
198223

199-
func runIperfCommand(ctx context.Context, opts []string) iperfCommandOutput {
200-
output, err := exec.CommandContext(ctx, "iperf", opts...).CombinedOutput()
224+
func runIperfCommand(ctx context.Context, opts []string, execWrap ExecWrapper) iperfCommandOutput {
225+
output, err := execWrap.CommandContext(ctx, "iperf", opts...).CombinedOutput()
201226
exitErr := &exec.ExitError{}
202227
if err != nil && !errors.As(err, &exitErr) {
203228
log.Err(err).Msg("failed to run iperf")

pkg/perf/iperf/iperf_task_test.go

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
package iperf
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"errors"
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
"github.com/threefoldtech/zosbase/pkg/perf/graphql"
11+
"go.uber.org/mock/gomock"
12+
)
13+
14+
func TestIperfTest_Run_Success(t *testing.T) {
15+
ctrl := gomock.NewController(t)
16+
defer ctrl.Finish()
17+
18+
mockGraphQL := NewMockGraphQLClient(ctrl)
19+
mockExec := NewMockExecWrapper(ctrl)
20+
mockCmd := NewMockExecCmd(ctrl)
21+
22+
task := &IperfTest{
23+
graphqlClient: mockGraphQL,
24+
execWrapper: mockExec,
25+
}
26+
27+
testNodes := []graphql.Node{
28+
{
29+
NodeID: 123,
30+
PublicConfig: graphql.PublicConfig{
31+
Ipv4: "192.168.1.100/24",
32+
Ipv6: "2001:db8::1/64",
33+
},
34+
},
35+
}
36+
37+
mockGraphQL.EXPECT().
38+
GetUpNodes(gomock.Any(), 0, uint32(1), uint32(0), true, true).
39+
Return([]graphql.Node{testNodes[0]}, nil)
40+
41+
mockGraphQL.EXPECT().
42+
GetUpNodes(gomock.Any(), 12, uint32(0), uint32(1), true, true).
43+
Return([]graphql.Node{}, nil)
44+
45+
mockExec.EXPECT().
46+
LookPath("iperf").
47+
Return("/usr/bin/iperf", nil)
48+
49+
tcpOutput := createMockIperfOutput(false, 1000000, 2000000)
50+
udpOutput := createMockIperfOutput(true, 500000, 800000)
51+
52+
tcpOutputBytes, _ := json.Marshal(tcpOutput)
53+
udpOutputBytes, _ := json.Marshal(udpOutput)
54+
55+
mockExec.EXPECT().
56+
CommandContext(gomock.Any(), "iperf", gomock.Any()).
57+
Return(mockCmd).
58+
Times(4)
59+
60+
mockCmd.EXPECT().CombinedOutput().Return(tcpOutputBytes, nil)
61+
mockCmd.EXPECT().CombinedOutput().Return(tcpOutputBytes, nil)
62+
mockCmd.EXPECT().CombinedOutput().Return(udpOutputBytes, nil)
63+
mockCmd.EXPECT().CombinedOutput().Return(udpOutputBytes, nil)
64+
65+
ctx := context.Background()
66+
result, err := task.Run(ctx)
67+
68+
assert.NoError(t, err)
69+
assert.NotNil(t, result)
70+
71+
results, ok := result.([]IperfResult)
72+
assert.True(t, ok)
73+
assert.Len(t, results, 4)
74+
75+
firstResult := results[0]
76+
assert.Equal(t, uint32(123), firstResult.NodeID)
77+
assert.Equal(t, "192.168.1.100", firstResult.NodeIpv4)
78+
assert.Equal(t, "tcp", firstResult.TestType)
79+
assert.Equal(t, float64(1000000), firstResult.UploadSpeed)
80+
assert.Equal(t, float64(2000000), firstResult.DownloadSpeed)
81+
}
82+
83+
func TestIperfTest_Run_GraphQLError(t *testing.T) {
84+
ctrl := gomock.NewController(t)
85+
defer ctrl.Finish()
86+
87+
mockGraphQL := NewMockGraphQLClient(ctrl)
88+
89+
// Create test task with injected dependencies
90+
task := &IperfTest{
91+
graphqlClient: mockGraphQL,
92+
}
93+
94+
// Mock GraphQL error
95+
mockGraphQL.EXPECT().
96+
GetUpNodes(gomock.Any(), 0, uint32(1), uint32(0), true, true).
97+
Return(nil, errors.New("graphql connection failed"))
98+
99+
// Execute the test
100+
ctx := context.Background()
101+
result, err := task.Run(ctx)
102+
103+
// Verify error
104+
assert.Error(t, err)
105+
assert.Nil(t, result)
106+
assert.Contains(t, err.Error(), "failed to list freefarm nodes from graphql")
107+
}
108+
109+
func TestIperfTest_Run_IperfNotFound(t *testing.T) {
110+
ctrl := gomock.NewController(t)
111+
defer ctrl.Finish()
112+
113+
mockGraphQL := NewMockGraphQLClient(ctrl)
114+
mockExec := NewMockExecWrapper(ctrl)
115+
116+
task := &IperfTest{
117+
graphqlClient: mockGraphQL,
118+
execWrapper: mockExec,
119+
}
120+
121+
mockGraphQL.EXPECT().
122+
GetUpNodes(gomock.Any(), 0, uint32(1), uint32(0), true, true).
123+
Return([]graphql.Node{}, nil)
124+
125+
mockGraphQL.EXPECT().
126+
GetUpNodes(gomock.Any(), 12, uint32(0), uint32(1), true, true).
127+
Return([]graphql.Node{}, nil)
128+
129+
mockExec.EXPECT().
130+
LookPath("iperf").
131+
Return("", errors.New("executable file not found in $PATH"))
132+
133+
ctx := context.Background()
134+
result, err := task.Run(ctx)
135+
136+
assert.Error(t, err)
137+
assert.Nil(t, result)
138+
}
139+
140+
func TestIperfTest_Run_InvalidIPAddress(t *testing.T) {
141+
ctrl := gomock.NewController(t)
142+
defer ctrl.Finish()
143+
144+
mockGraphQL := NewMockGraphQLClient(ctrl)
145+
mockExec := NewMockExecWrapper(ctrl)
146+
147+
task := &IperfTest{
148+
graphqlClient: mockGraphQL,
149+
execWrapper: mockExec,
150+
}
151+
152+
testNodes := []graphql.Node{
153+
{
154+
NodeID: 123,
155+
PublicConfig: graphql.PublicConfig{
156+
Ipv4: "invalid-ip",
157+
Ipv6: "invalid-ipv6",
158+
},
159+
},
160+
}
161+
162+
mockGraphQL.EXPECT().
163+
GetUpNodes(gomock.Any(), 0, uint32(1), uint32(0), true, true).
164+
Return(testNodes, nil)
165+
166+
mockGraphQL.EXPECT().
167+
GetUpNodes(gomock.Any(), 12, uint32(0), uint32(1), true, true).
168+
Return([]graphql.Node{}, nil)
169+
170+
mockExec.EXPECT().
171+
LookPath("iperf").
172+
Return("/usr/bin/iperf", nil)
173+
174+
ctx := context.Background()
175+
result, err := task.Run(ctx)
176+
177+
assert.NoError(t, err)
178+
results, ok := result.([]IperfResult)
179+
assert.True(t, ok)
180+
assert.Len(t, results, 0)
181+
}
182+
183+
func TestNewTask(t *testing.T) {
184+
task := NewTask()
185+
assert.NotNil(t, task)
186+
assert.Equal(t, "iperf", task.ID())
187+
}
188+
189+
// Helper function to create mock iperf output
190+
func createMockIperfOutput(isUDP bool, uploadSpeed, downloadSpeed float64) iperfCommandOutput {
191+
output := iperfCommandOutput{
192+
End: End{
193+
SumSent: Sum{
194+
BitsPerSecond: uploadSpeed,
195+
},
196+
SumReceived: Sum{
197+
BitsPerSecond: downloadSpeed,
198+
},
199+
CPUUtilizationPercent: CPUUtilizationPercent{
200+
HostTotal: 10.5,
201+
RemoteTotal: 8.2,
202+
},
203+
},
204+
}
205+
206+
if isUDP {
207+
output.End.Streams = []EndStream{
208+
{
209+
UDP: UDPSum{
210+
BitsPerSecond: downloadSpeed,
211+
},
212+
},
213+
}
214+
}
215+
216+
return output
217+
}

0 commit comments

Comments
 (0)