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