hs-test: use relative paths for docker volumes
[vpp.git] / extras / hs-test / hst_suite.go
1 package main
2
3 import (
4         "errors"
5         "flag"
6         "fmt"
7         "os"
8         "os/exec"
9         "strings"
10         "time"
11
12         "github.com/edwarnicke/exechelper"
13         "github.com/stretchr/testify/assert"
14         "github.com/stretchr/testify/suite"
15         "gopkg.in/yaml.v3"
16 )
17
18 const (
19         DEFAULT_NETWORK_NUM int = 1
20 )
21
22 var isPersistent = flag.Bool("persist", false, "persists topology config")
23 var isVerbose = flag.Bool("verbose", false, "verbose test output")
24 var isUnconfiguring = flag.Bool("unconfigure", false, "remove topology")
25 var isVppDebug = flag.Bool("debug", false, "attach gdb to vpp")
26 var nConfiguredCpus = flag.Int("cpus", 1, "number of CPUs assigned to vpp")
27 var vppSourceFileDir = flag.String("vppsrc", "", "vpp source file directory")
28
29 type HstSuite struct {
30         suite.Suite
31         containers       map[string]*Container
32         volumes          []string
33         netConfigs       []NetConfig
34         netInterfaces    map[string]*NetInterface
35         ip4AddrAllocator *Ip4AddressAllocator
36         testIds          map[string]string
37         cpuAllocator     *CpuAllocatorT
38         cpuContexts      []*CpuContext
39         cpuPerVpp        int
40 }
41
42 func (s *HstSuite) SetupSuite() {
43         var err error
44         s.cpuAllocator, err = CpuAllocator()
45         if err != nil {
46                 s.FailNow("failed to init cpu allocator: %v", err)
47         }
48         s.cpuPerVpp = *nConfiguredCpus
49 }
50
51 func (s *HstSuite) AllocateCpus() []int {
52         cpuCtx, err := s.cpuAllocator.Allocate(s.cpuPerVpp)
53         s.assertNil(err)
54         s.AddCpuContext(cpuCtx)
55         return cpuCtx.cpus
56 }
57
58 func (s *HstSuite) AddCpuContext(cpuCtx *CpuContext) {
59         s.cpuContexts = append(s.cpuContexts, cpuCtx)
60 }
61
62 func (s *HstSuite) TearDownSuite() {
63         s.unconfigureNetworkTopology()
64 }
65
66 func (s *HstSuite) TearDownTest() {
67         if *isPersistent {
68                 return
69         }
70         for _, c := range s.cpuContexts {
71                 c.Release()
72         }
73         s.resetContainers()
74         s.removeVolumes()
75 }
76
77 func (s *HstSuite) skipIfUnconfiguring() {
78         if *isUnconfiguring {
79                 s.skip("skipping to unconfigure")
80         }
81 }
82
83 func (s *HstSuite) SetupTest() {
84         s.skipIfUnconfiguring()
85         s.setupVolumes()
86         s.setupContainers()
87 }
88
89 func (s *HstSuite) setupVolumes() {
90         for _, volume := range s.volumes {
91                 cmd := "docker volume create --name=" + volume
92                 s.log(cmd)
93                 exechelper.Run(cmd)
94         }
95 }
96
97 func (s *HstSuite) setupContainers() {
98         for _, container := range s.containers {
99                 if !container.isOptional {
100                         container.run()
101                 }
102         }
103 }
104
105 func (s *HstSuite) hstFail() {
106         s.T().FailNow()
107 }
108
109 func (s *HstSuite) assertNil(object interface{}, msgAndArgs ...interface{}) {
110         if !assert.Nil(s.T(), object, msgAndArgs...) {
111                 s.hstFail()
112         }
113 }
114
115 func (s *HstSuite) assertNotNil(object interface{}, msgAndArgs ...interface{}) {
116         if !assert.NotNil(s.T(), object, msgAndArgs...) {
117                 s.hstFail()
118         }
119 }
120
121 func (s *HstSuite) assertEqual(expected, actual interface{}, msgAndArgs ...interface{}) {
122         if !assert.Equal(s.T(), expected, actual, msgAndArgs...) {
123                 s.hstFail()
124         }
125 }
126
127 func (s *HstSuite) assertNotEqual(expected, actual interface{}, msgAndArgs ...interface{}) {
128         if !assert.NotEqual(s.T(), expected, actual, msgAndArgs...) {
129                 s.hstFail()
130         }
131 }
132
133 func (s *HstSuite) assertContains(testString, contains interface{}, msgAndArgs ...interface{}) {
134         if !assert.Contains(s.T(), testString, contains, msgAndArgs...) {
135                 s.hstFail()
136         }
137 }
138
139 func (s *HstSuite) assertNotContains(testString, contains interface{}, msgAndArgs ...interface{}) {
140         if !assert.NotContains(s.T(), testString, contains, msgAndArgs...) {
141                 s.hstFail()
142         }
143 }
144
145 func (s *HstSuite) assertNotEmpty(object interface{}, msgAndArgs ...interface{}) {
146         if !assert.NotEmpty(s.T(), object, msgAndArgs...) {
147                 s.hstFail()
148         }
149 }
150
151 func (s *HstSuite) log(args ...any) {
152         if *isVerbose {
153                 s.T().Helper()
154                 s.T().Log(args...)
155         }
156 }
157
158 func (s *HstSuite) skip(args ...any) {
159         s.log(args...)
160         s.T().SkipNow()
161 }
162
163 func (s *HstSuite) SkipIfMultiWorker(args ...any) {
164         if *nConfiguredCpus > 1 {
165                 s.skip("test case not supported with multiple vpp workers")
166         }
167 }
168
169 func (s *HstSuite) SkipUnlessExtendedTestsBuilt() {
170         imageName := "hs-test/nginx-http3"
171
172         cmd := exec.Command("docker", "images", imageName)
173         byteOutput, err := cmd.CombinedOutput()
174         if err != nil {
175                 s.log("error while searching for docker image")
176                 return
177         }
178         if !strings.Contains(string(byteOutput), imageName) {
179                 s.skip("extended tests not built")
180         }
181 }
182
183 func (s *HstSuite) resetContainers() {
184         for _, container := range s.containers {
185                 container.stop()
186         }
187 }
188
189 func (s *HstSuite) removeVolumes() {
190         for _, volumeName := range s.volumes {
191                 cmd := "docker volume rm " + volumeName
192                 exechelper.Run(cmd)
193                 os.RemoveAll(volumeName)
194         }
195 }
196
197 func (s *HstSuite) getContainerByName(name string) *Container {
198         return s.containers[name]
199 }
200
201 /*
202  * Create a copy and return its address, so that individial tests which call this
203  * are not able to modify the original container and affect other tests by doing that
204  */
205 func (s *HstSuite) getTransientContainerByName(name string) *Container {
206         containerCopy := *s.containers[name]
207         return &containerCopy
208 }
209
210 func (s *HstSuite) loadContainerTopology(topologyName string) {
211         data, err := os.ReadFile(containerTopologyDir + topologyName + ".yaml")
212         if err != nil {
213                 s.T().Fatalf("read error: %v", err)
214         }
215         var yamlTopo YamlTopology
216         err = yaml.Unmarshal(data, &yamlTopo)
217         if err != nil {
218                 s.T().Fatalf("unmarshal error: %v", err)
219         }
220
221         for _, elem := range yamlTopo.Volumes {
222                 volumeMap := elem["volume"].(VolumeConfig)
223                 hostDir := volumeMap["host-dir"].(string)
224                 workingVolumeDir := logDir + s.T().Name() + volumeDir
225                 volDirReplacer := strings.NewReplacer("$HST_VOLUME_DIR", workingVolumeDir)
226                 hostDir = volDirReplacer.Replace(hostDir)
227                 s.volumes = append(s.volumes, hostDir)
228         }
229
230         s.containers = make(map[string]*Container)
231         for _, elem := range yamlTopo.Containers {
232                 newContainer, err := newContainer(s, elem)
233                 if err != nil {
234                         s.T().Fatalf("container config error: %v", err)
235                 }
236                 s.containers[newContainer.name] = newContainer
237         }
238 }
239
240 func (s *HstSuite) loadNetworkTopology(topologyName string) {
241         data, err := os.ReadFile(networkTopologyDir + topologyName + ".yaml")
242         if err != nil {
243                 s.T().Fatalf("read error: %v", err)
244         }
245         var yamlTopo YamlTopology
246         err = yaml.Unmarshal(data, &yamlTopo)
247         if err != nil {
248                 s.T().Fatalf("unmarshal error: %v", err)
249         }
250
251         s.ip4AddrAllocator = NewIp4AddressAllocator()
252         s.netInterfaces = make(map[string]*NetInterface)
253         for _, elem := range yamlTopo.Devices {
254                 switch elem["type"].(string) {
255                 case NetNs:
256                         {
257                                 if namespace, err := newNetNamespace(elem); err == nil {
258                                         s.netConfigs = append(s.netConfigs, &namespace)
259                                 } else {
260                                         s.T().Fatalf("network config error: %v", err)
261                                 }
262                         }
263                 case Veth, Tap:
264                         {
265                                 if netIf, err := newNetworkInterface(elem, s.ip4AddrAllocator); err == nil {
266                                         s.netConfigs = append(s.netConfigs, netIf)
267                                         s.netInterfaces[netIf.Name()] = netIf
268                                 } else {
269                                         s.T().Fatalf("network config error: %v", err)
270                                 }
271                         }
272                 case Bridge:
273                         {
274                                 if bridge, err := newBridge(elem); err == nil {
275                                         s.netConfigs = append(s.netConfigs, &bridge)
276                                 } else {
277                                         s.T().Fatalf("network config error: %v", err)
278                                 }
279                         }
280                 }
281         }
282 }
283
284 func (s *HstSuite) configureNetworkTopology(topologyName string) {
285         s.loadNetworkTopology(topologyName)
286
287         if *isUnconfiguring {
288                 return
289         }
290
291         for _, nc := range s.netConfigs {
292                 if err := nc.configure(); err != nil {
293                         s.T().Fatalf("network config error: %v", err)
294                 }
295         }
296 }
297
298 func (s *HstSuite) unconfigureNetworkTopology() {
299         if *isPersistent {
300                 return
301         }
302         for _, nc := range s.netConfigs {
303                 nc.unconfigure()
304         }
305 }
306
307 func (s *HstSuite) getTestId() string {
308         testName := s.T().Name()
309
310         if s.testIds == nil {
311                 s.testIds = map[string]string{}
312         }
313
314         if _, ok := s.testIds[testName]; !ok {
315                 s.testIds[testName] = time.Now().Format("2006-01-02_15-04-05")
316         }
317
318         return s.testIds[testName]
319 }
320
321 func (s *HstSuite) startServerApp(running chan error, done chan struct{}, env []string) {
322         cmd := exec.Command("iperf3", "-4", "-s")
323         if env != nil {
324                 cmd.Env = env
325         }
326         s.log(cmd)
327         err := cmd.Start()
328         if err != nil {
329                 msg := fmt.Errorf("failed to start iperf server: %v", err)
330                 running <- msg
331                 return
332         }
333         running <- nil
334         <-done
335         cmd.Process.Kill()
336 }
337
338 func (s *HstSuite) startClientApp(ipAddress string, env []string, clnCh chan error, clnRes chan string) {
339         defer func() {
340                 clnCh <- nil
341         }()
342
343         nTries := 0
344
345         for {
346                 cmd := exec.Command("iperf3", "-c", ipAddress, "-u", "-l", "1460", "-b", "10g")
347                 if env != nil {
348                         cmd.Env = env
349                 }
350                 s.log(cmd)
351                 o, err := cmd.CombinedOutput()
352                 if err != nil {
353                         if nTries > 5 {
354                                 clnCh <- fmt.Errorf("failed to start client app '%s'.\n%s", err, o)
355                                 return
356                         }
357                         time.Sleep(1 * time.Second)
358                         nTries++
359                         continue
360                 } else {
361                         clnRes <- fmt.Sprintf("Client output: %s", o)
362                 }
363                 break
364         }
365 }
366
367 func (s *HstSuite) startHttpServer(running chan struct{}, done chan struct{}, addressPort, netNs string) {
368         cmd := newCommand([]string{"./http_server", addressPort}, netNs)
369         err := cmd.Start()
370         s.log(cmd)
371         if err != nil {
372                 fmt.Println("Failed to start http server")
373                 return
374         }
375         running <- struct{}{}
376         <-done
377         cmd.Process.Kill()
378 }
379
380 func (s *HstSuite) startWget(finished chan error, server_ip, port, query, netNs string) {
381         defer func() {
382                 finished <- errors.New("wget error")
383         }()
384
385         cmd := newCommand([]string{"wget", "--timeout=10", "--no-proxy", "--tries=5", "-O", "/dev/null", server_ip + ":" + port + "/" + query},
386                 netNs)
387         s.log(cmd)
388         o, err := cmd.CombinedOutput()
389         if err != nil {
390                 finished <- fmt.Errorf("wget error: '%v\n\n%s'", err, o)
391                 return
392         } else if !strings.Contains(string(o), "200 OK") {
393                 finished <- fmt.Errorf("wget error: response not 200 OK")
394                 return
395         }
396         finished <- nil
397 }