blob: dfaae44eeffd4a3d352c06904833d2605b6ae68c [file] [log] [blame]
Maros Ondrejicka7943c902022-11-08 08:00:51 +01001Host stack test framework
2=========================
3
4Overview
5--------
6
7The goal of the Host stack test framework (**hs-test**) is to ease writing and running end-to-end tests for VPP's Host Stack.
8End-to-end tests often want multiple VPP instances, network namespaces, different types of interfaces
9and to execute external tools or commands. With such requirements the existing VPP test framework is not sufficient.
10For this, ``Go`` was chosen as a high level language, allowing rapid development, with ``Docker`` and ``ip`` being the tools for creating required topology.
11
12Go's package `testing`_ together with `go test`_ command form the base framework upon which the *hs-test* is built and run.
13
14Anatomy of a test case
15----------------------
16
17**Prerequisites**:
18
19* Tests use *hs-test*'s own docker image, so building it before starting tests is a prerequisite. Run ``sudo make`` to do so
20* Docker has to be installed and Go has to be in path of both the running user and root
21* Root privileges are required to run tests as it uses Linux ``ip`` command for configuring topology
22
23**Action flow when running a test case**:
24
25#. It starts with running ``./test``. This script is basically a wrapper for ``go test`` and accepts its parameters,
26 for example following runs a specific test: ``./test -run TestNs/TestHttpTps``
27#. ``go test`` compiles package ``main`` along with any files with names matching the file pattern ``*_test.go``
28 and then runs the resulting test binaries
29#. The go test framework runs each function matching :ref:`naming convention<test-convention>`. Each of these corresponds to a `test suite`_
30#. Testify toolkit's ``suite.Run(t *testing.T, suite TestingSuite)`` function runs the suite and does the following:
31
32 #. Suite is initialized. The topology is loaded and configured in this step
33 #. Test suite runs all the tests attached to it
34 #. Execute tear-down functions, which currently consists of stopping running containers
35 and clean-up of test topology
36
37Adding a test case
38------------------
39
40This describes adding a new test case to an existing suite.
41For adding a new suite, please see `Modifying the framework`_ below.
42
43#. To write a new test case, create a file whose name ends with ``_test.go`` or pick one that already exists
44#. Declare method whose name starts with ``Test`` and specifies its receiver as a pointer to the suite's struct (defined in ``framework_test.go``)
45#. Implement test behaviour inside the test method. This typically includes the following:
46
47 #. Start docker container(s) as needed. Function ``dockerRun(instance, args string)``
48 from ``utils.go`` serves this purpose. Alternatively use suite struct's ``NewContainer(name string)`` method
49 #. Execute *hs-test* action(s) inside any of the running containers.
50 Function ``hstExec`` from ``utils.go`` does this by using ``docker exec`` command to run ``hs-test`` executable.
51 For starting an VPP instance inside a container, the ``Vpp`` struct can be used as a forward-looking alternative
52 #. Run arbitrary commands inside the containers with ``dockerExec(cmd string, instance string)``
53 #. Run other external tool with one of the preexisting functions in the ``utils.go`` file.
54 For example, use ``wget`` with ``startWget(..)`` function
55 #. Use ``exechelper`` or just plain ``exec`` packages to run whatever else
56 #. ``defer func() { exechelper.Run("docker stop <container-name>) }()`` inside the method body,
57 to stop the running container(s). It's not necessary to do this if containers were created
58 with suite's ``NewContainer(..)`` method
59
60**Example test case**
61
62Two docker containers, each with its own VPP instance running. One VPP then pings the other.
63This can be put in file ``extras/hs-test/my_test.go`` and run with command ``./test -run TestMySuite``.
64
65::
66
67 package main
68
69 import (
70 "fmt"
71 "github.com/edwarnicke/exechelper"
72 )
73
74 func (s *MySuite) TestMyCase() {
75 t := s.T()
76
77 vpp1Instance := "vpp-1"
78 vpp2Instance := "vpp-2"
79
80 err := dockerRun(vpp1Instance, "")
81 if err != nil {
82 t.Errorf("%v", err)
83 return
84 }
85 defer func() { exechelper.Run("docker stop " + vpp1Instance) }()
86
87 err = dockerRun(vpp2Instance, "")
88 if err != nil {
89 t.Errorf("%v", err)
90 return
91 }
92 defer func() { exechelper.Run("docker stop " + vpp2Instance) }()
93
94 _, err = hstExec("Configure2Veths srv", vpp1Instance)
95 if err != nil {
96 t.Errorf("%v", err)
97 return
98 }
99
100 _, err = hstExec("Configure2Veths cln", vpp2Instance)
101 if err != nil {
102 t.Errorf("%v", err)
103 return
104 }
105
106 // ping one VPP from the other
107 //
108 // not using dockerExec because it executes in detached mode
109 // and we want to capture output from ping and show it
110 command := "docker exec --detach=false vpp-1 vppctl -s /tmp/2veths/var/run/vpp/cli.sock ping 10.10.10.2"
111 output, err := exechelper.CombinedOutput(command)
112 if err != nil {
113 t.Errorf("ping failed: %v", err)
114 }
115 fmt.Println(string(output))
116 }
117
118Modifying the framework
119-----------------------
120
121**Adding a test suite**
122
123.. _test-convention:
124
125#. Adding a new suite takes place in ``framework_test.go``
126
127#. Make a ``struct`` with at least ``HstSuite`` struct and a ``teardownSuite`` function as its members.
128 HstSuite provides functionality that can be shared for all suites, like starting containers
129
130 ::
131
132 type MySuite struct {
133 HstSuite
134 teardownSuite func()
135 }
136
137#. Implement SetupSuite method which testify runs before running the tests.
138 It's important here to call ``setupSuite(s *suite.Suite, topologyName string)`` and assign its result to the suite's ``teardownSuite`` member.
139 Pass the topology name to the function in the form of file name of one of the *yaml* files in ``topo`` folder.
140 Without the extension. In this example, *myTopology* corresponds to file ``extras/hs-test/topo/myTopology.yaml``
141
142 ::
143
144 func (s *MySuite) SetupSuite() {
145 // Add custom setup code here
146
147 s.teardownSuite = setupSuite(&s.Suite, "myTopology")
148 }
149
150#. Implement TearDownSuite method which testify runs after the tests, to clean-up.
151 It's good idea to add at least the suite's own ``teardownSuite()``
152 and HstSuite upper suite's ``stopContainers()`` methods
153
154 ::
155
156 func (s *MySuite) TearDownSuite() {
157 s.teardownSuite()
158 s.StopContainers()
159 }
160
161#. In order for ``go test`` to run this suite, we need to create a normal test function and pass our suite to ``suite.Run``
162
163 ::
164
165 func TestMySuite(t *testing.T) {
166 var m MySuite
167 suite.Run(t, &m)
168 }
169
170#. Next step is to add test cases to the suite. For that, see section `Adding a test case`_ above
171
172**Adding a topology element**
173
174Topology configuration exists as ``yaml`` files in the ``extras/hs-test/topo`` folder.
175Processing of a file for a particular test suite is started by the ``setupSuite`` function depending on which file's name is passed to it.
176Specified file is loaded by ``LoadTopology()`` function and converted into internal data structures which represent various elements of the topology.
177After parsing the configuration, ``Configure()`` method loops over array of topology elements and configures them one by one.
178
179These are currently supported types of elements.
180
181* ``netns`` - network namespace
182* ``veth`` - veth network interface, optionally with target network namespace or IPv4 address
183* ``bridge`` - ethernet bridge to connect created interfaces, optionally with target network namespace
184* ``tap`` - tap network interface with IP address
185
186Supporting a new type of topology element requires adding code to recognize the new element type during loading.
187And adding code to set up the element in the host system with some Linux tool, such as *ip*. This should be implemented in ``netconfig.go``.
188
189**Communicating between containers**
190
191When two VPP instances or other applications, each in its own Docker container,
192want to communicate there are typically two ways this can be done within *hs-test*.
193
194* Network interfaces. Containers are being created with ``-d --network host`` options,
195 so they are connected with interfaces created in host system
196* Shared folders. Containers are being created with ``-v`` option to create shared `volumes`_ between host system and containers
197 or just between containers
198
199**Adding a hs-test action**
200
201Executing more complex or long running jobs is made easier by *hs-test* actions.
202These are functions that compartmentalize configuration and execution together for a specific task.
203For example, starting up VPP or running VCL echo client.
204
205The actions are located in ``extras/hs-test/actions.go``. To add one, create a new method that has its receiver as a pointer to ``Actions`` struct.
206
207Run it from test case with ``hstExec(args, instance)`` where ``args`` is the action method's name and ``instance`` is target Docker container's name.
208This then executes the ``hs-test`` binary inside of the container and it then runs selected action.
209Action is specified by its name as first argument for the binary.
210
211*Note*: When ``hstExec(..)`` runs some action from a test case, the execution of ``hs-test`` inside the container
212is asynchronous. The action might take many seconds to finish, while the test case execution context continues to run.
213To mitigate this, ``hstExec(..)`` waits pre-defined arbitrary number of seconds for a *sync file* to be written by ``hs-test``
214at the end of its run. The test case context and container use Docker volume to share the file.
215
216**Adding an external tool**
217
218If an external program should be executed as part of a test case, it might be useful to wrap its execution in its own function.
219These types of functions are placed in the ``utils.go`` file. If the external program is not available by default in Docker image,
220add its installation to ``extras/hs-test/Dockerfile.vpp`` in ``apt-get install`` command.
221Alternatively copy the executable from host system to the Docker image, similarly how the VPP executables and libraries are being copied.
222
223**Eternal dependencies**
224
225* Linux tools ``ip``, ``brctl``
226* Standalone programs ``wget``, ``iperf3`` - since these are downloaded when Docker image is made,
227 they are reasonably up-to-date automatically
228* Programs in Docker images - see ``envoyproxy/envoy-contrib`` in ``utils.go``
229* ``http_server`` - homegrown application that listens on specified address and sends a test file in response
230* Non-standard Go libraries - see ``extras/hs-test/go.mod``
231
232Generally, these will be updated on a per-need basis, for example when a bug is discovered
233or a new version incompatibility issue occurs.
234
235
236.. _testing: https://pkg.go.dev/testing
237.. _go test: https://pkg.go.dev/cmd/go#hdr-Test_packages
238.. _test suite: https://github.com/stretchr/testify#suite-package
239.. _volumes: https://docs.docker.com/storage/volumes/
240