hs-test: log external apps
[vpp.git] / extras / hs-test / hst_suite.go
1 package main
2
3 import (
4         "errors"
5         "flag"
6         "fmt"
7         "io/ioutil"
8         "os"
9         "os/exec"
10         "strings"
11         "time"
12
13         "github.com/edwarnicke/exechelper"
14         "github.com/stretchr/testify/assert"
15         "github.com/stretchr/testify/suite"
16         "gopkg.in/yaml.v3"
17 )
18
19 const (
20         DEFAULT_NETWORK_NUM int = 1
21 )
22
23 var isPersistent = flag.Bool("persist", false, "persists topology config")
24 var isVerbose = flag.Bool("verbose", false, "verbose test output")
25 var isUnconfiguring = flag.Bool("unconfigure", false, "remove topology")
26 var isVppDebug = flag.Bool("debug", false, "attach gdb to vpp")
27 var nConfiguredCpus = flag.Int("cpus", 1, "number of CPUs assigned to vpp")
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 := ioutil.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                 s.volumes = append(s.volumes, hostDir)
225         }
226
227         s.containers = make(map[string]*Container)
228         for _, elem := range yamlTopo.Containers {
229                 newContainer, err := newContainer(elem)
230                 newContainer.suite = s
231                 if err != nil {
232                         s.T().Fatalf("container config error: %v", err)
233                 }
234                 s.containers[newContainer.name] = newContainer
235         }
236 }
237
238 func (s *HstSuite) loadNetworkTopology(topologyName string) {
239         data, err := ioutil.ReadFile(networkTopologyDir + topologyName + ".yaml")
240         if err != nil {
241                 s.T().Fatalf("read error: %v", err)
242         }
243         var yamlTopo YamlTopology
244         err = yaml.Unmarshal(data, &yamlTopo)
245         if err != nil {
246                 s.T().Fatalf("unmarshal error: %v", err)
247         }
248
249         s.ip4AddrAllocator = NewIp4AddressAllocator()
250         s.netInterfaces = make(map[string]*NetInterface)
251         for _, elem := range yamlTopo.Devices {
252                 switch elem["type"].(string) {
253                 case NetNs:
254                         {
255                                 if namespace, err := newNetNamespace(elem); err == nil {
256                                         s.netConfigs = append(s.netConfigs, &namespace)
257                                 } else {
258                                         s.T().Fatalf("network config error: %v", err)
259                                 }
260                         }
261                 case Veth, Tap:
262                         {
263                                 if netIf, err := newNetworkInterface(elem, s.ip4AddrAllocator); err == nil {
264                                         s.netConfigs = append(s.netConfigs, netIf)
265                                         s.netInterfaces[netIf.Name()] = netIf
266                                 } else {
267                                         s.T().Fatalf("network config error: %v", err)
268                                 }
269                         }
270                 case Bridge:
271                         {
272                                 if bridge, err := newBridge(elem); err == nil {
273                                         s.netConfigs = append(s.netConfigs, &bridge)
274                                 } else {
275                                         s.T().Fatalf("network config error: %v", err)
276                                 }
277                         }
278                 }
279         }
280 }
281
282 func (s *HstSuite) configureNetworkTopology(topologyName string) {
283         s.loadNetworkTopology(topologyName)
284
285         if *isUnconfiguring {
286                 return
287         }
288
289         for _, nc := range s.netConfigs {
290                 if err := nc.configure(); err != nil {
291                         s.T().Fatalf("network config error: %v", err)
292                 }
293         }
294 }
295
296 func (s *HstSuite) unconfigureNetworkTopology() {
297         if *isPersistent {
298                 return
299         }
300         for _, nc := range s.netConfigs {
301                 nc.unconfigure()
302         }
303 }
304
305 func (s *HstSuite) getTestId() string {
306         testName := s.T().Name()
307
308         if s.testIds == nil {
309                 s.testIds = map[string]string{}
310         }
311
312         if _, ok := s.testIds[testName]; !ok {
313                 s.testIds[testName] = time.Now().Format("2006-01-02_15-04-05")
314         }
315
316         return s.testIds[testName]
317 }
318
319 func (s *HstSuite) startServerApp(running chan error, done chan struct{}, env []string) {
320         cmd := exec.Command("iperf3", "-4", "-s")
321         if env != nil {
322                 cmd.Env = env
323         }
324         s.log(cmd)
325         err := cmd.Start()
326         if err != nil {
327                 msg := fmt.Errorf("failed to start iperf server: %v", err)
328                 running <- msg
329                 return
330         }
331         running <- nil
332         <-done
333         cmd.Process.Kill()
334 }
335
336 func (s *HstSuite) startClientApp(ipAddress string, env []string, clnCh chan error, clnRes chan string) {
337         defer func() {
338                 clnCh <- nil
339         }()
340
341         nTries := 0
342
343         for {
344                 cmd := exec.Command("iperf3", "-c", ipAddress, "-u", "-l", "1460", "-b", "10g")
345                 if env != nil {
346                         cmd.Env = env
347                 }
348                 s.log(cmd)
349                 o, err := cmd.CombinedOutput()
350                 if err != nil {
351                         if nTries > 5 {
352                                 clnCh <- fmt.Errorf("failed to start client app '%s'.\n%s", err, o)
353                                 return
354                         }
355                         time.Sleep(1 * time.Second)
356                         nTries++
357                         continue
358                 } else {
359                         clnRes <- fmt.Sprintf("Client output: %s", o)
360                 }
361                 break
362         }
363 }
364
365 func (s *HstSuite) startHttpServer(running chan struct{}, done chan struct{}, addressPort, netNs string) {
366         cmd := newCommand([]string{"./http_server", addressPort}, netNs)
367         err := cmd.Start()
368         s.log(cmd)
369         if err != nil {
370                 fmt.Println("Failed to start http server")
371                 return
372         }
373         running <- struct{}{}
374         <-done
375         cmd.Process.Kill()
376 }
377
378 func (s *HstSuite) startWget(finished chan error, server_ip, port, query, netNs string) {
379         defer func() {
380                 finished <- errors.New("wget error")
381         }()
382
383         cmd := newCommand([]string{"wget", "--timeout=10", "--no-proxy", "--tries=5", "-O", "/dev/null", server_ip + ":" + port + "/" + query},
384                 netNs)
385         s.log(cmd)
386         o, err := cmd.CombinedOutput()
387         if err != nil {
388                 finished <- fmt.Errorf("wget error: '%v\n\n%s'", err, o)
389                 return
390         } else if !strings.Contains(string(o), "200 OK") {
391                 finished <- fmt.Errorf("wget error: response not 200 OK")
392                 return
393         }
394         finished <- nil
395 }